Plate 89
Is webpack-bundle-analyzer Accurate? A 2.3x Size Gap
webpack-bundle-analyzer 5.4.0 vs source-map-explorer 2.5.3 on one webpack 5.111 production build (148,569 B): date-fns 44,313 B vs 19,196 B (2.3x) because of module concatenation estimates; lodash 70,136 vs 70,312 B. CLI medians 313 ms vs 224 ms (JSON).
Aditya Challa5 min read
I ran webpack-bundle-analyzer 5.4.0 and source-map-explorer 2.5.3 on the same webpack 5.111 production build, and they disagreed on date-fns by 2.3x: 44,313 bytes in the analyzer versus 19,196 bytes in source-map-explorer. Lodash matched within 176 bytes, so the gap was not random; it came from webpack's module concatenation, which forces the analyzer to estimate sizes inside the merged module.
Short answer: use source-map-explorer when you need to know which package actually owns the bytes in a minified, scope-hoisted bundle. Keep webpack-bundle-analyzer for chunk-level views, gzip numbers and quick treemaps, and read its numbers inside a "(concatenated)" box as estimates. Speed is not the deciding factor here: source-map-explorer took a median 224 ms for JSON output and the analyzer CLI took 313 ms. No affiliate links in this post.
What I tested
- Machine: 8 vCPU Intel Xeon / 15 GB RAM Linux cloud box, Node 20.19.2, tested 5 Oct 2026 (about 23:36 to 23:42 IST). Shared box; runs interleaved after one warmup each.
- Fixture:
/workspace/bench-sme-wba/with one entry, 120 small feature modules that each importformatandaddDaysfrom date-fns 4.4.0, plus a default lodash 4.18.1 import. webpack 5.111.1 + webpack-cli 7.2.3,mode: 'production',devtool: 'source-map'. Output:main.js148,569 B andmain.js.map930,179 B;stats.json3.78 MB. - Tools:
source-map-explorer2.5.3 readingmain.js+ its map;webpack-bundle-analyzer5.4.0 readingstats.json+dist/, both as CLIs. I also built once withBundleAnalyzerPluginin static mode. - Timing: seven interleaved CLI wall clocks per variant via
process.hrtimearound the full command; five interleaved webpack builds per build variant. - Findings: source-map-explorer JSON median 224.3 ms, HTML 243.8 ms. webpack-bundle-analyzer JSON median 312.6 ms, static HTML 327.3 ms. Build medians: plain 3.49 s,
--jsonstats 3.13 s, with plugin 3.34 s (all inside run-to-run noise on this box). - One surprise: source-map-explorer 2.5.3 exited 1 on its default run ("generated column Infinity on line 2"). It only worked with
--no-border-checks. - Not tested: Vite or Rollup output, multi-chunk apps with dynamic imports,
devtoolvariants other thansource-map, the analyzer's server mode, Windows/macOS.
Why do the two tools report different sizes?
| Module group | source-map-explorer bytes | Analyzer parsed bytes | Gap |
|---|---|---|---|
| lodash | 70,312 | 70,136 | about equal |
| date-fns (37 files) | 19,196 | 44,313 | analyzer 2.3x higher |
| my src (121 files) | 58,258 | 33,840 | analyzer 42% lower |
| whole bundle | 148,569 | 148,569 | same total |
Lodash is a single CommonJS module, so webpack kept it as its own module and both tools saw the same bytes. Date-fns and my source files were scope-hoisted into one module the analyzer labels "index.js + 157 modules (concatenated)". Its JSON report marks all 158 inner entries with inaccurateSizes: true. The numbers fit a proportional split: date-fns is 102,305 of the module's 180,547 source bytes, and 57% of the 78,237 minified bytes is about 44,330, within 20 bytes of what the analyzer showed. Date-fns source is verbose (JSDoc, long names), so it got credit for bytes that minification had already removed. source-map-explorer walks the source map byte by byte, so it does not need to guess.
For the bundler side of this, see the Rspack vs webpack post and the webpack 5.111 ESM output post.
Can you make webpack-bundle-analyzer accurate?
Yes, at a cost. I rebuilt with optimization.concatenateModules: false. The analyzer then reported date-fns at 22,174 bytes and flagged zero inaccurate entries, close to source-map-explorer's 19,966 bytes for the same build. But the bundle grew from 148,569 to 163,525 bytes (+10%), because scope hoisting is a real size win. Do this in a throwaway "analyze" build, not in the build you ship.
source-map-explorer also has a --gzip flag. With it (plus --no-border-checks), it reported a 31,346-byte gzip total, the same total the analyzer showed, and put date-fns at 9,008 gzip bytes.
Which bundle analyzer should you use?
| Situation | My pick |
|---|---|
| "Which npm package is making my bundle big?" in a production webpack build | source-map-explorer |
| You need chunk-level treemaps, gzip sizes per chunk, or a CI HTML artifact from the webpack build | webpack-bundle-analyzer (plugin or CLI) |
| Your bundler is not webpack (Vite, Rollup, esbuild) and emits source maps | source-map-explorer (analyzer needs webpack stats) |
| You want exact per-module numbers from the analyzer | Separate analyze build with concatenateModules: false |
| No source maps in production and you will not turn them on | webpack-bundle-analyzer |
Bottom line: on one webpack 5 build, the two tools agreed on the total and on lodash, but date-fns was 44 KB in one and 19 KB in the other. Who should not bother with source-map-explorer: teams whose big wins are whole chunks or duplicate vendor copies, which the analyzer shows well. To shrink what these tools find, the terser vs esbuild/swc minify post and the knip vs depcheck post cover minifiers and unused dependencies; for a CI byte budget, see size-limit vs bundlesize.
How this was made: I generated the fixture, built it with webpack, ran both analyzers on the ShopperCove test box, timed interleaved runs and kept the JSON; the write-up was drafted with AI help and checked against that output.
Sources
- https://github.com/danvk/source-map-explorer
- https://www.npmjs.com/package/source-map-explorer
- https://github.com/webpack/webpack-bundle-analyzer
- https://www.npmjs.com/package/webpack-bundle-analyzer
- https://webpack.js.org/configuration/optimization/#optimizationconcatenatemodules
- https://webpack.js.org/plugins/module-concatenation-plugin/
Related
- https://www.shoppercove.com/blog/rspack-vs-webpack
- https://www.shoppercove.com/blog/webpack-5-111-esm-output-stable-september-2026
- https://www.shoppercove.com/blog/terser-vs-esbuild-swc-minify
- https://www.shoppercove.com/blog/size-limit-vs-bundlesize
- https://www.shoppercove.com/blog/knip-vs-depcheck
- https://www.shoppercove.com/blog/rolldown-vs-rollup
- https://www.shoppercove.com/blog/madge-vs-dependency-cruiser
- https://www.shoppercove.com/blog/vite-8-3-2-renderbuilturl-bundled-dev-sourcemaps-october-2026
Lab evidence
What I found running this
Hands-on on ShopperCove box 5 Oct 2026 ~23:36-23:42 IST (8 vCPU Intel Xeon / 15 GB shared Linux, Node 20.19.2). Fixture: 1 entry + 120 feature modules importing date-fns 4.4.0 format/addDays + lodash 4.18.1 default import; webpack 5.111.1, webpack-cli 7.2.3, production, devtool source-map. main.js 148569 B, map 930179 B, stats.json 3775203 B. Tools: source-map-explorer 2.5.3 (default run exit 1 'generated column Infinity'; needs --no-border-checks), webpack-bundle-analyzer 5.4.0 CLI + plugin. 1 warmup, 7 interleaved CLI runs: sme json 224.3 ms, sme html 243.8 ms, wba json 312.6 ms, wba static 327.3 ms. 5 builds: plain 3490 ms, --json 3129 ms, plugin 3343 ms (noise). Attribution concat on: sme lodash 70312 / date-fns 19196 / src 58258; wba parsed lodash 70136 / date-fns 44313 / src 33840 (158 leaves inaccurateSizes:true, proportional to stat size). concatenateModules:false: bundle 163525 B (+10%), wba date-fns 22174, sme 19966, 0 inaccurate. sme --gzip total 31346 = wba gzip total. Raw: /workspace/bench-sme-wba/res-sme-wba.json. Not tested: Vite/Rollup output, multi-chunk, other devtools, server mode, Windows/macOS. No affiliate.
Related links
Plate 66
Jotai vs Zustand: 0.15 ms vs 0.17 ms per Update
5 Oct 2026
Plate 38
TanStack Query vs SWR: 10.4 KB vs 5.6 KB Gzip
5 Oct 2026
Plate 60
Zustand vs Redux Toolkit: 0.14 ms vs 0.68 ms per Update
zustand 5.0.15 vs @reduxjs/toolkit 2.13.0 + react-redux 9.3.0 on React 19.3.0 (jsdom, NODE_ENV=production). 1,000 selector-subscribed rows, 300 flushSync updates x 7 trials, 2 runs: per-update median 0.137/0.139 ms vs 0.676/0.694 ms (~5x); 1 row re-rendered per update in both. Store-only: 1,376,191 vs 6,502 updates/s; Immer autoFreeze off lifts RTK to 386,966. Gzip (React external): 387 B vs 9,708 B. Fixture /workspace/bench-zustand-rtk/.
5 Oct 2026