Code Splitting for React Router with ES6 Imports


March 10, 2016
Code Splitting: Partial Application Loading With React Router and ES6 imports

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.


Grisogono_Grgur square
Grgur Grisogono is a software architect at Modus Create, specializing in JavaScript performance, React JS, and Sencha frameworks. He helped clients ranging from Fortune 100 to major governments and startups successfully release enterprise and consumer facing web applications. He has also organized three tech conferences and co-authored Ext JS in Action SE. If Grgur's posts were helpful, maybe his 16 years of experience could help your project too.

  • sathish kategaru

    I saw your example and it looks awesome, what it loads is
    about 200 document Other 951 B 651 ms
    favicon.ico 200 text/html Other 951 B 108 ms
    2.bundle.js 200 script vendor.bundle.js:1 1.0 KB 105 ms
    style.css 200 stylesheet about:7 1.3 KB 106 ms
    bundle.js 200 script about:12 18.8 KB 413 ms
    vendor.bundle.js 200 script about:11 38.2 KB 312 ms

    File size of vendor.bundle.js is 38.2 kb..that is amazing..

    i cloned the git hub link and did use node version 5.11.1 and npm 3.8.6. I did npm install and npm start and I opened localhost:3000 ..it doesn’t work..I did npm run build, it built
    Hash: 570a5ac34e9ab750cdeb
    Version: webpack 2.1.0-beta.9
    Time: 4776ms
    Asset Size Chunks Chunk Names
    index.html 456 bytes [emitted]
    vendor.bundle.js 141 kB 0 [emitted] vendor
    1.bundle.js 433 bytes 1 [emitted]
    2.bundle.js 548 bytes 2 [emitted]
    bundle.js 76.6 kB 3 [emitted] js
    style.css 2.26 kB 3 [emitted] js
    style.css.map 86 bytes 3 [emitted] js
    [339] multi js 40 bytes {3} [built]
    [340] multi vendor 40 bytes {0} [built]

    vendor.bundle.js size is 141 kb..

    when i run localhost:3000, files loaded are
    localhost 200 document Other 675 B 2 ms
    style.css 200 stylesheet (index):7 2.4 KB 9 ms
    vendor.bundle.js 200 script (index):11 2.3 MB 25 ms
    bundle.js 200 script (index):12 658 KB 21 ms

    vendor bundle became very big..2.3 mb..can you help me to get that thing right?

    • sathish kategaru

      Hi Grgur,

      Thank you very much for your reply. I understood that about react-router.
      I am able to run it locally now. I did npm run build(produced vendor.bundle.js file of size 141kb) and changed the config to production in webpack.config and loaded that in browser and saw the following result in network

      vendor.bundle.js 200 script about:11 215 KB
      bundle.js 200 script about:12 75.1 KB
      2.bundle.js 200 script vendor.bundle.js:1 780 B

      still the file size(vendor.bundle.js) is 215kb. But your example shows just 38.2kb. How did you do that magic? Am I missing something? is there any further compression that I need to do?
      Thank you in advance for your help.

      • Martin Tsai

        Hey sathish:
        After I tried, I have the same question as you. Do u have any solution until now?

      • Pavel

        I’m shooting that 215 KB is gzipping to 38.2 KB you see the uncompressed file size.

  • sathish kategaru

    In your demo example, I observed that you have not added react-router in vendor array of webpack config. Did you add that in other bundle file? If it is not added in vendor bundle file how can I dynamically add it?

    • Grgur

      Good question. It’s not there because the entire package would get included. If I let webpack figure the dependencies, I can use tree shaking for react router. This is only because react router supports ES6 imports, whereas other libraries do not (yet)

      • Cadell Christo

        if i don’t include react-router in the vendor then each of my bundles includes react router; ~120kb each time. are you sure react router should be left out of the vendor bundle?

  • Dominic Tobias

    Thanks for the info – did you manage to get dynamic bundles working with css? The css is compiled into the bundles but not loaded for me?

  • Alex Mazurov

    After I slightly modified path (in routes.js) for example from path: ‘blog’ –> path: ‘blog/:id’ ore path: ‘blog/test’ routing don’t work any more it’s very strange?!

  • JD

    Thanks for your article!
    Is it possible to dynamically import modules with webpack 2 (ie: ajaxCall(url).then(function(moduleName) { System.import(moduleName)… };)
    I tried to switch to webpack v2 but any dynamic System.import like this one returns an error “Cannot find module” and no 404 at all

  • http://www.benduncan.me/ Ben Duncan

    One thing to note about this is that webpack 2 is not capable of doing tree-shaking with System.imports, so it will load the entire import, regardless of what you’re using in the callback.

  • Jimin Park

    It seems like if I follow your pattern, every single path defined in the router becomes a bundle of its own. How do you aggregate some paths into the same bundle and up with just a several bundles rather than so many?

  • Giacomo Rebonato

    Where can I get a sample using ExpressJS ?

  • Shrey Chaturvedi

    hi , how can we rename the bundles loaded using system.import like webpack gives them dynamic names as static1.bundle.js so on, if i want these files to be named as the names in path provided in system.import , what config should i add?

  • Brian from Indiana

    I installed the latest webpack, react, etc., and got everything set up OK. I followed the tutorial and now have a series of bundle files generated.

    The web app works perfectly on Chrome on my Android phone (several variants of both Chrome and Android) but when I save to desktop it doesn’t run anymore. The web version still runs fine.

    Putting this out there in case anyone else sees this and wonders what’s up. Also FYI the repo doesn’t build for me; it’s passing some options that the latest webpack doesn’t like (–progress) and I also found that react-router now calls its ES6 modules via ‘/es’, not ‘/es6’

    • Müs

      Awesome, thanks for sharing, I was just looking for exactly this; the react-router change log failed to clearly describe their namespace migration from ‘/es6’ to ‘/es’

  • Bharat

    Hey, really nice article. I have a small doubt though, how to handle child routes? As in if you need to load some component when user comes to `/about/company`(and other pages too like `about/terms`) and `/about` should still work as it supposed to, how do I handle that

  • Nathan Maves

    I thought I had everything configured correctly as everything is running but I don’t seem to have any bundles files for each of my routes. Anyone have any ideas where to start?

  • Clemens Paumgarten

    Cool article, thanks!
    However, wouldn’t it be awesome to load the other scripts as soon as the first script is mounted. Might be more “unnecessary” code that will be loaded lazily, but there would be less latency when navigating through the app the first time. Just a thought for big views, might not be important after all.

  • Vasiliy Pereverzev

    thanks, very interesting. How about compatibility with redux?

  • dvfurlong

    Webpack 2 official documentation recommends `import()` over the deprecating `require.ensure`


What We Do

We’ll work closely with your team to instill Lean practices for ideation, strategy, design and delivery — practices that are adaptable to every part of your business.

See what Modus can do for you.

LET'S GET STARTED

We're Hiring!

Join our awesome team of dedicated engineers.

Loading...

APPLY NOW