Imagine using an apartment listings app. If we land on a route listing the properties in our area (route-1) — we don’t need the code for viewing the full details for a property (route-2) or scheduling a tour (route-3), so we can serve users just the JavaScript needed for the listings route and dynamically load the rest.

This idea of code-splitting by route been used by many apps over the years, but is currently referred to as “route-based chunking”. We can enable this setup for React using the Webpack module bundler.

Code-splitting by routes in practice

Webpack supports code-splitting your app into chunks wherever it notices a require.ensure() being used (or in Webpack 2, a System.import). These are called “split-points” and Webpack generates a separate bundle for each of them, resolving dependencies as needed.

// Defines a "split-point"

require.ensure([], function () {

const details = require('./Details');

// Everything needed by require() goes into a separate bundle

// require(deps, cb) is asynchronous. It will async load and evaluate

// modules, calling cb with the exports of your deps.

});

When your code needs something, Webpack makes a JSONP call to fetch it from the server. This works well with React Router and we can lazy-load in the dependencies (chunks) a new route needs before rendering the view to a user.

Webpack 2 supports automatic code-splitting with React Router as it can treat System.import calls for modules as import statements, bundling imported filed and their dependencies together. Dependencies won’t collide with the initial entry in your Webpack configuration.

import App from '../containers/App'; function errorLoading(err) {

console.error('Lazy-loading failed', err);

} function loadRoute(cb) {

return (module) => cb(null, module.default);

}

export default {

component: App,

childRoutes: [

// ...

{

path: 'booktour',

getComponent(location, cb) {

System.import('../pages/BookTour')

.then(loadRoute(cb))

.catch(errorLoading);

}

}

]

};

Bonus: Preload those routes!

Before we continue, one optional addition to your setup is <link rel=”preload”> from Resource Hints. This gives us a way to declaratively fetch resources without executing them. Preload can be leveraged for preloading Webpack chunks for routes users are likely to navigate to so the cache is already primed with them and they’re instantly available for instantiation.

At the time of writing, preload is only implemented in Chrome, but can be treated as a progressive enhancement for browsers that do support it.

Note: html-webpack-plugin’s templating and custom-events can make setting this up a trivial process with minimal changes. You should however ensure that resources being preloaded are genuinely going to be useful for your averages users journey.

Asynchronously loading routes

Back to code-splitting — in an app using React and React Router, we can use require.ensure() to asynchronously load a component as soon as ensure gets called. Btw, this needs to be shimmed in Node using the node-ensure package for anyone exploring server-rendering. Pete Hunt covers async loading in Webpack How-to.

In the below example, require.ensure() enables us to lazy load routes as needed, waiting on a component to be fetched before it is used:

const rootRoute = {

component: Layout,

path: '/',

indexRoute: {

getComponent (location, cb) {

require.ensure([], () => {

cb(null, require('./Landing'))

})

}

},

childRoutes: [

{

path: 'book',

getComponent (location, cb) {

require.ensure([], () => {

cb(null, require('./BookTour'))

})

}

},

{

path: 'details/:id',

getComponent (location, cb) {

require.ensure([], () => {

cb(null, require('./Details'))

})

}

}

]

}

Note: I often use the above setup with the CommonChunksPlugin (with minChunks: Infinity) so I have one chunk with common modules between my different entry points. This also minimized running into missing Webpack runtime.

Brian Holt covers async route loading well in a Complete Intro to React. Code-splitting with async routing is possible with both the current version of React Router and the new React Router V4.

Easy declarative route chunking with async getComponent + require.ensure()

Here’s a tip for getting code-splitting setup even faster. In React Router, a declarative route for mapping a route “/” to a component `App` looks like <Route path=”/” component={App}>.

React Router also supports a handy `getComponent` attribute, which is similar to `component` but is asynchronous and is super nice for getting code-splitting setup quickly:

<Route

path="stories/:storyId"

getComponent={(nextState, cb) => {

// async work to find components

cb(null, Stories)

}} />

`getComponent` takes a function defining the next state (which I set to null) and a callback.

Let’s add some route-based code-splitting to ReactHN. We’ll start with a snippet from our routes file — this defines require calls for components and React Router routes for each route (e.g news, item, poll, job, comment permalinks etc):

var IndexRoute = require('react-router/lib/IndexRoute')

var App = require('./App')

var Item = require('./Item')

var PermalinkedComment = require('./PermalinkedComment') <--

var UserProfile = require('./UserProfile')

var NotFound = require('./NotFound')

var Top = stories('news', 'topstories', 500)

// .... module.exports = <Route path="/" component={App}>

<IndexRoute component={Top}/>

<Route path="news" component={Top}/>

<Route path="item/:id" component={Item}/>

<Route path="job/:id" component={Item}/>

<Route path="poll/:id" component={Item}/>

<Route path="comment/:id" component={PermalinkedComment}/> <---

<Route path="newcomments" component={Comments}/>

<Route path="user/:id" component={UserProfile}/>

<Route path="*" component={NotFound}/>

</Route>

ReactHN currently serve users a monolithic bundle of JS with code for all routes. Let’s switch it up to route-chunking and only serve exactly the code needed for a route, starting with comment permalinks (comment/:id):

So we first delete the implicit require for the permalink component:

var PermalinkedComment = require(‘./PermalinkedComment’)

Then we take our route..

<Route path=”comment/:id” component={PermalinkedComment}/>

And update it with some declarative getComponent goodness. We’ve got our require.ensure() call to lazy-load in our route and this is all we need to do for code-splitting:

<Route

path="comment/:id"

getComponent={(location, callback) => {

require.ensure([], require => {

callback(null, require('./PermalinkedComment'))

}, 'PermalinkedComment')

}}

/>

OMG beautiful. And..that’s it. Seriously. We can apply this to the rest of our routes and run webpack. It will correctly find the require.ensure() calls and split our code as we intended.

After applying declarative code-splitting to many more of our routes we can see our route-chunking in action, only loading up the code needed for a route (which we can precache in Service Worker) as needed:

Reminder: A number of drop-in Webpack plugins for Service Worker caching are available:

sw-precache-webpack-plugin which uses sw-precache under the hood

offline-plugin which is used by react-boilerplate

CommonsChunkPlugin

To identify common modules used across different routes and put them in a commons chunk, use the CommonsChunkPlugin. It requires two script tags to be used per page, one for the commons chunk and one for the entry chunk for a route.

const CommonsChunkPlugin = require("webpack/lib/optimize/CommonsChunkPlugin");

module.exports = {

entry: {

p1: "./route-1",

p2: "./route-2",

p3: "./route-3"

},

output: {

filename: "[name].entry.chunk.js"

},

plugins: [

new CommonsChunkPlugin("commons.chunk.js")

]

}

The Webpack — display-chunks flag is useful for seeing what modules occur in which chunks. This helps narrow down what dependencies are being duplicated in chunks and can hint at whether or not it’s worth enabling the CommonChunksPlugin in your project. Here’s a project with multiple components that detected a duplicate Mustache.js dependency between different chunks:

Webpack 1 also supports deduplication of libraries in your dependency trees using the DedupePlugin. In Webpack 2, tree-shaking should mostly eliminate the need for this.

More Webpack tips

The number of require.ensure() calls in your codebase generally correlates to the number of bundles that will be generated. It’s useful to be aware of this when heavily using ensure across your codebase.

Tree-shaking in Webpack2 will help remove unused exports. This can help keep your bundle sizes smaller.

Also, be careful to avoid require.ensure() calls in common/shared bundles. You might find this creates entry point references which have assumptions about the dependencies that have already been loaded.

In Webpack 2, System.import does not currently work with server-rendering but I’ve shared some notes about how to work around this on StackOverflow.

If optimising for build speed, look at the Dll plugin, parallel-webpack and targeted builds

If you need to async or defer scripts with Webpack, see script-ext-html-webpack-plugin

Detecting bloat in Webpack builds

The Webpack community have many web-established analysers for builds including http://webpack.github.io/analyse/, https://chrisbateman.github.io/webpack-visualizer/, and https://alexkuz.github.io/stellar-webpack/. These are handy for understanding what your largest modules are.

source-map-explorer (via Paul Irish) is also fantastic for understanding code bloat through source maps. Look at this tree-map visualisation with per-file LOC and % breakdowns for the ReactHN Webpack bundle:

You might also be interested in coverage-ext by Sam Saccone for generating code coverage for any webapp. This is useful for understanding how much code of the code you’re shipping down is actually being executed.

Beyond code-splitting: PRPL Pattern

Polymer discovered an interesting web performance pattern for granularly serving apps called PRPL (see Kevin’s I/O talk). This pattern tries to optimise for interactivity and stands for: