frontend / build tools / 05_source_maps.md

Source Maps — Modes and Trade-offs

5 min read source

Source Maps — Modes and Trade-offs

TL;DR

A source map is a sidecar file that maps positions in the bundled/transformed output back to the original source — so the debugger and error stack traces show your TS/JSX, not minified t.exports.a(b). Several modes trade off build speed, runtime cost, bundle size, and debug fidelity. Production: ship hidden source maps so monitoring tools (Sentry) can de-minify stacks without leaking source to users. Dev: fast, high-fidelity maps (eval, inline). The bundler’s devtool (Webpack) or build.sourcemap (Vite) is the lever.

Interview Q&A

Q: How does a source map work?

A: A .map file (or inline data URL) contains a base-64 VLQ encoding of position mappings: for each character in the output, which line/column in which source file it came from. The browser reads the map (referenced via //# sourceMappingURL=foo.js.map comment) and substitutes positions for everything debugger-related — breakpoints, stack traces, console.log line numbers.

output.js  → //# sourceMappingURL=output.js.map
output.js.map
  {
    "version": 3,
    "sources": ["src/app.ts", "src/util.ts"],
    "sourcesContent": ["// original TS code...", "..."],
    "mappings": "AAAA;AACA;..."
  }

sourcesContent embeds the original source so the debugger doesn’t need separate fetches.

Q: Webpack devtool options — name the ones you’d use.

A: Webpack has ~20 devtool variants combining speed/fidelity/format. The ones to know:

devtool Speed Fidelity When
eval fastest low (per-module eval-string positions) dev only
eval-cheap-source-map very fast line-only (no columns) dev, fast rebuild
eval-source-map medium full dev with rich debug
source-map slow full, separate .map file prod (often hidden-source-map)
hidden-source-map slow full, no sourceMappingURL comment prod — upload to Sentry, don’t expose URL
nosources-source-map slow maps positions but omits source content prod with stronger source protection
inline-source-map slow full, data-URL embedded dev when you don’t want a separate file
false none when you actively don’t want them

Modern Webpack default for dev: eval-source-map (or the cheap variant for big projects). For prod: source-map (often hidden-source-map).

Q: Vite build.sourcemap options.

A: Simpler:

// vite.config.ts
export default defineConfig({
  build: {
    sourcemap: true,            // separate .map file with sourceMappingURL comment
    // sourcemap: "inline",     // embed as data URL in the JS bundle
    // sourcemap: "hidden",     // generate .map but omit sourceMappingURL — for Sentry
    // sourcemap: false,        // none
  },
});

Vite dev always serves source maps for transformed files (it’s serving original sources mostly).

Q: Hidden source maps — why?

A: You want error tools (Sentry, Datadog, Bugsnag) to symbolicate production stack traces, but you don’t want users to be able to read your source.

hidden-source-map:

  • Generates the .map file in your build output.
  • Doesn’t include the //# sourceMappingURL= comment in the JS.
  • You upload the .map to the error tracker out-of-band (their CLI).
  • Users opening DevTools see minified code; Sentry sees original.

Some teams ship nosources-source-map for stronger protection — maps positions but omits source content (Sentry still de-minifies if you upload source separately).

A:

Dev:

  • Webpack: eval-source-map (rich) or eval-cheap-source-map (faster).
  • Vite: defaults are fine.

Prod:

  • Webpack: hidden-source-map + upload to error tracker via CLI in CI.
  • Vite: build.sourcemap: "hidden" + Sentry upload.

Never ship inline-source-map to prod — embeds source in the JS bundle (massive size increase, source leaked).

Q: How big are source maps?

A: Roughly same size as the original source (because sourcesContent embeds it). For a 200 KB minified bundle from 800 KB of source, the map is ~800 KB-ish.

This is why source maps usually go in a separate file — keeping users’ bundle download small. Inline is dev-only.

Q: Can source maps slow down production?

A: Loading a map costs nothing at runtime — browsers only load it when DevTools is open. Generation costs build time, not runtime.

The risk: shipping a public sourceMappingURL comment lets anyone download the map and read your source. Hidden source maps eliminate that risk.

Q: How does Sentry / Datadog use them?

A:

  1. Build produces app.[hash].js + app.[hash].js.map.
  2. CI uploads the map to Sentry (sentry-cli releases files upload-sourcemaps).
  3. User encounters an error in prod; the minified stack trace gets sent to Sentry.
  4. Sentry matches the bundle hash, fetches the uploaded map, symbolicates the stack — you see “at handleSubmit in LoginForm.tsx:42” instead of at t in main.[hash].js:1:8421.

Without source maps uploaded, prod stack traces are unreadable.

Q: Source maps + transformers (Babel/SWC/esbuild) — do they compose?

A: Yes. Each transformer can emit its own source map. The bundler chains them: SWC’s map (TS → modern JS) + minifier’s map (modern → minified) compose into one final map (TS → minified). The browser sees the original TS source, not the intermediate.

Webpack/Rollup/Vite handle this automatically when their loaders/plugins emit source maps. Misconfigured plugins that drop source maps mid-pipeline break the chain — debug points show the wrong file.

Q: Source maps in monorepos.

A: If your bundle includes code from packages/utils, the source map references packages/utils/src/x.ts. The browser tries to resolve that — it works if the dev server is serving from the monorepo root, but otherwise paths show as webpack-internal:///... or similar.

For Sentry: configure source root or upload all source files alongside maps so symbolication finds them.

Gotchas / edge cases

  • Mismatch between map version and bundle version — if you deploy a new bundle but old maps, Sentry symbolicates against the wrong source → garbage stack traces. CI should upload maps from the same build.
  • Removing sourceMappingURL comment manually doesn’t help if .map is still served on the same path — bots can guess ${file}.map. Use hidden-source-map.
  • CSS source maps — separate concept, separate file. Vite/Webpack support them; useful when debugging CSS-in-JS or PostCSS transforms.
  • Source map size in CI artifacts — they can dominate disk space. Compress, set retention policies.
  • Dev rebuild speed trade-off: full source-map per rebuild is slow on big projects → use eval-cheap-source-map for fast inner loop.
  • sourcesContent: false strips embedded source — smaller map file, but DevTools needs the original files served separately.

What a senior is expected to say

  • “Dev: fast, high-fidelity maps (eval-source-map or Vite default). Prod: hidden-source-map + upload to error tracker out-of-band.”
  • “Never ship inline-source-map to prod — embeds source in the bundle. Or expose sourceMappingURL publicly unless you’re fine with users reading your source.”
  • “Source maps don’t cost runtime — they’re only loaded when DevTools opens. Build-time cost is real; pick the fastest variant for dev that still gives useful line/column info.”
  • “Sentry/Datadog symbolication is the production justification — without uploaded maps, prod stack traces are unreadable.”
  • “Mismatched map version vs bundle version = wrong source shown. CI uploads maps from the same build.”

Cross-references

Further reading