Code Splitting for React Router with ES6 Imports

   DevOps and Tools

Partial application loading is an essential technique for improving the time-to-first-impression for single page applications. The goal is to prioritize loading of the code needed to render the view whilst deferring other assets. We are about to see how we can achieve that with modern web development’s latest and greatest tools:
– React 15.0
– React Router 2.0
– Webpack 2.1

In addition to using the latest versions of the above stack, we’ll also do it in style:
– Use native ES6 imports (without requires)
– Implement code splitting
– Harness tree shaking

I showcased the process in a simple React app that uses React Router and loads route chunks dynamically. The entire project’s source code is available on Github and you can also see the final demo live.

Basic Setup

Our basic npm setup will include React, React Router and Webpack 2. You can set it up manually following my example or just clone this git repo.

Code Splitting (Webpack Chunks)

Webpack is smart enough to figure out the chunks automatically when it scans your application code. All you need to provide it with is an entry point.

Our sample Webpack config file lists two entry points, application logic (js) and libraries (vendor):

 entry: {
   js: [
     'index', 'pages/Home'
   ],
   vendor: [
     'react', 'react-dom'
   ]
 },

While this step isn’t required, it makes sense to separate libraries from app logic so that we can leverage browser caching to a greater extent. Vendors will become a separate javascript bundle that we can reuse for any initial route the visitors use to access the app.

The js entry point should load most of the shared application code. This is how we expect Webpack to learn about the code that needs to be split into separate chunks. All direct imports that belong to the js entry point will be bundled into a single file. However, Webpack will read through our source code and understand which bits we might need to load asynchronously, then create meaningful chunks (separate javascript files) for future usage.

How does it do that, you might ask? Let’s take a look at that now.

Dynamic Routes

Ryan Florence authored a great article on the future of web application delivery that outlines a very similar process using require.ensure. My recent post on Tree Shaking with Webpack 2 explains how Webpack 2 is capable of reading native import statements. This example, in contrast to Ryan Florence’s approach, makes use of the newly introduced Webpack 2 features.

Here’s the key concept: React Router configuration will use getter functions for the routes that we want bundled separately. Check it out:

import App from 'containers/App';
function errorLoading(err) {
 console.error('Dynamic page loading failed', err);
}


function loadRoute(cb) {
 return (module) => cb(null, module.default);
}


export default {
 component: App,
 childRoutes: [
   {
     path: '/',
     getComponent(location, cb) {
       System.import('pages/Home').then(loadRoute(cb)).catch(errorLoading);
     }
   },
   {
     path: 'blog',
     getComponent(location, cb) {
       System.import('pages/Blog').then(loadRoute(cb)).catch(errorLoading);
     }
   },
   {
     path: 'about',
     getComponent(location, cb) {
       System.import('pages/About').then(loadRoute(cb)).catch(errorLoading);
     }
   },
 ]
};

In our example, we used the getComponent function that Router automatically calls when a route is requested. System.import will know which file to load because Webpack created all the instructions needed. Webpack will look for System.import statements to know which chunks of your code should be bundled. Magic!

Let’s review the process:

  1. Webpack scans your code during the build process. It treats System.import calls as import statements and bundles the imported file along with its dependencies. It’s important to know that dependencies will not collide with those in the main application bundle (aka the initial entry point of the Webpack configuration file).
  2. A visitor accesses the app, starting from any random route. Anything from index.html is loaded, including the vendor files and the initial entry point (your main app logic). React Router executes getComponent based on the requested route path.
  3. System.import is now a polyfill that executes a JSONP request in order to fetch the bundle required for the route to execute. This is an asynchronous process (like any XHR request).
  4. System.import is also a Promise. When the response is delivered, we loadRoute. In other words, we execute the chunk that was loaded. Voila, the view is displayed to our user.
  5. If the route is accessed again, the router (actually the System.import polyfill) will already know about it and no additional code downloading will occur.

Go ahead and try it on the demo site. Open your network inspector tab and monitor the traffic. You will see how chunks are loaded (or not) automatically. You can also go to a route (e.g. /blog) and reload from there. Pure awesomeness!

Tree Shaking for React Router

Tree shaking works with native ES6 modules, but not the CommonJS counterpart. Luckily, React Router ships with es6 module support, but you need to access them explicitly.

Our index.js shows exactly how to do it:

import { Router, browserHistory } from 'react-router/es6';

Notice the trailing /es6. Now our Webpack 2 build process will only load the components it needs from the React Router package.

Caveats

System.import should be straightforward for Webpack to read. Routes built from variables will not load properly, at least in Webpack 2.1 beta 4. This makes sense as it would take complex parsing to figure out all the possible combinations.

There is a good reason why we didn’t include React Router and the History package in our vendors chunk. Remember, we only used React in it. That’s because we want to leverage the tree shaking technology and bundle only the code our app needs. React doesn’t ship with ES6 imports quite yet, so we can move the entire thing into vendors.js. To recap, we put infrequently changed libraries into the vendors bundle, which can be more aggressively cached on the browser side. Having a separate file also helps with download concurrency.

Conclusion

41% of modern React apps use React Router, according to npm package download stats for February 2016. This makes React Router an essential part of the modern React stack. Support for partial application loading is extremely important from the performance perspective. As we can see, it’s also a lot of fun to implement.


Like What You See?

Got any questions?