DEV Community

Marko Marinovic for Blank

Posted on • Edited on

How to use React Suspense for Code-Splitting?

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.

This post shows an example of how I used suspense on my simple blog site markomarinovic.com .

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

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;

Enter fullscreen mode Exit fullscreen mode

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

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

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

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.

Top comments (5)

Collapse
 
asp2809 profile image
Anshu S Panda

Thanks for the post.
As per what I can see if this is the scenario then I can also lazy load every default export component in my project. So, what would you recommend, lazy loading every component or just the ones which increase my bundle size by a good amount?

Collapse
 
sergiodxa profile image
Sergio Daniel Xalambrí • Edited

Start lazy loading at the router level, then only lazy load big components. Remember every lazy loaded component will need a new request to the server, lazy loading everything could make your app slower, you need to find the right spot for your app.

Collapse
 
marinovicmarko profile image
Marko Marinovic

I agree with Sergio. You should find the best use case for your app. Starting at the router level is perfect. I used this approach for my website because blog post content is in a form of .md files and it would be bad bundle it all together. It kinda mimics the behavior you would get by storing blog content in database and retrieving it on demand.

Collapse
 
asp2809 profile image
Anshu S Panda

Thanks for the reply. Now it's clear to me how and where to do the lazy loading.

Collapse
 
tracker1 profile image
Michael J. Ryan

Here's how I use it...

gist.github.com/tracker1/f0b91b0bc...