webpack bundle analyzer answers one question with brutal clarity: what is actually inside the JavaScript you ship? The webpack-bundle-analyzer plugin renders every module in your build as a zoomable treemap sized by bytes, and while most teams install it to fight bloat, its more interesting use is as a supply chain instrument — it shows you duplicated packages, transitive dependencies you never chose, moment locales you never localized, and occasionally source code that should never have left the building. This guide covers setup, reading the treemap correctly, and turning a one-off curiosity tool into a CI guardrail.
Setup in two minutes
Install as a dev dependency and add the plugin:
npm install --save-dev webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
plugins: [
new BundleAnalyzerPlugin({
analyzerMode: 'static', // writes report.html, no server
openAnalyzer: false,
generateStatsFile: true, // stats.json for CI diffing
}),
],
};
Two modes matter. server (the default) spins up a local viewer on port 8888 — fine on a laptop, wrong in CI. static writes a self-contained report.html you can archive as a build artifact. The npm webpack-bundle-analyzer package also ships a CLI, so you can decouple analysis from the build entirely:
webpack --profile --json > stats.json
npx webpack-bundle-analyzer stats.json dist --mode static --report bundle-report.html
Next.js users get the same engine prewrapped as @next/bundle-analyzer; Vite users want rollup-plugin-visualizer instead, since Vite's production build is Rollup-based.
Reading the treemap: three sizes, one truth
The report toggles between stat (raw module size before transforms), parsed (size in the output bundle after minification), and gzip. For "what does the user download," gzip is the number. For "what does the browser parse and execute," parsed size is what correlates with main-thread cost. Stat size is mostly useful for spotting modules where minification is doing heroic work — often a sign of embedded data blobs.
The instinctive read is "find the biggest rectangle." The higher-value reads:
- Same package, multiple rectangles.
lodashappearing under twonode_modulespaths means version conflicts prevented deduplication. Runnpm dedupe, or find which dependency pins the odd version withnpm ls lodash. - Full library where you use one function. A complete
momentwith all locales (historically hundreds of KB) when you format one date, or all oflodashbecause someone wroteimport _ from 'lodash'instead ofimport debounce from 'lodash/debounce'. - Polyfills you no longer need.
core-jssized for browsers your support matrix dropped two years ago. - Things that should not be in a browser bundle at all. Node-only libraries dragged in by an isomorphic import, test fixtures, or a
.env-adjacent config module. This is the category that turns a performance tool into a security tool.
The bundle is a supply chain artifact
Your package.json says what you asked for; the treemap shows what you got. That difference is where supply chain risk hides in frontend builds.
Every rectangle in the treemap is code that executes in your users' browsers with your origin's privileges. A vulnerable or malicious transitive package matters very differently depending on whether it lands in the bundle: a prototype-pollution flaw in a build-time-only tool never reaches users, while the same flaw in something bundled does. The analyzer gives you the bundled set; cross-referencing it against your vulnerability findings tells you which advisories are actually exposed in the client. This is a manual version of what reachability-aware SCA tooling like Safeguard automates — distinguishing "in the lockfile" from "in the artifact you ship."
Concrete checks worth doing quarterly, or after any large dependency change:
- Inventory surprise packages. Sort the module list (the sidebar, not the treemap) and scan for names you do not recognize. Trace each with
npm ls <name>. Most are boring; the exercise is cheap. - Check for secrets and internal paths. Search the generated
stats.jsonfor strings likeapiKey, internal hostnames, or absolute paths from your build machine. Webpack'sDefinePluginmisuse — inlining an entireprocess.env— shows up here before it shows up in an incident report. - Diff against the last release. A dependency hijack that injects code produces a size delta. It will not catch a careful attacker, but the infamous cases (coin miners, credential skimmers appended to popular packages) added tens of KB — visible at a glance in a treemap diff.
Making it a CI guardrail
A report nobody opens is shelfware. Two lightweight enforcement patterns:
Size budgets with webpack itself. The analyzer diagnoses; webpack's performance config enforces:
performance: {
maxAssetSize: 250000, // bytes, post-minification
maxEntrypointSize: 400000,
hints: 'error', // fail the build, not just warn
},
Stats diffing in pull requests. Generate stats.json on every PR build, compare total and per-chunk parsed sizes against the base branch, and comment the delta. Tools like bundlesize, size-limit, or a small script over two stats files all work. The rule that keeps signal high: block only on unexplained growth. A PR adding a charting feature will grow the bundle; the reviewer's job is checking that the growth matches the intent — 40 KB for a chart library is plausible, 400 KB means someone imported the wrong entry point.
Archive the static HTML report as a build artifact on release builds. When an incident response question arrives months later — "did version 3.4.1 ship the vulnerable module?" — an archived treemap answers in seconds what re-creating an old build environment answers in hours. Pair it with the SBOM you already generate for the release, and you have both the declared and the delivered inventory; the Safeguard Academy has a walkthrough on wiring exactly this into a release pipeline.
Common misreads to avoid
- Optimizing stat size. Teams burn sprints shrinking a number users never experience. Optimize gzip for transfer, parsed for execution.
- Treating tree-shaking as guaranteed. The treemap shows what survived tree-shaking, which depends on
sideEffectsflags and ESM-vs-CJS packaging of each dependency. If a "tree-shakable" library appears whole, check whether you are importing its CommonJS build. - Ignoring lazy chunks. A perfect initial bundle with a 2 MB lazily-loaded admin chunk still ships 2 MB to admins. Budget every entry point and the largest async chunks, not just
main. - One-time cleanup, no follow-up. Bundles regrow. The half-life of an unmonitored bundle diet is about two quarters.
FAQ
What does webpack bundle analyzer actually measure?
It parses webpack's build output and renders every bundled module as a treemap with three size measures: stat (pre-transform), parsed (post-minification, what the browser executes), and gzip (what the network transfers).
How do I run webpack-bundle-analyzer in CI without a server?
Use analyzerMode: 'static' with openAnalyzer: false to write a standalone report.html, and generateStatsFile: true to emit stats.json for programmatic diffing. The default server mode is for local use only.
Can webpack bundle analyzer find security issues?
Indirectly, and usefully: it reveals which dependencies actually ship to browsers (where client-side vulnerabilities are exposed), surfaces surprise transitive packages, and makes suspicious size jumps between releases visible. It is an inventory tool — pair it with a vulnerability scanner for advisories.
What is the equivalent for Vite or Next.js?
Next.js wraps this same plugin as @next/bundle-analyzer. Vite's production build uses Rollup, so use rollup-plugin-visualizer, which produces a comparable treemap.