Code Splitting & Lazy Loading a Server Side Rendered React app

2019-09-29

Reasoning, approach, and goals

Goals

  1. Faster intitial load times. Users only download the code they need for the features they are using. This leads to fast load times and more efficient use of resources.
  2. Faster subsequent loads. Code splitting enables more efficient caching. Developers can now update isolated parts of the app without forcing users to redownload the entire application code.

Approach

My initial approach is going to be to agressively split all of my components and node_modules. This will result in 10s if not 100s of js files, which is okay due to HTTP/2 enabling parallel loading of multiple files over the same connection. Gone are the days of round trips to your server for each file.

This approach is not the “100% most optimal” however, because there are other things to consider. Compression is better on larger files, and there is still some downsides to serving many smaller files. This article is a good summary.

The library

Use @loadable/components. This library is recomended by the React team, is actively maintained, and supports all the features we need (namely SSR).

react-loadable is no longer maintained, despite the myraid of tutorials that exist for it. I started implementing this lib before realizing this.

There are other options and other ways to optimize your app (worth learning about, but outside the scope of this post).

Install & config

We’ll need 4 packages.

npm i -S @loadable/component @loadable/babel-plugin
npm i -D @loadable/component @loadable/webpack-plugin

Configure the plugins.

  1. Add @loadable/babel-plugin to .babelrc.
{
  "plugins": [
    "@loadable/babel-plugin"
  ]
}
  1. Add the Webpack plugin. I customized the loadable json output file name. This file is referenced on the server.
const LoadablePlugin = require('@loadable/webpack-plugin')

new LoadablePlugin({ filename: 'loadable.json', writeToDisk: { filename: `${paths.serverBuild}` } })

Server setup

TODO: Link to full examples

Extract the JavaScript chunks.

// server/render.js
import { ChunkExtractor } from '@loadable/server'

const publicPath = process.env.NODE_ENV === 'production' ? `${paths.cdn}/build/` : paths.publicPath
const statsFile = process.env.NODE_ENV === 'production'
  ? path.resolve('build/server/loadable.json')
  : `${paths.cloudFunctions}/build/server/loadable.json`

const extractor = new ChunkExtractor({
  statsFile,
  entrypoints: ['bundle'],
  outputPath: paths.clientBuild,
  publicPath
})

const content = renderToString(
  extractor.collectChunks(
    sheet.collectStyles(
      <Provider store={req.store}>
        <Router location={req.url} context=>
          { renderRoutes(routes) }
        </Router>
      </Provider>
    )
  )
)

const scriptTags = extractor.getScriptTags()

Add the script tags to your HTML.

// server/components/HTML.js
<div dangerouslySetInnerHTML= />

Client setup

import { loadableReady } from '@loadable/component'

loadableReady(() => {
  hydrate(
    <Provider store={store}>
      <Router history={browserHistory}>
        <ScrollToTop>
          { renderRoutes(routes) }
        </ScrollToTop>
      </Router>
    </Provider>,
    document.getElementById('app')
  )
})

Start code splitting

import loadable from '@loadable/component'

export const MyComponent = loadable(() => import('./MyComponent'))

Sources, further reading

Things I still want to explore, questions I have

  • Group / concatenate similar bundles to reduce overall number of requests.
  • AggressiveSplittingPlugin
  • Is it best to have a single entrypoint file for 3rd party libs (node_modules), so that you only have to implement @loadable in one place?