Webpack is a reasonably advanced tool that gives you a lot of capability to control your Javascript bundles. This control was mentioned in a prior article, and using Webpack well gives you the power to improve performance drastically. However, you will need to dig into those Webpack bundles once running and figuring out what you can optimize. If you are just getting started with Webpack, be sure to start with my article to be a better primer for this one. Let’s explore just how to measure Webpack bundles.
Suppose you are looking for examples for further examples from what I mentioned in this article. I made a “template” project with Vue and focused on aspects of performance. It utilizes Webpack heavily in the project to produce optimized bundles. Check it out here. This project tries to do by example many of the things I suggest here and is a good template building performant front ends. The project will further improve overtime as well, so check back in on it.
Focus on analyzing the production builds
Its common practice to split up your webpack builds into 3 or more different webpack configurations. They can all inherit from a base or common config for shareable build configuration. The three core configurations usually are along these lines:
- Development: Webpack config that builds in such a way to optimize debugging, leaving as much code un-minified, source mapped, and whatever helps with development flow.
- Test: Special config for building in a way that suits whatever test runners or harnesses your after. This build would be specific for unit tests and an end to end tests.
- Production: This config is for release to the user and means business. This build’s output should focus on bundling and minifying the source code to help with the output bundle size.
A typical folder structure would look like so:
- webpack.config.common.js
- webpack.config.development.js
- webpack.config.test.js
- webpack.config.production.js
Test and development packs are not optimizing the bundle output and often intentionally have extra overhead with them. Because of this, you want to focus your efforts on the production build. Ensure you configure your package.json for commands to build with the production webpack config and then analyze immediately.
For example, this is what I typically use in my package.json setup (do note I’ll get to explaining the analyze step below shortly):
{
"scripts": {
"analyze": "webpack-bundle-analyzer dist/stats.json --report dist/report.html -m static",
"compile": "webpack --config build/webpack.config.prod.js",
"compile:analyze": "npm run compile && npm run analyze"
},
...
}
The compile and analyze build command should not involve the use of the webpack development server. (The development server adds a bunch of extra overhead to allow hot module replacement). Therefore, If you see anything with web sockets and hot module bundles inching their way into your pack, it is highly likely you may have used the development server build with your analysis.
The stats.json file
If requested (by config or command-line argument), every time Webpack runs, it will produce an output file called stats.json filled with diagnostics information from the build. To ensure you output the stats.json file with the build, you can do either of the following:
- Add this to the command line call:
- webpack –profile –json > stats.json
- –json tells Webpack to output the stats, and the > stats.json indicates you want the output to go into a file
- –profile adds extra output that goes into module-specific output
- If you use WebPackAnalyzer (I’ll get into this more below), it has an option built into output the stats.json for you (so it’ll be built into the Webpack config files instead.
- webpack –profile –json > stats.json
You can often find this output in the ‘dist’ directory off the root of your project. (Which is a standard output directory). Otherwise, it will be where ever you’re output from Webpack is set. The configuration looks as so:
const ROOT = path.resolve(__dirname, "..");
function root(args) {
args = Array.prototype.slice.call(arguments, 0);
return path.join.apply(path, [ROOT].concat(args));
}
output: {
path: root("/dist"),
filename: "js/[name].[hash].js",
chunkFilename: "js/[name].[hash].js",
publicPath: "/"
}
The stats.json is often quite large and full of information, but it is usually not time-efficient to use it directly. It is essential to know where and what it is because all the tooling I will mention will depend on it. Here is a further read for good detail on the structure of the stats.json file.
As a small side note, that stats.json is not related to the stats Webpack build configuration. This configuration is for the direct console output while running the Webpack build (instead of the diagnostic output to the stats.json file).
Tools for measuring Webpack bundles
There are numerous bundle analysis tools; some don’t even need Webpack. However, I will focus on the ones I have used personally and recommend them as they take little setup and provide excellent value. I prefer UI tools for this as it is quick to see the bundles’ spatial size differences.
Webpack Bundle Analyzer
The Webpack Bundle Analyzer is a great tool to get started with as it helps visualize what is inside your bundles and has some excellent utility built-in. The UI looks as so:
The left rail shows you all of the Webpack bundles you produced, and the main view gives a full breakdown and visual of your bundles’ relative size. The big rectangles of different colors are the bundles, and the sub rectangles in those bundles are modules in that bundle. In addition, the UI can zoom in and see where you should invest your time in trimming the fat.
It has a plugin that integrates with your Webpack builds or runs on the command line. I showed an example of a package.json earlier above that uses Webpack Bundle Analyzer on the command line. Here is my typical setup with the Webpack Plugin apart of the build config (note that I use this tool to generate the stats file):
plugins = [
new BundleAnalyzerPlugin({
analyzerMode: "static",
openAnalyzer: false,
defaultSizes: "gzip",
generateStatsFile: true
}),
]
You can either set openAnalyzer to true to auto open the web UI or run the npm run compile:analyze
from my example package.json earlier. Lastly, directly on the command line with webpack-bundle-analyzer dist/stats.json --report dist/report.html -s stat -m static
.
Webpack Visualizer
I often use Webpack Bundle Analyzer for keeping an eye on my immediate bundles and their sizes. However, one tough thing that it does not do well with visualizing is how rogue dependencies sneak into your bundles. For example, let’s say you upgrade all your dependencies in package.json. One of those dependencies introduced a rogue dependency that added 1 MB to your bundle! Who did it?! Webpack Bundle Analyzer will give you a nice visual of it being there and taunting you.
Webpack Visualizer helps with this issue. Several command-line tools do a good job; heck, you can even search the dependency lock file or scrape node_modules for it. I much much prefer the visual approach as it shows the scale better.
This tool can optionally be installed locally and ran similar to Webpack Bundle Analyzer, but I much prefer just using the web app linked above. You can drag and drop your stats.json there, and voila.
With this visual representation, I quickly see that my web app’s total dependencies are ~64% external. Each segment outwards on the circle is a different dependency module pulled in by the inner ring. I can likely visually see that 1 MB addition pretty quickly and travel up or down the layers to find what is pulling it in.
Webpack Visualizer is a great tool and allows you to focus on specific bundles that make searching for things more manageable. The only gripe I have with it is that it is missing a helpful search tool, which would be great for highlighting and quickly finding things.
Setting your max bundle size limits
If you haven’t already done so, I’d consider it a best practice to find how much space your critical bundles take and set those as upper maximum size limits. By “critical,” I mean bundles that are needed when your app first loads for either first render or immediate functionality. Since it’s always important that these critical bundles have a watchdog on them so they don’t unexpectedly grow, let’s set one up now.
By default (unless you squelch the Webpack output), Webpack will output many diagnostic data to the command line during its build.
A lot of this diagnostic data can be found similarly in the Webpack Bundle Analyzer. To the rightmost column is the name of the produced bundle. Three columns to the left of that are the bundle sizes. Now that we have this information, we can make some reasonable decisions on setting our bar for the project.
Setting your project maximum bar
The goal of establishing a bar now rather than later is to prevent your critical bundles from growing. They will become more prominent over time. However, when they do, it should be your (or your team’s) decision to acknowledge the growth. Then define or question what is too much. You can then push the maximum bar higher with that decision. Without having a bar allows uncapped growth. The uncapped growth will lead to more clean-up at some point. This will cost more than if you were to have done this preventative measure as you will also need to spend time tracking down the culprit.
Webpack allows setting bundle size limits built-in by default. Here is an example of the production configuration from my Vue performance template project.
webpackConfig.performance = {
hints: "warning",
maxEntrypointSize: 400000,
maxAssetSize: 250000
};
It is not yet optimal for the max size of my bundles. I need to move the maximum bar down quite a bit in this case. Also, I can make it an error instead of warning should these limits start to be exceeded. After using Webpack Analyzer to see my bundles, I can change the configuration to:
webpackConfig.performance = {
hints: "error",
maxEntrypointSize: 400000,
maxAssetSize: 180000
};
You can test that the bar is low enough as reducing it lower will produce an error (similar to below) letting you know the limit was exceeded:
ERROR in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (381 KiB). This can impact web performance.
Entrypoints:
main (391 KiB)
js/runtime.f9f501576b43fc2d78c6.js
css/0.7840a3a5b1b7aacace89.css
js/npm.bootstrap-vue.f9f501576b43fc2d78c6.js
css/18.9c016cc5693253778eea.css
js/styles.f9f501576b43fc2d78c6.js
js/npm.process.f9f501576b43fc2d78c6.js
js/npm.setimmediate.f9f501576b43fc2d78c6.js
js/npm.timers-browserify.f9f501576b43fc2d78c6.js
js/npm.vue-router.f9f501576b43fc2d78c6.js
js/npm.vue.f9f501576b43fc2d78c6.js
js/npm.vuex.f9f501576b43fc2d78c6.js
js/npm.webpack.f9f501576b43fc2d78c6.js
css/6.b86b0acfc6c5dedd874e.css
js/main.f9f501576b43fc2d78c6.js
This concludes my rundown on measuring Webpack bundles. If you enjoyed this article, I recommend you check out my other articles on Webpack. In the comments below, let me know if there is anything else you are curious about, and I will be sure to focus efforts there.