Code-splitting with Webpack 2.2.0-rc.4

I’m working with React, and I’m trying to keep performance in my mind while I write it everyday. Today was the day I introduced code-splitting into the mix. I’m working with release candidate Webpack and React Router 3.0.

There are many code-splitting blog entries. I read many of them. They helped, but Webpack 2 and React Router are rapid moving targets. This guide will probably be out of date by next week, but since I’m not going to remember any of this next week I’m going to write it down today.

I have a pretty simple app so I didn’t think it’d be difficult to convert. Here is the code pre-code-split:

import React from 'react';
import ReactDOM from 'react-dom';
import { Route, IndexRoute, Router, browserHistory } from 'react-router';
import Login from 'src/Login';
import Home from 'src/Home';
import Account from 'src/Account';
import Triage from 'src/Triage';
const routes = (
<Route>
<Route path="/login" component={Login} onEnter={requireNoAuth} />
<Route path="/" onEnter={requireAuth}>
<IndexRoute component={Home} />
<Route path="account" component={Account} />
<Route path="triage" component={Triage} />
</Route>
</Route>
);
ReactDOM.render(<Router history={browserHistory}>{Routes}</Router>, document.getElementById('react-container'));

First I read advanced code splitting and the react training dynamic routes. The first thing I noticed was that it looked like using components for the routing doesn’t work. You must turn your routes into javascript objects. That was a little unfortunate, because it makes your code less readable in my opinion. However, it’s pretty straightforward to port it over. Here is the new route object:

//I've removed the imports of the components. Now the components
// are only referenced by their path in the getComponents functions
const loadRoute = (cb) => (module) => cb(
null, module.default,
);
const errorLoading = (err) => console.log('Dynamic page loading failed', err);
const routes = {
childRoutes: [{
path: '/login_success',
onEnter: requireNoAuth,
getComponent(location, cb) {
import('src/loginSuccess').then(loadRoute(cb)).catch(errorLoading);
},
}, {
path: '/login',
onEnter: requireNoAuth,
getComponent(location, cb) {
import('src/login').then(loadRoute(cb)).catch(errorLoading);
},
}, {
onEnter: requireAuth,
path: '/',
indexRoute: {
getComponent(location, cb) {
import('src/home').then(loadRoute(cb)).catch(errorLoading);
},
},
childRoutes: [{
path: 'account',
getComponent(location, cb) {
import('RVCare/src/screens/account')
.then(loadRoute(cb))
.catch(errorLoading);
},
}, {
path: 'triage',
getComponent(location, cb) {
import('src/triage').then(loadRoute(cb)).catch(errorLoading);
},
}],
}],
};

A lot of this makes sense. A route becomes an object. You can put the lifecycle hooks on the object. Most of the children of a Route go in the array for childRoutes, but not all of them. IndexRoute components are put on the route object like I did for '/'. Each route has a couple of properties path and getComponent. The path properties behaves the same as it does on a <Route/> component. The getComponent part is new for this setup though. getComponent is a function that a route expects for asynchronous/dynamic routing. Technically, I could put the login logic inside of the getComponent function. That’s what makes it dynamic.

However, for code-splitting you use the import function that Webpack provides to get the chunk that contains your component. In the beta release this was System.import, but it was deprecated because it didn’t match the import spec. In Webpack 1.x, we had require.ensured . That tidbit might help you make the connection between what I’m writing and any tutorials that you might find for Webpack 1. So, now we have import and some magic. I straight-up stole the logic of getComponent from advanced code splitting, only changing from System.import to import after I found that System.import was deprecated during the docs dive. I think if you read the React Router docs for getComponent it’ll probably make sense, maybe.

In any case, there were a couple of gotchas left for me to waste time on which inspired me to make these notes.

  1. You need to remove any references to the components on the router page else Webpack will not be able to chunk the code effectively. Because you have references to all the components, like: Login, Triage, and Account on the routes page which is imported into app.js and fed to Router you’re whole app is synchronously connected. We must break the connection. Eslint would have eventually told me I had unused variables, but it took me a very long time for me to understand on my own why the code-splitting wasn’t happening.
  2. There one change that I needed to make to webpack.config.js to for the requests to work with my server. I’m using Elixir-Phoenix as my server and if you look at lib/{appname}/endpoint.ex you’ll find some logic which shows that requests to {domain}/js are automatically routed to /priv/static/js. However, after the code-splitting was happening my chunk requests were going to {domain}/5.js but with /js missing from the path Phoenix wasn’t serving the assets correctly. I had to tell Webpack what the public path for the chunks should be. I made the following change to my webpack.config.js:
module.exports = {
//other config
output: {
path: './priv/static/js', //the phoenix static assets path
publicPath: '/js/', //the path put in front requests made by Webpack, the only change I actually made to this part
filename: '[name].js',
};

Once I made that change then the request that Webpack made to get the chunk was routed to the right place on my server and it worked.

So, what the effect of all this? I throttled the network to ‘Good 3G’ on the Network tab and ‘High End Device` on the Timeline tab of Chrome.

Before Code Splitting
After code splitting

With those handicaps, my load times are now only 25% of what they were. I’m not really sure that means in terms of IRL performance, but I feel comfortable that it’s a good move.

As an Elixir side-note, Phoenix’s HTTP module Cowboy v1.3, doesn’t yet support HTTP/2. Cowboy v2 will support HTTP/2, so I think I could get some more improvements from this approach whenever that happens.