Put Your Webpack Bundle On A Diet - Part 2

October 19, 2017 0 Comments

Put Your Webpack Bundle On A Diet - Part 2

 

 

We’re covering how you can make sure that your users get the best experience possible by giving them a lightning fast site. In the first part of this series, we decreased the bundle from 1.7MB to 640KB via webpack -p. Part two will dig a bit deeper into the soil of webpack optimization by showing you how to use the NODEENV=production webpack environment variable to get a better bundle size reduction and much more.

In this article, you’ll learn how to:

  • Minify the code
  • Add the loaderOptionsPlugin to enable minify
  • Make sure everything is set to production mode
  • Enable module concatenation
  • Extract the css to enable better cacheability, parallel downloading and parsing
  • Enable tree-shaking by using the new es modules import syntax

What we’ll be using

Throughout this article, we’ll use our file-upload-example app. It’s a progressive web app that we will optimize in terms of loading speed and payload size. A key aspect of this app is that it demonstrates our upload feature.

If you're on Windows you need to set up cross-env, and then use cross-env NODEENV=production webpack to prevent your Windows command prompt from choking.

Webpack on a dies part II - image1

The optimized webpack—where we are and where we want to go

Last week, we described how running webpack with the -p switch is one of the fastest ways to reduce the payload. But saving bytes on the wire is always important, and that’s why we’re offering you an alternative solution.

Fortunately, decreasing the bundle size and increasing load speed on your own is not that hard. We can supply our own config to UglifyJS, or use another minification plugin. All of this will take place in your webpack.config.js file.

To get started, I’d recommend you do the following:

1. Minify the code

Add webpack.optimize.UglifyJsPlugin. For reference, you can also check out the plugin config and the uglify config options. If you want to deliver ES6 or newer code, you have to use babel-minify (formerly known as babeli) with its webpack plugin. There are also other versions of UglifyJS that support ES6 code, but as far as I can tell, none of them are stable enough yet.

A typical config would be something like:

1 
2
3
4
5
6
7
8
9
10
11
12
13

const webpackConfig = { ... plugins: [ ... new webpack.optimize.UglifyJsPlugin({ compress: { screw_ie8: true, warnings: false } }) ... ]
}

Because the plugin configuration changed from webpack 1 to webpack 2, this module tries to bridge the gap for plugins that were not upgraded to the new syntax. The code below ensures all (older) plugins are minimized and no longer include debug code.

1 
2
3
4
5
6
7
8
9
10
11

const webpackConfig = { ... plugins: [ ... new webpack.LoaderOptionsPlugin({ minimize: true, debug: false }) ... ]
}

3. Make sure all dependencies are built in production mode

Many libraries only include code for specific environments. React is a good example of this. With the EnvironmentPlugin, you can specify values for the environment variables that are typically used by these libraries to opt-in or out of environment-specific code.

1 
2
3
4
5
6
7
8
9
10
11

const webpackConfig = { ... plugins: [ ... new webpack.EnvironmentPlugin({ NODE_ENV: 'development', DEBUG: false }) ... ]
}

Note: EnvironmentPlugin is actually a shortcut for a common pattern with the DefinePlugin.

4. Enable module concatenation

With the ModuleConcatenationPlugin, your dependencies and the related modules are wrapped in a single function closure as opposed to having function closures for each module. This process has minimal impact on the bundle size, but it can speed up the execution time of your code.

1 
2
3
4
5
6
7
8

const webpackConfig = { ... plugins: [ ... new webpack.optimize.ModuleConcatenationPlugin() ... ]
}

Note: Webpack has a nice visualization of this output on their GitHub repository.

5. Extract the CSS to enable better cacheability, parallel downloading, and parsing

Use the ExtractTextWebpackPlugin to extract text from specific loaders into their own files. This is common practice to get separated CSS files that can be cached by the browser and therefore, reduce your bundle size.

The code snippet below will also make sure that the css-loader is minimizing your CSS for production.

1 
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

const ExtractTextPlugin = require('extract-text-webpack-plugin')
const PROD = process.env.NODEENV === 'production' const webpackConfig = { ... module: { rules: [ { test: /.css$/, include: [ join('your', 'static', 'css', 'files'), /nodemodules/ ], use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: [ { loader: 'css-loader', options: { importLoaders: true, minimize: PROD } } ] }) } ] }, ... plugins: [ ... ExtractTextPlugin('style.css') ... ]
}

Webpack on a dies part II -image2

Webpack comparison Stat size Parsed size Gzipped size Webpack on a dies part II -image3 First paint on 3G and low-end mobile
intentionally unoptimized 1.69MB 1.76MB 399.17KB ~ 3292 ms
webpack -p 1.65MB 640.45KB 208.79KB ~ 2276 ms
manually optimized 1.56MB 564.25KB 166.39KB ~ 2240 ms

You can find the actual commit for this improvement here.

Sweet, this just gave us a 21% boost in the gzipped size compared to the default webpack -p approach. The first meaningful paint in Lighthouse, however, was only minimally improved. This is due to the delay of the emulated 3G connection, and we might not see much more improvement there going forward. There’s a chance that server-side rendering could help here, but our webpack diet doesn’t cover this.

6. Enable tree-shaking by using the new ES Modules syntax

ES5/ES6 Modules is the first standardized way of encapsulating JavaScript into reusable modules. It will replace CommonJS, UMD, AMD and others, plus it has been supported in Chrome since the beginning of 2017. We may see Node natively supporting ES6 Modules soon, also.

So how does this benefit your webpack? The new ES Modules syntax allows you to tree-shake your code, which means it will automatically exclude unused parts of code in your webpack bundle. It’s basically dead code elimination with some other neat tricks. This can significantly reduce your bundle size. If you want to dive deep into this—what it exactly is and how it works—here’s some recommended reading. Rollup, another JS module bundler besides webpack, has a wonderful explanation about the benefits using ES Modules. And this article by Rick Harris, the inventor of Rollup, explains the differences between dead code elimination and tree-shaking.

TL;DR When using webpack 2 or newer, the only thing you have to do is to replace your require with import statements. With our SDK, your code would go from this:

1

const { createClient } = require('contentful');

To this:

1

import { createClient } from 'contentful';

How to avoid dependency duplication

A simple way to avoid dependency duplication is to try and keep your dependencies up to date. This will help ensure that you don’t have the same dependency in different versions in your bundle.

In case a dependency does not have a version with an up to date lodash, try to open an issue on GitHub and ask the maintainer to update the dependencies and then re-release it. Hinting about the need for a dependency update in the issue queue is often enough to spark action.

The new npm 5 can also be an issue. If you update your dependencies one by one, the deduplication might fail due to your lock file—and you might end up with some duplicate dependencies. This process gave my colleague Khaled Garbaya a headache some time ago.

The following command can help to reduce dependency duplication, especially for projects that have been maintained over a longer period of time:

1

rm package-lock.json && npm i

Contentful’s contribution: new versions of CDA and CMA JavaScript SDKs

With major refactoring of the bundling and provisioning scripts, our own JavaScript SDK’s are now exported with an ES Modules version. Bundlers like webpack and RollUp will automatically pick these versions.

The SDKs come with a lot of other new features and tweaks, while the only breaking change was a rename of the browser bundles. Deciding to upgrade should be a no-brainer.

So, let’s integrate the new version into the example app. As you can see in the screenshot below, the big chunk contentful-management is gone and the code of the SDK now consumes a way smaller part of our file-upload app.

Webpack on a dies part II -image4

Webpack comparison Stat size Parsed size Gzipped size Webpack on a dies part II -image3 First paint on 3G and low-end mobile
intentionally unoptimized 1.69MB 1.76MB 399.17KB ~ 3292 ms
webpack -p 1.65MB 640.45KB 208.79KB ~ 2276 ms
manually optimized 1.56MB 564.25KB 166.39KB ~ 2240 ms
CMA with modules syntax 1.51MB 558.71KB 165.48KB ~ 2200 ms

You can find the actual commit for this improvement here.

Even when the JS CMA bundle size was optimized, the ES Modules version only gives us a minimal advantage. There are several reasons for this behavior:

Since we rely on lodash, the lodash code duplication got even worse. Our HTTP client Axios is pretty big, so we’re looking into a replacement. Any suggestions for a good alternative supporting proxies and caching in node and browsers are very welcome.

By breaking all dependencies of the Contentful SDK into modules, we can apply further optimization to our example app. We will do this in the next part of this blog post series.

By working our way through each of these steps, we’ve efficiently slimed down the size of our webpack bundle. We went from a gzipped size of 399.17KB, all the way down to 165.48KB. And in doing that, we managed to lower the load time on emulated bad 3G connections from around 3292 ms to about 2200 ms. Quite impressive!

If you’d like to see a complete webpack.config.js with all of the optimizations mentioned in this article, head over to our file-upload-example repository.

But wait, the best is still to come: In part three of this series, we’re going to tackle more detailed optimizations related to some pretty common modules like Moment.js, Babel, and Lodash. Check back on the Contentful Blog next week or follow us on Twitter to stay updated.


Tag cloud