React 18 - performance improvements
Marko Rajević
Posted on May 29, 2022
Recently React released version 18 with some great features.
In this post we will take closer look at performance related features.
useTransition
It's part of the concurrency concept where you can prioritize state updates.
Urgent state updates can be prioritized over less urgent (blocking) updates.
How to use it and how this new hook improves your app performance will learn in the example which can be found here.
This is our example. It's a simple one, we have a button that opens a modal, and within the modal, we render a list of 500 comments.
500 comments is a lot but this will work just fine on most devices.
import { useState } from "react";
import Comments from "../components/Comments";
import Modal from "../components/Modal";
import data from "../data/index.json";
export default function Home() {
const [isOpen, setIsOpen] = useState(false);
const [comments, setComments] = useState([]);
return (
<div className="p-4">
<button
className="px-4 py-2 border-none rounded-sm bg-blue-800 text-white"
onClick={() => {
setIsOpen(true);
setComments(data);
}}
>
Toggle modal
</button>
<Modal
isOpen={isOpen}
onClose={() => {
setIsOpen(false);
setComments([]);
}}
>
<Comments comments={comments} />
</Modal>
</div>
);
}
But, if we slow down the rendering of the Comment
component 😈 things get more interesting.
To achieve that I added for
loop to iterate one milion times.
const Comment = ({ name, email, body, className, onClick }: CommentProps) => {
const soooSloww = [];
for (let i = 0; i < 1000000; i++) {
soooSloww.push(i);
}
return (
<article className={className} onClick={onClick}>
<h3 className="font-semibold">{name}</h3>
<h4 className="text-gray-500 italic">{email}</h4>
<p>{body}</p>
</article>
);
};
Now, when you click the button to open the modal nothing happens for a few seconds.
That's because the browser is busy rendering slow 500 Comment
components.
After some time the modal and the comments are rendered.
From a user perspective, this is very bad UX.
How to improve it?
We can prioritize renders, and in our example, it's more important to first render the modal and after that comments.
useTransition
hook returns two variables, pending
which is a boolean flag that the transition is not yet finished, and startTransition
function where you execute your less important state updates.
const [pending, startTransition] = useTransition();
Now, our example look like this
export default function Home() {
const [isOpen, setIsOpen] = useState(false);
const [comments, setComments] = useState([]);
const [pending, startTransition] = useTransition();
return (
<div className="p-4">
<button
className="px-4 py-2 border-none rounded-sm bg-blue-800 text-white"
onClick={() => {
setIsOpen(true);
startTransition(() => {
setComments(data);
});
}}
>
Toggle modal
</button>
<Modal
isOpen={isOpen}
onClose={() => {
setIsOpen(false);
setComments([]);
}}
>
{pending ? "Loading..." : <Comments comments={comments} />}
</Modal>
</div>
);
}
You can notice that on the button click we update the state to show the modal, which is the action with the higher priority, and update the comments state within the startTransition
function which tells React that state update is with lower priority.
Also, we used the pending
flag to show a user the' Loading...' text while slow comments are rendered.
Now, after clicking the button you will immediately get the modal which looks like this:
Much better user experience! 😀
useDeferredValue
This hook also tells React that certain state updates have a lower priority.
It is similar to the useTransition
and to be honest I'm not sure what are use cases when you should prefer useDeferredValue
over useTransition
, if you have an idea please let me know in the comments. 👇
Our previous example now looks like this and behaves similarly except we don't have the pending
flag.
export default function UseDeferredValues() {
const [isOpen, setIsOpen] = useState(false);
const [comments, setComments] = useState([]);
const commentsToRender = useDeferredValue(comments);
return (
<div className="p-4">
<button
className="px-4 py-2 border-none rounded-sm bg-blue-800 text-white"
onClick={() => {
setIsOpen(true);
setComments(data);
}}
>
Toggle modal
</button>
<Modal
isOpen={isOpen}
onClose={() => {
setIsOpen(false);
setComments([]);
}}
>
<Comments comments={commentsToRender} />
</Modal>
</div>
);
}
Automatic batching
When you are working with React you should aim to have rerenders as less as possible.
Now React 18 helps you to achieve that with automatic batching.
Earlier versions of React batched multiple state updates only inside React event handlers like onClick
or onChange
to avoid multiple re-renders and improve performance.
Now, React batches state updates in React events handlers, promises, setTimeout, native event handlers and so on.
const AutomaticBatching = () => {
const [countOne, setCountOne] = useState(0);
const [countTwo, setCountTwo] = useState(0);
console.log("render");
const onClick = useCallback(() => {
setCountOne(countOne + 1);
setCountTwo(countTwo + 1);
}, [countOne, countTwo]);
useEffect(() => {
document.getElementById("native-event").addEventListener("click", onClick);
return () =>
document
.getElementById("native-event")
.removeEventListener("click", onClick);
}, [onClick]);
const onClickAsync = () => {
fetch("https://jsonplaceholder.typicode.com/todos/1").then(() => {
setCountOne(countOne + 1);
setCountTwo(countTwo + 1);
});
};
const onClickTimeout = () =>
setTimeout(() => {
setCountOne(countOne + 1);
setCountTwo(countTwo + 1);
}, 200);
return (
<div className="p-4">
<ul className="mb-8">
<li>Count one: {countOne}</li>
<li>Count two: {countTwo}</li>
</ul>
<Button onClick={onClick}>Batching in click event</Button>
<Button id="native-event" className="ml-4">
Batching in native click event
</Button>
<Button className="ml-4" onClick={onClickAsync}>
Batching in fetch
</Button>
<Button className="ml-4" onClick={onClickTimeout}>
Batching in timeout
</Button>
</div>
);
};
In this example, you can see that in every event handler we have two state changes but only one rerender. You can notice that with one console.log for each event.
Improved Suspense
Suspense
works with React.lazy
in that way that suspends component rendering until it's loaded and during that time renders a fallback.
const LazyComponent = lazy(() => import("../components/LazyComponent"));
<Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</Suspense>
This is a great way to improve performance in that way you will not include in the initial bundle some parts of the app that you don't need immediately (e.g. modals).
But, Suspense
is not a new feature, it existed in the previous versions of React, the new is that now it works with server-side rendering which wasn't the case before.
That's all folks, hope you like the new version of React. 😀
All examples from above can find it here.
Posted on May 29, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 28, 2024