Splitting Bundles with Webpack 4

In this article, I will introduce the concept of bundle splitting, and provide two simple examples that illustrate how to implement bundle spltting and it’s benefits.

The source code for this article is here.

Introducing optimization.splitChunks

The optimization.splitChunks option allows the main.js generated by webpack by default to be split into multiple chunks - in other words, multiple files. Let’s see the most common use case: splitting initial and vendor assets.

Firstly, a simple example. We are building a hello world Vue application, and want to use bundle splitting to improve the performance of our application, and reduce the load on our server.

Setup

Create a package.json run running echo {} >> package.json, and install wepback and vue:

npm install webpack webpack-cli vue --save

And create a webpack config and entry point:

touch webpack.config.js
mkdir src 
touch src/index.js 
touch src/create-app.js 

In src/create-app.js, create a new Vue app as follows:

import Vue from "vue"

export default function createApp() {
  const el = document.createElement("div")
  el.setAttribute("id", "app")

  document.body.appendChild(el)

  new Vue({
    el: "#app",
    render: h => h("div", "Hello world")
  })
}

Import create-app and execute it in src/index.js:

import createApp from "./create-app"

document.addEventListener("DOMContentLoaded", () => {
  createApp()
})

Add a minimal webpack config:

const webpack = require("webpack")

module.exports = {
}

Now run npx webpack --mode development to bundle using the default settings:

Hash: 6aca10b38e4ad1df98f1
Version: webpack 4.12.0
Time: 355ms
Built at: 2018-06-09 15:56:50
  Asset     Size  Chunks             Chunk Names
main.js  236 KiB    main  [emitted]  main
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 489 bytes {main} [built]
[./src/create-app.js] 246 bytes {main} [built]
[./src/index.js] 110 bytes {main} [built]
    + 4 hidden modules

main.js 236 KiB - that’s a large payload, just for hello world. We only wrote about 10 lines of code, though. The rest is all of vue.js. We can split the vendor code (vue.js) and our code src/index.js.

Splitting Into Two Bundles

Update webpack.config.js:

const webpack = require("webpack")

module.exports = {
  optimization: {
    splitChunks: {
      cacheGroups: {
        commons: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendor",
          chunks: "initial",
        }
      }
    }
  }
}

More details about optimization are here. This basically wll bundle any code from node_modules into a file called vendor.js, and our own code into main.js.

Run npx wepback --mode development again:

Hash: 128feabada91842ba3ce
Version: webpack 4.12.0
Time: 362ms
Built at: 2018-06-09 15:57:21
    Asset      Size  Chunks             Chunk Names
  main.js  7.62 KiB    main  [emitted]  main
vendor.js   231 KiB  vendor  [emitted]  vendor
[./node_modules/webpack/buildin/global.js] (webpack)/buildin/global.js 489 bytes {vendor} [built]
[./src/create-app.js] 246 bytes {main} [built]
[./src/index.js] 110 bytes {main} [built]
    + 4 hidden modules

Much better. main.js is only 6.62 KiB (and will be even smaller once bundled with --mode production.

Now we can serve vendor.js from a CDN, reducing the amount of code our server has to transfer, and speeding up the delivery of long term cacheable assets. Let’s make a quick index.html to see this working:

touch index.html

And inside index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
</head>
<body>
</body>
<script src="/dist/main.js"></script>
<script src="/dist/vendor.js"></script>
</html>

Run a server with python -m SimpleHTTPServer, and access localhost:8000. You should see “Hello world”. Inspect the devtools > network tab, you can see the bundles:

Name        Size
main.js     7.6 KB 
vendor.js   231 KB

Splitting Local Code

We can also split our own code into multiple pieces. Let’s make a few simple functions, and split them up.

touch src/my-module-1.js

Inside, add a greet function:

export default function greetingOne() {
  const el = document.createElement("div")
  
  el.innerText = "Hello from my module!"

  document.body.appendChild(el)
}

We want a few more chunks to split. Copy the module twice:

cp src/my-module-1.js src/my-module-2.js
cp src/my-module-1.js src/my-module-3.js

To make it clear what is going on, update src/index.js and import the new modules we made, while commenting out create-app:

// import createApp from "./create-app"

import firstGreeting from "./my-module-1"
import secondGreeting from "./my-module-2"
import thirdGreeting from "./my-module-3"

document.addEventListener("DOMContentLoaded", () => {
  // createApp()

  firstGreeting()
  secondGreeting()
  thirdGreeting()
})

Try building this with npx webpack --mode development:

Hash: dc9e548302c4e4708a02
Version: webpack 4.12.0
Time: 111ms
Built at: 2018-06-09 16:00:29
  Asset      Size  Chunks             Chunk Names
main.js  6.48 KiB    main  [emitted]  main
[./src/index.js] 298 bytes {main} [built]
[./src/my-module-1.js] 162 bytes {main} [built]
[./src/my-module-2.js] 162 bytes {main} [built]
[./src/my-module-3.js] 162 bytes {main} [built]

We can see each my-module, after compilation, comes out at 162 bytes. Let’s split each into their own chunk with AggressiveSplittingPlugin. Update webpack.config.js:

// ...

module.exports = {
  plugins: [
    new webpack.optimize.AggressiveSplittingPlugin({
      minSize: 100,
      maxSize: 200,
    })
  ],

  // ...
}

minSize and maxSize are bytes. Each my-module is 162 bytes. By specifying a maxSize of 200, each my-module will be split into it’s own chunk, in order not to exceed the maxSize of 200. Compile again with npx webpack --mode development:

Hash: d70d8ef6e435320e0d10
Version: webpack 4.12.0
Time: 114ms
Built at: 2018-06-09 16:03:27
Asset       Size  Chunks             Chunk Names
 0.js    7.2 KiB       0  [emitted]
 1.js  719 bytes       1  [emitted]
 2.js  719 bytes       2  [emitted]
 3.js  719 bytes       3  [emitted]
[./src/index.js] 298 bytes {0} [built]
[./src/my-module-1.js] 162 bytes {1} [built]
[./src/my-module-2.js] 162 bytes {2} [built]
[./src/my-module-3.js] 162 bytes {3} [built]

Now each my-module has it’s own chunk. The combined size of the separate chunks is a lot large, because of all the boilerplate webpack adds (inspect any file to see). For these tiny modules, it doesn’t make sense to split the code, but it does serve is a simple example. Simply import each chunk as a <script> tag if you want to see this working.

In a large complex application though, splitting can be very beneficial. For example, each chunk could be separate page, which is asynchronously loaded only when a user visits that page. This is something I will discuss in a future article.

Conclusion

We learned how to

Improvements

Some improvements could be:

The source code for this article is here.