Unpacking the Mysteries of Webpack -- A Novice's Journey
Ryan Stenberg, Former Developer
Article Category:
Posted on
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.
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:
- Help.
- 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.
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.
- 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! - 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.
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!
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:
- Stuff from
node_modules
in both bundles - Big
.json
files in the middle ofbundle.js
- A million things from
react-icon
bloatingnode_modules
inside my mainbundle.js
. Ack! I'm surereact-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:
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:
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?