Optimization

JavaScript Splitting

We already saw how Webpack handles bundling a single file. But often in a big application the JavaScript can become so large that it needs to be broken up. Otherwise, you force users to download a heavy payload up front caused by JavaScript they may never use. Webpack can split your JavaScript into chunks, allowing users to only download the JavaScript they need, when they need it.

Webpack does require you to specify your splits and won’t figure it out for you. However, it’s easily changeable so you can figure out the splits that work best for your application.

Let’s pretend we have a 3-page application, made up by index.html, events.html, and video.html. Our JavaScript files look like this:

/**
 * @file index.js
 */
'use strict';

import $ from 'jQuery';        // also used by events.js
/**
 * @file events.js
 */
'use strict';

import $ from 'jQuery';        // also used by index.js
import moment from 'moment';
import React from 'react';     // also used by video.js
/**
 * @file video.js
 */
'use strict';

import React from 'react';     // also used by video.js
import ReactPlayer from 'react-player';

We started by bundling everything together via webpack.config.js:

module.exports = {
  context: __dirname + '/src',
  entry: {
    app: ['./js/events.js', './js/index.js', './js/video.js']
  },
  output: {
    filename: '[name].bundle.js',
    path: __dirname + '/dist/js',
  },
};

However, when we run webpack -p, the following 363 KB bundle is created:

┬—— dist/
|   └—— js/
|       └–– app.bundle.js  // 363 KB
├—— src/
|   ├—— js/
|   |   ├—— events.js
|   |   ├—— index.js
|   |   └—— video.js
|   ├—— events.html
|   ├—— index.html
|   └—— video.html
├—— package.json
└—— webpack.config.js

That requires users to download that 363 KB bundle up front to view any single page. We can do better than that:

module.exports = {
  context: __dirname + '/src',
  entry: {
    events: './js/events.js',  // Entry point 1
    index: './js/index.js',    // Entry point 2
    video: './js/video.js',    // Entry point 3
  },
  output: {
    filename: '[name].bundle.js',
    path: __dirname + '/dist/js',
  },
};

We now have 3 entry points—also called chunks—instead of 1. Notice the object keys events, index, and video: Webpack will replace [name] below with each of these values when building. When we run webpack -p to bundle our code for production, we’ll get 3 files in dist:

┬—— dist/
|   └—— js/
|       ├—— events.bundle.js // 333 KB
|       ├—— index.bundle.js  //  88 KB
|       └—— video.bundle.js  //  50 KB

We’ve reduced the inital load of index.html from 363 KB down to 88 KB—a 75% reduction! Each of those 3 JavaScript bundles are self-contained and have all the dependencies they need.

However, we’ve introduced a new problem. Although we’ve reduced the load of each page, we’ve generated a total of 471 KB of JavaScript in the process—a 30% total increase from where we started—which users will be subject to if they hit every page. We’re not done optimizing yet. We can leverage caching more efficiently in the next step.

Commons Chunk

It’s preferable for users to download a little bit of JavaScript on every page load, instead of all the JavaScript up front. In doing so, we achieve load times that are roughly distributed across the application. Chunk splitting is how we accomplish this. But we must take care users aren’t re-downloading the same libraries they’ve already downloaded in a previous bundle. That’s a waste of bandwidth and adds unnecessary slowness to the application.

Fortunately Webpack has thought of this. We can use the Commons Chunk Plugin to automatically place the most central parts of our code into a commons.js file that gets immediately served and cached on the user end. Let’s add the following to webpack.config.js:

module.export = {

  // …

  plugins:
  [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'commons',
      filename: 'commons.js',
      minChunks: 2,
    }),
  ],
};

Compiling again, we’ll get:

┬—— dist/
|   ├—— commons.js       // 109 KB
|   ├—— events.bundle.js // 225 KB
|   ├—— index.bundle.js  // < 1 KB
|   └—— video.bundle.js  //  29 KB

There’s a new commons.js file in dist/. In our particular example, this file contains jQuery and React since they were required by 2 modules (minChunks: 2). If we declare minChunks: 3, our commons.js file would be empty since we don’t have 3 or more modules that require the same libraries. Conversely, setting it to 1 is pointless—worse than just bundling everything into one file (because we’d be using an additional request). For our example, minChunks: 2 is ideal; for your unique application, this is one of the main settings you’ll have to experiment with for the best optimization.

To include it in your application, simply include an additional <script> tag:

<script src="js/commons.js"></script>
<script src="js/index.bundle.js"></script>

Our index.html payload is back up by a hair to 109 KB—70% of the original—but we were able to reduce both our events and video scripts, and prevent any bundles from having duplicate JavaScript. We’ve now struck a good balance in our project by distributing load times more evenly across pages. We were able to lessen the initial payload and spread it out across the application, reducing the average load time of each user in the process.

Uglifying

For performance, Webpack doesn’t uglify when running webpack. You can uglify and minify by invoking webpack -p, but for more granular control it’s advisable to modify webpack.config.js:

module.exports = {
  plugins: [
    new webpack.optimize.UglifyJsPlugin({
      comments: false,
    }),
  ],
};

You can configure the following options for UglifyJsPlugin:

Option Accepts Default Description
beautify boolean false Enable output beautification (assuming it’s not compressed)
comments boolean|RegExp (keeps all @preserve, @license, and @cc_on comments) Configure whether to keep comments, discard comments, or keep RegEx-matched comments based on their content.
compress boolean|object true Set options for UglifyJS compression
debug boolean false Keep console.log() and breakpoints in your output
sourceMap boolean true Map error messages to modules (set to false to speed up compilation)

Deduping

Webpack’s DedupePlugin will search for equal or similar files and remove duplication on output. Enable it with:

module.exports = {
  plugins: [
    new webpack.optimize.DedupePlugin(),
  ],
};

There are no options to configure. Due to performance this should only be used for production, and not in watched builds. It’s worth noting that while this may reduce the overall output size, it won’t solve multiple module instance, and may not significantly reduce gzipped size (since gzip is very efficient at compressing duplicated parts of files).

results matching ""

    No results matching ""