DEV Community

Marko Rajević
Marko Rajević

Posted on • Edited on

React 18 - performance improvements

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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:

Image description

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>
  );
}
Enter fullscreen mode Exit fullscreen mode

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>
  );
};
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)