Unpacking the Mysteries of Webpack -- A Novice's Journey

Slow incremental builds got you down? Let's figure it out together.

I'd worked on a handful of JavaScript applications with webpack before I inherited one in particular that had painfully sluggish builds. Even the incremental builds were taking up to 20 seconds...every single time I saved a change to a JS file. Being able to detect code changes and push them into my browser is a great feedback loop to have during development, but it kind of defeats the purpose when it takes so long.

so-slow

What's more, as a compulsive saver and avid collector of Chrome tabs, I basically lit my computer on fire as it screamed like an F-15 every time webpack ran one of these builds. I put up with this for awhile because I was scared of webpack. I shot a handful of awkward glances at webpack.config.js over the course of a few weeks. Right before permanent madness set in, I resolved to make things better. Thus started my journey into webpack.

What are you, webpack?

First off, what exactly is this webpack and what does it do? Let's ask webpack:

webpack is a module bundler for modern JavaScript applications. When webpack processes your application, it recursively builds a dependency graph that includes every module your application needs, then packages all of those modules into a small number of bundles - often only one - to be loaded by the browser.

In development, webpack does an initial build and serves the resulting bundles to localhost. Then, as mentioned earlier, it will re-build every time it detects a change to a file that's in one of those bundles. That's our incremental build. webpack tries to be smart and efficient when building assets into bundles. I had suspicions that the webpack configuration on the project was the equivalent of tying a sack of bricks to its ankles.

First off, I had to figure out what exactly I was looking at inside my webpack config. After a bit of Googling and a short jaunt over to my package.json, I discovered the project was using version 1.15.0 and the current version was 2.4.X. Usually newer is better -- and possibly faster as well -- so that's where I decided to start.

Next stop, webpack documentation! I was delighted to find webpack's documentation included a migration guide for going from v1 to v2. Usually migration guides do one of two things:

  1. Help.
  2. Make me realize how little I actually know about the thing and confuse me further.

Thankfully, upgrading webpack through the migration guide wasn't bad at all. It highlighted all the major configuration options I'd need to update and gave me just enough information to get it done without getting too in the weeds.

here-we-go

10/10, would upgrade again.

At this point, I had webpack 2 installed but I still had an incomplete understanding of what was actually in my config and how it was affecting any given webpack build. Fortunately, I work with a lot of smart, experienced Javascript developers that were able to point out a few critical pieces of configuration that needed attention. Focusing in on those, I started to learn more about what was going on under the hood as well as ways to speed things up without sacrificing build integrity. Before we get there though, let's take a pit stop and discuss terminology.

webpack, you talk funny.

As I was going through this process, I encountered a lot of terminology I hadn't run into before. In webpack land, saying something like "webpack dev server hot reloads my chunks" makes sense. It took some time to figure out what webpack terms like "loaders", "hot module replacement", and "chunks" meant.

Here are some simple explanations:

  • Hot Module Replacement is the process by which webpack dev server watches your project directory for code changes and then automatically rebuilds and pushes the updated bundles to the browser.
  • Loaders are file processors that run sequentially during a build.
  • Chunks are a lower-level concept in webpack where code is organized into groups to optimize hot module replacement

Paul Sherman's post was helpful early on for giving me some perspective on webpack terminlogy outside of webpack's own documentation. I'd suggest checking both of them out.

Now that we all understand each other a little better, let's dig into some of the steps I took during my dive into webpack.

Babel and webpack

Babel is a Javascript compile tool that let's you utilize modern language features (like Javascript classes) when you're writing code while minimizing browser and browser-version support concerns. Coming from Ruby, I love so much about ES6 and ES7. Thanks Babel!

But wait, weren't we talking about webpack? Right. So Babel has a webpack loader that will plug into the build process. In webpack 2, you use loaders inside rules in the top-level module config setting. Here's a sizzlin' example:

// webpack.config.js
{
  module: {
    rules: [
      {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        loader: 'babel-loader',
        options: {
          cacheDirectory: '.babel-cache'
        }
      }
    ]
  }
}

There are two particularly spicy bits in there that'll speed up your builds.

  1. Exclude /node_modules/ (directory and everything inside it) -- most libraries don't require you to run Babel over them in order for them to work. No need to burden Babel with extra parsing and compilation!
  2. Cache Babel's work -- turns out the Babel loader doesn't have to start from scratch every time. Add an arbitrary place for the Babel loader to keep a cache and you'll see build time improvements.

The speed, I can almost taste it. Let's not stop there though, because Babel has its own config -- .babelrc that needs tending to. In particular, when using the es2015 preset for Babel, turning the modules setting to false sped up incremental build times:

// .babelrc
{
  "presets": [
    "react",
    ["es2015", { "modules": false }],
    "stage-2"
  ]
}

Turns out that webpack is capable of handling import statements itself and it doesn't need Babel to do any extra work to help it figure out what to do. Without turning the modules setting off, both webpack and Babel are trying to handle modules.

workin

Riding the Rainbow with Webpack Bundle Analyzer

While searching the interwebs for webpack optimization strategies, I stumbled across webpack-bundle-analyzer. It's a plugin for webpack that will -- during build -- spin up a server that opens a visual, interactive representation of the bundles generated by webpack for the browser. Feast your eyes on the majestic peacock of the webpack ecosystem!

bundle-visual

So majestic. If you're like me, eventually you'd ask yourself, "But.. what does it mean!?". Got u fam.

Each colored section represents a bundle, visualizing its contents and their relative size. You're able to mouse over any of the files to get specifics on size and path. I didn't really know how to organize bundles and their contents, but I did notice a few things immediately based on the visual output of the analyzer:

  1. Stuff from node_modules in both bundles
  2. Big .json files in the middle of bundle.js
  3. A million things from react-icon bloating node_modules inside my main bundle.js. Ack! I'm sure react-icons is a great package, but are we really using hundreds of distinct icons? Not even close.

My next task was straightforward -- in concept -- but it took me awhile to figure out how to address each of those issues. Here's what I ended up with:

result

Thanks to the bundle analyzer, I learned some helpful things along the way. I'll step through the solutions to each of the problems I listed above.

Vendor Code Appearing in Multiple Bundles

Solution: CommonsChunkPlugin

Using CommonsChunkPlugin, I was able to extract all vendor code (files in node_modules and manifest-related code (webpack boilerplate that helps the browser handle its bundles) into their own bundles. Here's some of the related config straight out of my webpack.config.js:

{
  plugins: [
    new webpack.optimize.CommonsChunkPlugin({
      name: 'vendor',
      minChunks: function(module) {
        return module.context && module.context.indexOf('node_modules') !== -1
      }
    }),

    new webpack.optimize.CommonsChunkPlugin({
      name: 'manifest'
    })
  ]
}

Big .json Files in the Main Bundle

Solution: Asynchronous Imports

The app was only using the JSON files in a few React components. Rather than using import at the top of my React component files, I moved the import statements into the componentWillMount function (lifecycle callback). When webpack parses import statements inside functions, it knows to separate those files into their own bundles. The browser will download them as needed rather than up front.

Unused Dependencies

Solution: Single File Imports

With react-icons in particular, there are multiple ways to import icons. Originally, the import statements looked like this: import CloseIcon from 'react-icons/md/close'

react-icons also has a compiled folder (./lib) where pre-built icon files can be imported directly. Updating the import statements to use the icons from that path eliminated the extra bloat: js import CloseIcon from 'react-icons/lib/md/close'

That covers the things I learned from the bundle analyzer. To wrap up, I'll cover one other webpack config option that made a big difference.

Pick the Right devtool

Last, and certainly not least, is the devtool config setting in webpack. The devtool in webpack does the work of generating source maps. There are a number of options that all approach source map generation differently, making tradeoffs between build time and quality/accuracy. After trying out a number of the available source map tools, I landed on this configuration:

// webpack.config.js
{
  devtool: isProd ? 'source-map' : 'cheap-module-inline-source-map',
}

webpack documentation recommends a full, separate source map for production, so we're using source-map in production as it fits the bill. In development, we use cheap-module-inline-source-map. It was the fastest option that still gave me consistently accurate, useful file and line references on errors and during source code lookup in the browser.

Journey Still Going (Real Strong)

At this point, I'm still no expert in webpack and its many available loaders/plugins, but I at least know enough to be dangerous -- dangerous enough to slay those incremental build times, am i rite?

yes

Ryan Stenberg

Ryan Stenberg