Put Your Webpack Bundle On A Diet - Part 4

In part one, two, and three of this four-part series on webpack optimization we have learned various ways to slim down the payload.

By reading the fourth and final part of this series, you will learn how to tie it all together using lazy loading to get your webpack size just right.

Split up your bundle and apply lazy loading

Webpack by default squeezes your code and assets into one bundle, and with all of the improvement we made in the last parts, we are almost at the maximum of what we can achieve with JavaScript and CSS optimizations. But there are a few more tricks that you should know about.

Getting started with chunking

We’ll start by using the so-called chunking process to split the bundle up into multiple chunk files, which enables browsers to do very effective caching. We will put all libraries into a vendor chunk and then separate it from the business logic.

Because library updates are not as common as changes to your app’s code, they only need to redownload the chunk that contains your actual app. The libraries will stay cached on their machine until you purposefully update them.

Afterwards, we’ll split up your app code into multiple chunks and then set up your app to only transmit code that is relevant to the current route where the user is looking. Code related to other routes will be loaded later, and only when the user needs the code. This principle is called lazy loading and the webpack docs have a wonderful guide about lazy loading right here.

Webpack recommends a maximum chunk size of 250kb to avoid long running parsing of your code. This is especially important on low-end devices and is easily achievable with chunking and lazy loading.

Note: I want to thank John Tucker for his excellent medium article about the webpack commons chunk plugin because my code snippets are based on his article.

The quickest option—create a vendor chunk automatically

This is a very straightforward but effective version, especially when you can’t invest the time for detailed splitting or it’s not necessary for your app:

1
2
3
4
5
6
7
8
9
10
11
new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: ({ resource }) => (
    resource !== undefined &&
    resource.indexOf('node_modules') !== -1
  )
}),
new webpack.optimize.CommonsChunkPlugin({
  name: 'manifest',
  minChunks: Infinity
})

The code above creates a vendor chunk that includes all of your dependencies out of the node_modules folder. This is preferred over manually creating the vendor chunk since it ensures you do not leave out any required dependencies.

Another chunk called manifest will also be created. This specific chunk contains the loader logic for the webpack chunks. Make sure to include the manifest as the first chunk in your HTML, followed by the other script tags.

Important: To enable effective caching and cache busting, ensure that your chunks include their hash value in their filename. You can do this like so:

1
2
3
4
5
6
7
8
const webpackConfig = {
  ...
  output: {
    filename: '[name]-[chunkhash].js',
    chunkFilename: '[name]-[chunkhash].js'
  },
  ...
}

How to use the ExtractTextPlugin

You need to tell the ExtractTextPlugin to create one css file per chunk, so make sure you are not using a fixed filename for it:

1
2
3
4
5
6
7
8
9
10
11
const webpackConfig = {
  
  plugins: [
    ...
    new ExtractTextPlugin({
      filename: '[name]-[chunkhash].css'
    })
    ...
  ],
  ...
}

You may also want to force some code to be included in the vendor file. Doing so can be handy for modules like normalize.css:

1
2
3
4
5
6
7
8
9
10
const webpackConfig = {
  
  entry: {
   app: [join(webpackSource, 'app.js')],
   vendor: [
     'normalize.css'
   ]
 },
  ...
}

So let’s have a look at the resulting bundle:

webpack4 image1

Webpack optimization Stat size Parsed size Gzipped size First paint on 3g
and low-end mobile
Webpack 3.6
build time
Optimized single-chunk app 841.94KB 287.42KB 89.64KB ~ 1708 ms ⌀ 9s
Vendor-chunk
App-chunk
Manifest

Total
777.63KB
64.31KB
0

841.94KB
251.48KB
35.63KB
1.5KB

288.61KB
77.87KB
11.35KB
806B

90.02KB
~ 1686 ms ⌀ 9.2s

Check out the commit for this improvement right here

Getting advanced with asynchronous lazy loading

In bigger applications, you might have code that is only needed for some parts of your app. Code that some users might never execute — or at least not right away.

It is often a good idea to split these parts apart of your main bundle, to reduce the initial payload of your app.

Since manually splitting can be somewhat difficult we’ll automate it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new webpack.optimize.CommonsChunkPlugin({
  name: 'vendor',
  minChunks: ({ resource }) => (
    resource !== undefined &&
    resource.indexOf('node_modules') !== -1
  ),
}),
new webpack.optimize.CommonsChunkPlugin({
  name: 'main',
  children: true,
  async: true,
  minChunks: ({ resource }) => (
    resource !== undefined &&
    resource.indexOf('node_modules') !== -1
  ),
})

But one more step is missing — we need to tell webpack where to split the code.

This is done by doing dynamic imports by using the import() syntax that in turn implements a promise interface.

1
2
3
System.import(/* webpackChunkName: "chunkName" */ 'path/to/code.js')
.then(module => module.default)
.then(yourModule => yourModule('do sth'))

If you are using react-router, you should read this guide on dynamic routing (Note: In the article they are using the old require.ensure syntax from webpack 1. You should go for using the new import syntax when using webpack)

Preact users can use preact-async-route to enable dynamic routing.

For Vue.js users a solution can be found in the Vue.js docs. Angular provides lazy loading via the loadChildren property on routes.

webpack4 image2

Webpack optimization Stat size Parsed size Gzipped size First paint on 3g
and low-end mobile
Webpack 3.6
build time
Optimized single-chunk app 841.94KB 287.42KB 89.64KB ~ 1708 ms ⌀ 9s
Vendor-chunk
App-chunk
Manifest

Total
777.63KB
64.31KB
0

841.94KB
251.48KB
35.63KB
1.5KB

288.61KB
77.87KB
11.35KB
806B

90.02KB
~ 1686 ms ⌀ 9.2s
Vendor-chunk
App-chunk
Assets-chunk
Manifest

Total
690.26KB
61.06KB
97.04KB
0

848.36KB
220.19KB
33.92KB
36.31KB
1.5KB

291.92KB
67.62KB
10.67KB
12.30KB
806B

91.40KB
~ 1624 ms ⌀ 9.2s

Check out the commit for this improvement on GitHub.

As you can see the total size increased somewhat. But for the first page load we only need to load the vendor chunk, the app chunk and the manifest. The assets chunk will be loaded later when the user navigates to the assets route of our app.

This means the user only has to load 79.09KB when they visit the page instead of 90.02KB (gzipped). The benefit will increase significantly when the app is more complex compared to our very simple file-upload-example app.

Summary

This blog post concludes this series on webpack optimization. We have come a long way since working with the webpack-bundle-analyzer in the first post, using the loaderOptionsPlugin in part two and parsing with Moment.js in part three.

But there are still ways to update assets such as images and fonts to reduce your bundle even further. I will write about this and many more tricks in a future blog post.

What's next

Getting started with Contentful is easy and free. Just head over to our sign-up page and create your account. You will be up and running in no time with our reliable content infrastructure.

Blog posts in your inbox

Subscribe to receive most important updates. We send emails once a month.