Learning Webpack with React and ES6

When I create a React/ES6 app, I reach for my tool of choice – browserify. However, that has certain issues – things that I am certain I can work around if I try, but I’m spending all my time in the tooling. I want to write the application – not the tools. A lot of my articles over the past year have been simply adjustments to my tooling. It’s time for a different approach. Way back in the early parts of last year, I mentioned Webpack in the same breath as Browserify. Webpack is a different style of bundler. It bundles everything together – not just your code. To get to the point of using Webpack, I need some configuration. My hope is that I can build the entire web site with a single build script.

Firstly, let’s get rid of Gulp. That means getting rid of the Gulpfile.js and my gulp directories. It also means I need to handle linting and testing elsewhere. I’ve done this by changing the scripts section of my package.json to the following:

  "scripts": {
    "clean": "rimraf public",
    "pretest": "eslint client/src server/src client/test server/test && sass-lint -v -q -f stylish",
    "test": "mocha --recursive --compilers js:babel-register --reporter dot client/test server/test",
    "start": "node ./bin/www"
  },

Of these, only the start script was there before. I can run npm test to test the package. I can also run npm run clean to clean up the generated files. This handles all the tasks except for the building of the public area. My next step is to integrate the index.html file to the express server. Right now, I serve it up as a static file, and the gulp build system would copy the index.html file from the source area to the destination area. I’ve added the following to my server (the code goes above the staticFiles() middleware initialization):

    // Display the index.html file
    app.get('/', function (request, response) {
        response.status(200).type('text/html').send(index);
    });

The index variable is initialized to the content I want to send elsewhere. The variable is just a copy of my old index.html file.

Webpack Basics

On to the webpack configuration. Webpack, at it’s core, bundles things together and then allows you to load them via loaders. In order to load my JSX files, I need to include the Babel transpiler. The idea is that the files will be compiled from ES6 to normal browser-ready JavaScript. Babel also compiles my JSX for me, so I don’t need an extra step for that. Just like Gulp and Grunt before them, Webpack has a configuration file: webpack.config.js – it’s a Javascript file that exports the configuration object. Here is my simple version:

module.exports = {
    entry: {
        grumpywizards: './client/src/app.jsx'
    },
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                loaders: [ 'babel' ],
                exclude: /node_modules/
            }
        ]
    }
    output: {
        filename: 'public/[name].js'
    }
};

This will walk the entry point, compile all the source files into the right form and then store the file in public/grumpywizards.js. The module.loaders has three elements – firstly, a test – this is a regular expression that is matched against the filename. If the test matches, this loader definition is used. The version here accepts .js and .jsx extensions. The next thing is the exclude – this says don’t include anything in node_modules. Finally, the list of loaders is applied from right to left – I’ve only got one – babel – because Babel compiles JSX as well.

To run this, you need to install webpack and the loader:

npm install --save-dev webpack babel-loader

You can now add the following script definition to the package.json:

  "scripts": {
    "clean": "rimraf public",
    "pretest": "eslint client/src server/src client/test server/test && sass-lint -v -q -f stylish",
    "test": "mocha --recursive --compilers js:babel-register --reporter dot client/test server/test",
    "prestart": "webpack -p",
    "start": "node ./bin/www"
  },

When you run npm start now, the webpack is run prior to starting the server. What you will see is an uglified packed file. Note that it’s on the larger side – about 1MB. That’s because all the React libraries are included (and after I just got rid of them with Browserify!). Also, there are no source maps yet.

Dealing with External Libraries

In my last article, I used an extra module – browserify-shim – to abstract libraries from my code. This functionality is built in to webpack. I just need to add a little bit of configuration to the webpack.config.js:

module.exports = {
    entry: {
        grumpywizards: './client/src/app.jsx'
    },
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                loaders: [ 'babel' ],
                exclude: /node_modules/
            }
        ]
    },
    externals: {
        'react': 'React',
        'react-dom': 'ReactDOM'
    },
    output: {
        filename: 'public/[name].js'
    }
};

On the left hand side is what the module is called. On the right hand side is the global variable that it is exposed as when you load the library from the CDN. Yep – this is super simple. Building this takes my library from 1MB to 2Kb, which is eminently more reasonable. I’ll leave the library serving to the CDN.

Source Maps

Just like the external library configuration, source maps has been thought about as well. Just add an option to generate source maps to your webpack.config.js:

module.exports = {
    entry: {
        grumpywizards: './client/src/app.jsx'
    },
    devtool: 'source-map',
    module: {
        loaders: [
            {
                test: /\.jsx?$/,
                loaders: [ 'babel' ],
                exclude: /node_modules/
            }
        ]
    },
    externals: {
        'react': 'React',
        'react-dom': 'ReactDOM'
    },
    output: {
        filename: 'public/[name].js'
    }
};

The important line here is the devtool configuration – there are various values here but the source-map value should make the webpack emit a .map file.

I had a major amount of pain here though. Most of the online tutorials suggested putting babel-loader and jsx-loader in. This caused the source maps to be the ES5 versions of the files – after transpiling. However, Babel transpiles JSX as well, so there is no need for jsx-loader. Well, it turns out that jsx-loader doesn’t have source map support. Fortunately, we don’t need jsx-loader any more. So just do away with it and be happy.

Wrap Up

So, where do we go from here. I like webpack – much more than the gulp + browserify + babelify + all the rest. There is still some work to do. I need to find a solution to stylesheets for my components and I want to start looking at live reloading as I save files. That, however, is for another day. In the mean time, you can find my continuing code at my GitHub Repository.

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.

Web Dev Tools 101: Modules (Part 2)

Hopefully by now, you’ve settled on a module format. If you haven’t, then please read my prior article for some information on that topic. You cannot select a module loader unless you know what sort of modules you are writing. If you want the short version:

  • ECMAScript 6 – use the babel-core engine to compile to the AMD or CommonJS format
  • AMD – good for browser applications
  • CommonJS – good for server applications

What we will look at today is the various major module loaders and how to use them in the browser. If you are using NodeJS, then the CommonJS loader is built-in, so you don’t need to worry about it.

TL;DR

If you are using ECMAScript 6 then use the babel-core compiler to compile your modules into an equivalent AMD or CommonJS format, depending on requirements.

What to look for

There are only 4 things to look for in a module loader when you are starting out.

  1. Support for Module Format: Your chosen module loader needs to support the modules you are going to write plus the modules in libraries that you need.
  2. Asynchronous or not?: Module loaders are only really used in the browser (node has its own loader support for CommonJS). If you are a multi-page application, then you get major benefits from caching by using asynchronous loading. If you are a single-page application, it’s likely less important but caching is still good. I recommend an Asynchronous loader.
  3. Support for Gulp and Grunt?: If your module loader requires pre-processing of the modules, then it should support grunt or gulp – your chosen task runner.
  4. Support for loading plugins: Sometimes you want to lazy-load your CSS and HTML as well, mostly so you can componentize your CSS and templates. If your module loader doesn’t support it, you can’t have it.

The Candidates

I’ve listed the major candidates I found out on the Internet. Note that ECMAScript 6 support is not in there. Babel-core will compile your ES6 modules to one of the formats (AMD or CommonJS), then you use a module loader as normal. Just look for the requirements.

RequireJS

RequireJS was the original standlone module loader and is still probably the most used module loader out there today. It supports AMD natively. You can wrap your CommonJS modules in a simple wrapper. It’s got lazy (asynchronous) loading and it doesn’t need any pre-processing of the modules (beyond the ES6 to ES5 conversion that babel-core does). It also has a third-party plugin for CSS. This is a really good choice if you are just starting out and don’t know what else you need out of a module loader.

Downsides are few. However, there is a major one. If you are using certain frameworks (Angular is a culprit here) in a single-page application, then using RequireJS is more complex than it needs to be. In this case, you may want to re-visit the module loader topic based on the recommendations of that framework.

Browserify

Browserify is a bundling application, not a module loader. Why then do I include it here? Well, the workflow is “create a whole tree of stuff incorporating all your modules (written in CommonJS or AMD via a plugin) and produces a single Javascript file. You can then include that Javascript file in your HTML page and away you go.

Of course, this gets rid of all the benefits of lazy loading – most of which you don’t want anyway. The one you do want is when you have that large charting library. For that purpose, Browserify has a require functionality that is in CommonJS. The problem will be, of course, that now you have to mix require() (the old syntax) and import (the ES6 syntax) in your code, so you lose the benefit of writing code in ES6.

Browserify does get around the complexity described around AngularJS. If you are or are intending on using a framework like Angular, you may want to check out Browserify in that context.

Webpack

Webpack is another bundling application, much like Browserify. It has native support for AMD formatted modules (which isn’t to say it lazy loads them – just that it supports them). You can use loader plugins to pre-process files, so you can distribute ES6, Coffeescript or the like if you include their preprocessors as well. You can load external libraries, just like Browserify, although implementing lazy-loading is much easier. It has support for grunt and gulp, just like Browserify. Basically, it’s Browserify plus native support for AMD formatted modules and lazy loading.

The others

Yes, there are a whole load of other dynamic or asynchronous module loaders out there. None of them have a following like the ones above. Here is a list for investigation purposes.

Geez – come on guys. We don’t need more module loaders!

The Verdict

I mentioned in a prior article that I like ECMAScript 6, so I’m going to compile my code to another format anyway. That format is AMD as more module loaders support asynchronous loading using AMD than the others. It’s hardly a ringing endorsement, I know, but this area is likely to be in flux while the browsers start supporting ECMAScript 6 anyways.

My AMD loader of choice is RequireJS, but I’ll be spending more time with Webpack over the next few weeks to decide if that is worth the investment.