Reduce the size of your Browserified React applications

I’ve been using React for most of my browser-side applications recently. The recommended approach here is to use Browserify to bundle your application. You create ES6 modular components, then bundle them all with React for your application. Let’s take a small application. I’ve got a basic bootstrap component called app.jsx:

import React from 'react';
import ReactDOM from 'react-dom';

import Application from './views/Application.jsx';

ReactDOM.render(
    <Application/>,
    document.getElementById('rootelement')
);

This includes the React libraries (I’m using v0.14.4 here) and my singular component, which looks like this:

import React from 'react';

/**
 * Main Application Router
 * @extends React.Component
 */
export default class Application extends React.Component {
    /**
     * React API - render() method
     * Renders the application view
     * @returns {JSX.Element} a JSX Expression
     */
    render() {
        return <h1>{'Application Booted'}</h1>;
    }
}

I’ve also got a Gulp task to build the app.js that my HTML file loads that looks like this:

gulp.task('client:build:javascript', [ 'client:test' ], function () {
    return browserify({ debug: true })
        .add(config.source.client.entry.javascript, { entry: true })
        .transform(babelify, { presets: [ 'es2015', 'react' ], sourceMaps: true })
        .transform(browserifyshim)
        .bundle()
        .pipe(source('app.js'))
        .pipe(buffer())
        .pipe(sourcemaps.init({ loadMaps: true }))
        .pipe(uglify())
        .pipe(sourcemaps.write('./'))
        .pipe(gulp.dest(config.destination.directory));
});

Finally, I’ve got my HTML file:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Grumpy Wizards</title>
    <link rel="stylesheet" href="app.css">
</head>
<body>
    <div id="rootelement"></div>
    <script src="app.js"></script>
</body>
</html>

This works, but at what cost? The app.js file is 214Kb and the map file is 1.6Mb. That strikes me as a little excessive for 15 lines of code. Of course, the culprit is React and ReactDOM – those libraries occupy the majority of the space. I want to get economies of scale. Lots of sites use jQuery, React, Angular and other major frameworks. Why not let the CDN serve up that content for me? I get a bunch of benefits from this:

  1. My code is much smaller
  2. I don’t pay for bandwidth for serving libraries
  3. The user can take advantage of browser caching
  4. The user experiences shorter load times

Utilizing CDNs is a good idea. Back to the problem at hand – how do I rig Browserify so that it doesn’t bundle libraries? The answer is in a small module called browserify-shim. Here is how you use it.

Step 1: Update your index.html file to bring in the libraries from CDN

React is located at //fb.me/react-0.14.4.min.js and ReactDOM is located at //fb.me/react-dom-0.14.4.min.js – thank Facebook for providing the CDN for this! In fact, since Facebook uses these libraries, they are likely already in your cache. My new index.html file looks like this:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <title>Grumpy Wizards</title>
    <link rel="stylesheet" href="app.css">
</head>
<body>
    <div id="rootelement"></div>
    <script src="//fb.me/react-0.14.4.min.js"></script>
    <script src="//fb.me/react-dom-0.14.4.min.js"></script>
    <script src="app.js"></script>
</body>
</html>

Note that I use the double-slash without the protocol. This means use the same protocol as I am using for the main page. It avoids the “different security policies” dialog boxes.

Re-run your project and check out the window object with your developer tools. React and ReactDOM both add a global variable to the window object. I use Chrome Developer Tools – I can just go over to the Console tab and type window, then expand the returned variable. If you have another big library, then find out its global variable. jQuery uses $, for example, and d3 uses THREE.

Step 2: Update your code to use the actual global variables

My code didn’t need to be updated. However, if you are doing something like this:

import * as jq from 'jquery';
import three from 'three';

If you are doing this, then you need to update your code to use the actual global variable:

import * as $ from 'jquery';
import THREE from 'three';

Step 3: Install browserify-shim

Browserify-shim is available on npmjs.com:

npm install --save-dev browserify-shim

You will also need to update your gulp task:

var browserifyshim = require('browserify-shim');

gulp.task('client:build:javascript', [ 'client:test' ], function () {
    return browserify({ debug: true })
        .add(config.source.client.entry.javascript, { entry: true })
        .transform(babelify, { presets: [ 'es2015', 'react' ], sourceMaps: true })
        .transform(browserifyshim)
        .bundle()
        .pipe(source('app.js'))
        .pipe(buffer())
        .pipe(sourcemaps.init({ loadMaps: true }))
        .pipe(uglify())
        .pipe(sourcemaps.write('./'))
        .pipe(gulp.dest(config.destination.directory));
});

Step 4: Specify the global mapping in package.json

Finally, you need to add a section to your package.json file to tell the browserify-shim what global variables you want to shim in your application:

  "browserify-shim": {
    "react": "global:React",
    "react-dom": "global:ReactDOM"
  }

The left hand side (the key) is the name of your import/require statement. The right side is the global variable that it creates.

Wrap-up

Doing this reduced my file size from 214K to 2.5K and the map file from 1.6M to under 9K. These are massive savings. The user is able to take advantage of caching in their browser which is not just good for browsers but great for mobile browsers where data is at a premium. The smaller sizes also mean the user gets the shortest load time possible and I save on storage and bandwidth.

Want to support the vendors who put out these packages? Most vendors can utilize the data from the CDN to understand apps that are using their libraries and the browsers that are being used with their vendors. This information can inform the development plans and enable them to target high-impact changes. So it’s really a plus for the vendor as well.

I do believe this is a win all around. Don’t serve up common libraries yourself – let the CDN do it.