How to Find Bundle Bloat with source-map-explorer

This guide adds a bundler-agnostic tool to Webpack Bundle Analysis Techniques, part of JavaScript Bundle Optimization & Code Splitting. Bundler plugins such as webpack-bundle-analyzer and rollup-plugin-visualizer report what the bundler thinks each module contributes, often before minification. source-map-explorer takes a different approach: it reads the final minified file and its source map, and attributes every byte of output to the original source file that produced it.

That has two advantages. It measures exactly what ships — after minification, dead-code elimination and any post-processing — and it works with any toolchain that emits source maps: webpack, Rollup, Vite, esbuild, Parcel, Turbopack, or a CDN-hosted third-party bundle whose map you can download. When numbers from a bundler plugin and the actual file size disagree, source-map-explorer is the tie-breaker.

How source-map-explorer attributes bytes Steps from a minified bundle and its source map to a per-file breakdown of output bytes. How source-map-explorer attributes bytes Minified JS final shipped file Source map output → source mappings Attribute bytes each column to a file Aggregate by file and package Treemap HTML or JSON

Rapid Diagnosis

  • Ensure source maps are generated for production builds, even if not deployed publicly (hidden-source-map in webpack, sourcemap: 'hidden' in Vite).
  • Run the tool on the largest initial chunk first. That is where bytes hurt most.
  • Look for [unmapped] bytes. Large unmapped areas mean injected code (runtime, helpers) or broken source maps.
  • Look for the same package under multiple paths — node_modules/a/node_modules/lodash and node_modules/lodash — indicating duplication.

Root Cause Analysis: What It Commonly Finds

1. Packages that survived tree shaking. A library expected to be partially removed appears in full because of side effects or CommonJS format.

2. Duplicated dependencies. Different versions of the same package installed for different dependents, both bundled.

3. Generated code. Large chunks attributed to generated files — GraphQL documents, i18n catalogues, route manifests — that grew silently.

4. Bundler runtime and helpers. Unmapped or runtime-attributed bytes that grow with chunk count or with module-interop overhead.

Top contributors in a 410KB main chunk (minified) Bar chart of the largest source contributors to a minified main chunk as attributed by source-map-explorer. Top contributors in a 410KB main chunk (minified) react-dom 128KB lodash (two copies) 71KB generated/graphql.ts 54KB app src/ 96KB unmapped 18KB

Step-by-Step Workflow

1. Generate production source maps

javascript
// webpack.config.js
module.exports = { mode: 'production', devtool: 'hidden-source-map' };
// vite.config.js
export default { build: { sourcemap: 'hidden' } };
// trade-off: source maps add build time and disk space. Generate them in CI and
// upload to your error tracker; do not deploy them publicly unless you intend
// your source to be readable.

Expected outcome: a .map file next to every emitted chunk.

2. Run the analysis

bash
npx source-map-explorer 'dist/assets/index-*.js' --html report.html
npx source-map-explorer 'dist/assets/*.js' --json > sme.json     # all chunks, machine-readable
# trade-off: analysing all chunks together merges lazy and initial code into
# one picture. Analyse the initial chunks separately for critical-path decisions.

Expected outcome: an interactive treemap and a JSON breakdown per file.

3. Investigate the top items

For each large contributor, decide: is it needed on this route? Is it larger than it should be (tree shaking failed)? Is it duplicated? Trace the import chain with your bundler's analysis tool or a why-style command (npm ls lodash, pnpm why lodash).

bash
npm ls lodash
# my-app@1.0.0
# ├── lodash@4.17.21
# └─┬ legacy-widget@2.3.0
#   └── lodash@3.10.1
# trade-off: deduplicating across major versions is not always possible; if
# the old version is required, consider replacing or forking the dependent.

Expected outcome: a short list of concrete fixes, each with an expected byte saving.

4. Compare before and after

Save the JSON output for the base build and compare per-file totals after changes, or across releases, to confirm savings and catch regressions.

source-map-explorer vs bundler plugins Comparison of source-map-explorer with bundler-integrated analysers by what they measure and when to use each. source-map-explorer vs bundler plugins Aspect source-map-explorer Bundler plugin Measures final minified bytes bundler's module sizes Works with any bundler yes, needs maps bundler-specific Explains why a module is included no yes, import graph Third-party prebuilt bundles if map available no

Verification

Check that the tool's total for a chunk matches the file's size (minus a small unmapped remainder); a large mismatch means the source map is incomplete. After fixing an issue, re-run and confirm the item shrank or disappeared, and that the chunk's total file size fell by a corresponding amount.

Worked Example: Two Copies of lodash

A React app's main chunk was 410KB minified. The bundler plugin showed lodash at 24KB, which seemed fine. source-map-explorer showed lodash under two paths totalling 71KB: the app's own lodash (full build, imported via import _ from 'lodash') and lodash@3 nested under an old widget dependency. The team switched the app's imports to lodash-es per-function imports, replaced the old widget, and the main chunk dropped by 64KB. The plugin had under-reported because it measured pre-minification module sizes and grouped the nested copy separately.

Common Mistakes

  • Analysing development builds. Unminified, un-shaken code produces a misleading picture.
  • Ignoring [unmapped]. A large unmapped share means the analysis is incomplete; fix source map generation first.
  • Deploying public source maps by accident. Use hidden maps and keep them out of the public deploy.
  • Comparing gzip to raw. Be consistent about whether you discuss minified or compressed sizes; the tool reports minified bytes by default (--gzip changes that).

Edge Cases That Distort the Picture

Concatenated modules. Scope hoisting (webpack's ModuleConcatenationPlugin, Rollup's default) merges modules into one function scope; source maps still attribute bytes correctly, but small helper modules may appear to vanish into their importers.

Inlined constants and minifier transforms. Minifiers inline constants and collapse code, so bytes attributed to a file can be smaller than its source suggests — or attributed to the call site rather than the definition. Interpret per-file numbers as approximate for very small files.

Multiple maps per chunk. Some pipelines run a second transform (a post-build minifier or obfuscator) that produces a new map; if the maps are not chained, attribution points at the intermediate file rather than the original source. Generate maps from the last step or chain them.

Inline data URLs. Fonts or images inlined into JavaScript appear as one large attribution to the importing module, often a CSS-in-JS file. Large inline assets are almost always better served as separate files.

FAQ

Why are source-map-explorer and webpack-bundle-analyzer numbers different?

webpack-bundle-analyzer's "parsed" size approximates minified size per module, but its attribution happens at the module level inside webpack, while source-map-explorer attributes the final bytes column by column. Concatenation, inlining and minifier transforms make the two diverge slightly; for "what ships", trust source-map-explorer.

Can I analyse a third-party script?

If the vendor publishes a source map, download both files and run the tool locally. That can reveal, for example, that a 90KB widget bundles its own copy of a framework — useful evidence when negotiating with a vendor or choosing an alternative.

Does it work with CSS?

It is designed for JavaScript. For CSS, use your bundler's CSS output with source maps and coverage data from DevTools' Coverage panel to find unused rules.

How often should I run this analysis?

On every significant dependency change and before major releases, plus whenever a size budget fails without an obvious cause. Automating a JSON run in CI and storing results lets you compare any two builds later without regenerating them.

Can source-map-explorer show gzip sizes?

Yes, with the --gzip flag it estimates compressed contributions per file. Use minified sizes for parse-cost questions and gzip or brotli estimates for transfer questions, and say which one you are quoting.

What should I do with the findings first?

Rank items by bytes on the critical path, not by total bytes. A 40KB library in the landing route's initial chunk matters more than a 120KB library in a lazy admin chunk. Fix duplicates first (pure savings, no behaviour change), then failed tree shaking, then heavy dependencies that can move behind dynamic imports, and finally candidates for replacement with lighter alternatives.