Webpack Bundle Splitting

Webpack, Webpack, Webpack. Seems like that's all I ever talk about these days. That's because I'm giving a webinar on the topic on Wednesday :) Will I see you there?

Today let's talk about bundle splitting.

I alluded yesterday to Webpack's ability to break our bundle into smaller files (aka 'chunks') so that changes on one file don't necessitate a full download of all your source code for your users when they return to your site. I'm going to show you how to configure your project with bundle splitting, and discuss some of the considerations and tradeoffs when deciding how to set things up.

Here's a pretty good starter config:

// webpack.config.js

module.exports = {
  entry: "./src/index.js",
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: '[name].[chunkhash].js'
  },
  optimization: {
    splitChunks: {
      chunks: 'all',
      name: 'chunk',
      cacheGroups: {
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendor'
        }
      }
    },
    runtimeChunk: true
  }
}

Our entry and output properties say that we'll start with src/index.js and build a finished bundle to dist/index.[chunkhash].js, where chunkhash is the computed hash from the contents of the file.

The new bit is the optimization block. Webpack gives us many configuration options to optimize our builds, most of which are outside the scope of what I want to get into here. As it's configured here, we're doing three things:

First, look at splitChunks.cacheGroups.

  //...
  cacheGroups: {
    vendor: {
      test: /[\\/]node_modules[\\/]/,
      name: 'vendor'
    }
  }
  //...

This is saying that I want a chunk called "vendor", which contains any module with node_modules in the path. The name: 'vendor' declaration is the name that will be fed into the filename definition in the output block. So the resulting chunk here will be called vendor.d2e3a6d3b2e3ef.js, and contain all the modules that are imported from npm.

Next, we have the runtimeChunk: true entry.

runtimeChunk: true

This will create a separate chunk called runtime.d2e3a6d3b2e3ef.js, which is just the Webpack boilerplate for registering and loading the transpiled modules in the build. I'll go into more about why Webpack has to maintain a runtime at all in the next email.

Finally, take a look at the top properties of the splitChunks block:

chunks: 'all',
name: 'chunk'

These options are actually irrelevant with the specific setup here, because we only have one entrypoint. But if you configured multiple entrypoints at the top of the file, this configuration would tell webpack to find the modules that they import in common and extract them into their own chunk, so they don't have to be downloaded twice. This is the scenario I described last week when I first introduced you to the notion of bundles.

Like I said, this is a good general-purpose configuration for chunking your project, but the internet is full of opinions about how to do it best. Most of them come down to weighing the savings of raw bytes downloaded that you get by fine-grained chunking against the overhead of lots of HTTP requests.

The thing is, this debate is not relevant anymore. Request overhead and resource blocking were real considerations with HTTP/1, but with HTTP/2 they are no longer issues to worry about in most cases. If you want to read more on the subject, I found this article to be a very compelling argument for creating many small chunks. Remember though that premature optimization is the root of all evil. If you haven't identified a specific reason push for the level of optimization described in that article, you'd probably do just as well to stick with some reasonable defaults.

Enjoy your weekend. More on Monday.


Did you like this?

I send a daily email with tips and ideas like this one. Join the party!

Icon