Code splitting with Suspense in React

·

4 min read

Bundling

Most React apps will have their files/modules bundled using tools like webpack. This bundle can then be included on a webpage to load an entire app at once.

Bundling is the process of following imported files and merging them into a single file: a bundle.

Let's build a small IMDB-like app component to demonstrate the problem.

npx create-react-app my-app
cd my-app
npm start

Synopsis.js

import React from "react";

const Synopsis = () => {
  return (
    <p>
      A thief who steals corporate secrets through the use of dream-sharing
      technology is given the inverse task of planting an idea into the mind of
      a C.E.O.
    </p>
  );
};

export { Synopsis };

Plot.js

import React from "react";

const Plot = () => {
  const styles = {
    container: {
      width: "75%",
      marginTop: "15px",
    },
    moveiGif: {
      float: "left",
      marginRight: "21px",
    },
  };

  return (
    <div style={styles.container}>
      <img
        src="https://i.gifer.com/TZQY.gif"
        alt="inception"
        height="300"
        width="250"
        style={styles.moveiGif}
      />
      Dom Cobb is a skilled thief, the absolute best in the dangerous art of extraction,
      stealing valuable secrets from deep within the subconscious during the dream
      state, when the mind is at its most vulnerable. Cobb's rare ability has made
      him a coveted player in this treacherous new world of corporate espionage,
      but it has also made him an international fugitive and cost him everything
      he has ever loved. Now, Cobb is being offered a chance at redemption. One last
      job could give him his life back but only if he can accomplish the impossible,
      inception. Instead of the perfect heist, Cobb and his team of specialists have
      to pull off the reverse: their task is not to steal an idea but to plant
      one. If they succeed, it could be a perfect crime. But no amount of careful
      planning or expertise can prepare the team for the dangerous enemy that seems
      to predict their every move. An enemy that only Cobb could have seen coming.
      <hr />
      —Warner Bros. Pictures
    </div>
  );
};

export default Plot;

index.js

import React from "react";
import { render } from "react-dom";
import { Synopsis } from "./Synopsis";
import Plot from "./Plot";

const Movie = () => {
  const [showPlot, setShowPlot] = React.useState(false);

  return (
    <>
      <h2>Inception</h2>
      <Synopsis />
      <button onClick={() => setShowPlot((prevState) => !prevState)}>
        {showPlot ? "Hide" : "Show full plot.."}
      </button>
      {showPlot && <Plot />}
    </>
  );
};

const rootElement = document.getElementById("root");
render(<Movie />, rootElement);

code-split-before.png

The app just works fine. But there is room for improvement in terms of performance. What if I have a list of 100 movies and each plot component has ever-growing content and high-pixel assets?

We can load JavaScript (module/code) related to these components lazily or on-demand.

React.lazy

The React.lazy function lets us render a dynamic import as a regular component.

Because the import is inside of a function passed to lazy(), the loading of the component won’t happen until we actually use the component.

Ex,

// SomeComponent must be Default export, like - export default SomeComponent;
// If we use the Named export it fails, like export { SomeComponent }

const LazyComponent = React.lazy(() => import("./SomeComponent"));

const App = () => {
  return (
    <div>
      <LazyComponent />
    </div>
  );
};

React.lazy takes a function that must call a dynamic import().
This must return a Promise which resolves to a module with a default export containing a React component.

Suspense

If the module containing the OtherComponent is not yet loaded by the time the app renders, we must show some fallback content while we’re waiting for it to load, such as a loading indicator. This is done using the Suspense component.

const LazyOtherComponent = React.lazy(() => import("./OtherComponent"));

const App = () => {
  return (
    <div>
      <React.Suspense fallback={<div>Loading...</div>}>
        <LazyOtherComponent />
      </React.Suspense>
    </div>
  );
};

Now, the Movie app can be rewritten like below with lazy loading or code splitting. Synopsis.js and Plot.js do not need to be changed.

index.js; updated,

import React from "react";
import { render } from "react-dom";

import { Synopsis } from "./Synopsis";

const LazyPlot = React.lazy(() => import("./Plot"));

const Movie = () => {
  const [showPlot, setShowPlot] = React.useState(false);

  return (
    <>
      <h2>Inception</h2>
      <Synopsis />
      <button onClick={() => setShowPlot((prevState) => !prevState)}>
        {showPlot ? "Hide" : "Show full plot.."}
      </button>
      {showPlot && (
        <React.Suspense fallback={<div>Loading</div>}>
          <LazyPlot />
        </React.Suspense>
      )}
    </>
  );
};

const rootElement = document.getElementById("root");
render(<Movie />, rootElement);

Output,

  • Observe in the dev tools: click on the "Show plot..." button of the movie app, it downloads the code from the server and caches it for any future call.
  • Again, click as many times as you want on "Show plot...". We will not see a network call and all the assets will be cached at the browser level.

code-split-after.png

Like this, we can lazy load any component that is not needed in initial loading, like the exception handling component, modal dialog components, etc.

React.lazy currently only supports default exports.

The source code available here - react-code-split