Skip to content

Illustration by unDraw.

Lazy loading React components

Lazy loading React components in both server and browser environments can prove challenging depending on the framework you are using.

React apps, like any other type of web app (Angular, Vue, etc) can become very big and bloated with unused code at any given time while it is being downloaded and executed in a browser.

Example bundle analysis

Common UX patterns have many components like dialogs, alerts and messages, appearing only after a user interaction or a specific event. Take the example of clicking "Add to cart" in a website and then a popup message appears saying "Added to cart successfully!". That popup component is not needed when the web app is initially downloaded into the browser so it can be code-split (e.g. removed from the main JS bundle) and lazy-loaded the moment the user adds something to the cart.

Bundlers have become smarter and can tree-shake some of that unused code for you, but they can't decide whether each file/component should be lazy-loaded and removed completely from the main JS bundle. This code-splitting and lazy-loading functionality is usually accomplished with the dynamic import() method that is part of the bundler compilation runtime, that is different from the browser one.

However, due to React's JSX we need to do more than just importing and rendering the component with the dynamic import(). In this article we will see how to code-split and lazy-load React components in different runtime contexts (server vs browser) and for different web frameworks (Next.js and Gatsby.js).

React.js#

Note that the following approach is for Client-Side-Rendered (CSR) web apps that are executing all their code inside a browser.

In the example that follows, we are using React's own lazy loading components lazy and Suspense.

header.jsx
import React, { useState } from 'react'
const NavMenu = React.lazy(() => import('./NavMenu'))

function Header() {
  const [showMenu, setShowMenu] = useState(false)
  
  return (
  <div>
  	<React.Suspense fallback={<p>Loading...</p>}>
      {showMenu && <NavMenu />}
  	</React.Suspense>
  </div>
)}

React takes care of translating the imported JS file into a React component so that it can be rendered using JSX. The code-splitting and lazy-loading part is taken care of by Webpack during compilation when it sees the dynamic import() function being called.

Note that if the import() function is not being recognised by your bundler, you may have to install this Babel plugin.

The component to be lazy-loaded needs a placeholder while it is being downloaded parsed and rendered, so React requires <Suspense> to be used to provide that fallback component. This could be anything really, a spinner, animated SVG or just a plain text to signify that something will load in its place.

For a live example please have a look at this Glitch.

Naming output chunks in Webpack#

Sometimes you might wanna give names to your code-split chunks, as bundlers will not give them one by default. It certainly helps me debug when I have a number of lazy-loaded component in a page.

For Webpack, you only have to use the webpackChunkName magic comment like below:

import(/* webpackChunkName: "<Component Name>" */ '<path>/<to>/<component>')

So taking the previous example, where we lazy-loaded the <NavMenu> component, we would do something like this:

import(/* webpackChunkName: "NavMenu" */ './NavMenu')

Using react-router#

Code bloat is a common issue for Single Page Applications. If not done correctly, you end up with a main JS bundle that consists of all possible routes in your web app, whereas you only need one at a time.

Luckily this is an easy fix. Simply use the above approach to lazy load components when you are specifying your routes:

router.js
import React from 'react'
import { 
  BrowserRouter as Router, 
  Switch 
} from 'react-router-dom'

const HomePage = React.lazy(() => import('./Pages/Home'))
const LoginPage = React.lazy(() => import('./Pages/Login'))

export default function () {
  return (
    <Router>
      <Switch>
        <Route exact path='/'>
          <React.Suspense fallback={<p>Loading...</p>}>
            <HomePage />
          </React.Suspense>
        </Route>
        <Route exact path='/login'>
          <React.Suspense fallback={<p>Loading...</p>}>
            <LoginPage />
          </React.Suspense>
        </Route>
      </Switch>
    </Router>
  )
}

Next.js#

Note that the approach for Next.js is different as it is handling Server-Side-Rendering (SSR) where code is not always executed in the browser - it is also executed in the server.

Next.js offers dynamic imports out of the box, with SSR included!

It is fairly straight forward to code-split and convert any of your components. If we take the previous example of the <Header /> component, it would look something like this:

header.jsx
import dynamic from 'next/dynamic'
const NavMenu = dynamic(
  () => import('./NavMenu'),
  { loading: () => <p>Loading...</p> }
)

function Header() {
  const [showMenu, setShowMenu] = useState(false)

  return (
    <div>
      {showMenu && <NavMenu />}
    </div>
  )
}

export default Header

Gatsby.js#

Note that the approach for Gatsby.js is different as it is handling Server-Side-Rendering (SSR) where code is not always executed in the browser - it is also executed in the server.

Gatsby does not currently offer out of the box lazy loading with SSR as Next.js does. To add lazy-loading we will have to look at one of the following three options. Though the list is not exhaustive - it's what I found to be more convenient 😊.

Note that Gatsby.js uses Webpack by default to build and bundle up the code.

Gatsby #1: React lazy and Suspense#

Just like in plain React.js in the browser, we can use React lazy and Suspense but first we need to check if the window object is not undefined:

{typeof window !== 'undefined' && (
  <React.Suspense fallback={<Loading />}>
    <LazyThing />
  </React.Suspense>
)}

More on this workaround can be found in the Gatsby.js Docs.

Note that the lazy-loaded component above will not be server-side rendered!

Gatsby #2: Loadable components#

Loadable components is a library that offers lazy-loading of React components with SSR baked in. The react-universal-component library also offers similar functionality.

gatsby-node.js
const LoadablePlugin = require('@loadable/webpack-plugin')
exports.onCreateWebpackConfig = ({ actions, plugins }) => {
  actions.setWebpackConfig({
    plugins: [new LoadablePlugin()]
  })
}
src/templates/page.js
import loadable from '@loadable/component'

export default function Page(props) {
  const LazyComponent = loadable(() => import(`../components/lazyComponent.js`))
  return (
    <Layout>
      // ...
      <LazyComponent whatAmI={"Lazy!"} />
    </Layout>
  )
}

More on this workaround can be found in the Gatsby.js Docs.

Tip: In-depth article explains how to setup loadable components with Gatsby.

Gatsby option #3: Dynamic import() + React state#

The final workaround that we will look at is taking advantage of the dynamic import() function we saw earlier used inside the React lazy function along with the React state.

We essentially import the component file (line 12) after a user interaction and create a new React component at runtime (line 14) by calling the React.createElement method:

lazyButton.jsx
import React, { useState } from 'react'

const Button = () => {
  const [ LazyComponent, setLazyComponent ] = useState(null)

  const loadLazyComponent = async () => {
    if (LazyComponent !== null) {
      return
    }

    // import default export of `lazy.js` module
    const { default: lazyModule} = await import('./lazy')
    // create React component so it can be rendered
    const LazyElement = React.createElement(lazyModule, {
      // pass props to component here
      whatAmI: "lazy! 🤤"
    })
    setLazyComponent(LazyElement)
  }

  return (
    <>
      <button onClick={loadLazyComponent}>Lazy load component</button>
      {LazyComponent}
    </>
  )
}

For an example project please look at this demo Gatsby Github repository.

Further reading#