Black Lives Matter

I stand in solidarity with the Black community against racism and injustice.

Code splitting & lazy loading an SSR React app

Code splitting and lazy loading are two important ways to improve your app's performance. This article serves as a living document describing my process working towards best practices in implementing these features.

Goals

  1. Faster initial load times. Users only download the code they need for the features they are using. This leads to faster 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 re-download the entire application code.

Approach

My initial approach is going to be aggressively splitting all of my components and node_modules. This will result in 10s if not 100s of JavaScript 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 are still some downsides to serving many smaller files. This article is a good summary.

The library

@loadable/components

This library is recommended by the React team, is actively maintained, and supports all the features we need (namely SSR).

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

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

Install & config

We'll need four 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 later 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 isProd = process.env.NODE_ENV === 'production'
const publicPath = isProd ? `${paths.cdn}/build/` : paths.publicPath
const statsFile = isProd
? 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={{ __html: scriptTags }} />

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 the overall number of requests.
  • AggressiveSplittingPlugin
  • Is it best to have a single entry point file for 3rd party libs (node_modules), so that you only have to implement @loadable in one place?