Technology

How to use React Suspense for Code-Splitting?

React 16.6 shipped an interesting feature called Suspense that allows rendering while waiting for something. The loading indicator is displayed during the wait time. Learn more about that powerful tool for improving application performance!

React 16.6 shipped an interesting feature called Suspense. Suspense allows React to suspend rendering while waiting for something. Loading indicator is displayed during wait time.

Why is this important to us?

We bundle our React apps using tools like webpack and rollup. Our bundle grows as our application grows, especially when we include different third party libraries. Over time, our bundle will be huge and impact loading time of our application. To prevent this from happening, we can start splitting our big bundle into multiple smaller bundles with a process called code-splitting. Code-Splitting is supported by tools such as webpack and rollup. These tools can generate multiple bundles that we can load on demand in runtime. I'll focus on webpack since it's used in this project.

With code-splitting we can give to the user only the code he currently needs, loading additional code as user navigates though the app.

How can Suspense and lazy help us?

Suspense will display the fallback component while bundle is dynamically loaded. It works together with lazy function.

lazy takes a single argument called factory (function returning a promise) and creates a new component of type LazyComponent. LazyComponent will call a factory function on the first render and resulting promise will be used by Suspense to show/hide the fallback component. In the case of code-splitting, we will pass a dynamic import of our component module.

import { lazy } from 'React';
const Home = lazy(() => import('./pages/Home'));
const BlogDetails = lazy(() => import('./pages/BlogDetails'));

webpack will see a dynamic import and create a promise that will resolve once the bundle is loaded. You can read more about webpack dynamic imports here.

Keep in mind that lazy only supports default exports, so make sure your component module has a default export. If you have a named export, you can re-export it as default to work around this.

Lazy loading pages and blog content

This is the App.ts for this blog project. Each page is dynamically loaded the first time we navigate to it.

import React, { Suspense, lazy } from 'react';
import { Router } from '@reach/router';
import { ErrorBoundary } from 'shared/components/ErrorBoundary';
import SuspenseFallback from 'shared/components/SuspenseFallback';
import { posts } from 'data/blog';
const Home = lazy(() => import('./pages/Home'));
const BlogDetails = lazy(() => import('./pages/BlogDetails'));
const App = () => (
  <React.StrictMode>
    <ErrorBoundary>
      <Suspense fallback={<SuspenseFallback />}>
        <Router>
          <Home path="/" />
          {posts.map((post) => {
            const { id, slug } = post;
            return <BlogDetails post={post} key={id} path={`/${slug}`} />;
          })}
        </Router>
      </Suspense>
    </ErrorBoundary>
  </React.StrictMode>
);
export default App;

<SuspenseFallback /> will be displayed while we wait for the bundle to load. You can test this by throttling your internet connection and refreshing the page.

import React from 'react';
const SuspenseFallback = () => <span>Suspended. Loading data...</span>;
export default SuspenseFallback;

Lazy loading blog post content

The interesting thing is that we don't have to use this concept just for routing. Lazy loading is used in this blog project for content fetching as well. Content for each blog post is in the form of .mdx file.

BlogDetails component will be loaded the first time we click on the blog post. Each blog post has a separate content which will be loaded separately. This allows us to load BlogDetails component once and separately load content depending on the post. Without lazy loading the content, we would have to bundle all .mdx files in the main bundle, drastically increasing our bundle size. mdx files replace database calls in this case.

Blog post data looks like this:

import { lazy } from 'react';
interface IBlogPost {
  id: number;
  title: string;
  description: string;
  date: string;
  slug: string;
  Content: React.LazyExoticComponent<any>;
}
const post: IBlogPost = {
  id: 4,
  title: 'How to use React Suspense for Code-Splitting?',
  description:
    'Suspense allows React to suspend rendering while waiting for something.',
  date: '10.02.2020 @ 21:30',
  slug: 'how-to-use-react-suspense-for-code-splitting',
  Content: lazy(() => import('./content.mdx')),
};
export default post;

Content is our lazy component which dynamically imports content.mdx file.

BlogDetails component renders lazy Content component, initiating .mdx file load.

import React from 'react';
import { RouteComponentProps } from '@reach/router';
import Layout from 'shared/components/Layout';
import { StyledHeader } from 'shared/styles/components';
import { IBlogPost } from 'shared/types/models/blog';
import MDXWrapper from 'shared/wrappers/MDXProvider';
import { StyledContent, StyledPublishedOn } from './BlogDetailsStyles';
interface IOwnProps {
  post: IBlogPost;
}
type IProps = IOwnProps & RouteComponentProps;
const BlogDetails = (props: IProps) => {
  const { post } = props;
  const { title, date, description, slug, Content } = post;
  const pageMeta = {
    title,
    description,
    slug,
    date,
  };
  return (
    <Layout meta={pageMeta}>
      <StyledHeader>
        <h1>{title}</h1>
        <StyledPublishedOn>Published on {date}</StyledPublishedOn>
      </StyledHeader>
      <StyledContent>
        <MDXWrapper>
          <Content />
        </MDXWrapper>
      </StyledContent>
    </Layout>
  );
};
export default BlogDetails;

If you open the network tab in dev tools you will see that the first time you visit a blog post it loads multiple bundles. Visiting other blog posts will initiate a load of additional content.mdx bundles.

Conclusion

Suspense for code-splitting is a very powerful tool for improving application performance. This is something you can start implementing right now to greately improve performance of your web app. There are more Suspense related things comming in the new concurrent mode for React which is currently in experimental phase.

Share this article on

Wanna see our work?

Check out our rich portfolio and all the projects we are proud of.