React Performance

Code Splitting

We already know that it's possible to code split React components using React.lazy and Suspense:

const BigComponent = React.lazy(() => import('../bigComponent'));

const App = () => {
  const [show, setShow] = useState(false);

  return (
    <>
      <button onClick={() => setShow(true)}>Show</button>
      <React.Suspense fallback='Loading...'>
        {show && <BigComponent />}
      </React.Suspense>
    </>
  );
};

The code for BigComponent is now only fetched when the user clicks the "Show" button.

Eager loading

In the above example, what happens if clicking that button causes the fallback to hang for too long? (Maybe the component is really big.)

To solve this, we can perform more eager loading by placing the dynamic import in a function. This gives us the ability to load BigComponent imperatively.

const loadBiggie = () => import('../bigComponent');
const BigComponent = React.lazy(loadBiggie);

const App = () => {
  const [show, setShow] = useState(false);

  return (
    <>
      <button onClick={() => setShow(true)} onMouseEnter={loadBiggie}>
        Show
      </button>
      <React.Suspense fallback='Loading...'>
        {show && <BigComponent />}
      </React.Suspense>
    </>
  );
};

In this refactor, we loadBiggie when the user hovers over the "Show" button, which means the load happens sooner, leading to a smoother user experience.

Pro tip: loadBiggie could get invoked multiple times by the user, but that's okay because bundlers like webpack come with caching of dynamic imports. So, the browser won't repeatedly fetch BigComponent. It will just give you back the results of the initial fetch.

Prefetching

When you prefetch a chunk of JS, that means the browser will automatically load the code for you as soon as it has a spare moment.

This feature is best implemented when you want to lazy load some code that you're very confident the user will use right away. So this code is fetched after all the necessary code but before the user is likely to interact with it.

You can add this functionality through webpack by adding a /* webpackPrefetch: true */ comment in your dynamic import.

import(/* webpackPrefetch: true */ '../bigComponent');

Position of React.Suspense

Every lazily loaded component inside a React.Suspense renders the same fallback. In other words, React.Suspense is shared.

const App = () => (
  <React.Suspense fallback='Loading...'>
    <LazyComponent1 />
    <LazyComponent2 />
  </React.Suspense>
);

When you wrap a bunch of children inside React.Suspense and a component starts lazily loading, everything inside of React.Suspense gets replaced by fallback. This includes any components that aren't lazily loaded as well.

const App = () => (
  <React.Suspense fallback='Loading...'>
    <h1>This heading disappears while LazyComponent loads</h1>
    <LazyComponent />
  </React.Suspense>
);

Best practice: Try to keep React.Suspense wrapped around the components that actually are related to the lazy loading.

const App = () => (
  <>
    <h1>This heading disappears while LazyComponent loads</h1>
    <React.Suspense fallback='Loading...'>
      <LazyComponent />
    </React.Suspense>
  </>
);

Pro tip: You can manually display the fallback for a component by selecting your lazy component and clicking the stopwatch icon in React DevTools.

useMemo for Expensive Calculations

React.memo for Reducing Re-renders

Window Large Lists with react-virtual

Last updated