Frontend / Build tools / 05_source_maps.md

Source Maps — Modes and Trade-offs

Updated 5 interview angles 6 min read source
On this page14
  1. How does a source map work?
  2. Webpack devtool options — name the ones you’d use.
  3. Vite build.sourcemap options.
  4. Hidden source maps — why?
  5. Dev vs prod — recommended settings?
  6. How big are source maps?
  7. Can source maps slow down production?
  8. How does Sentry / Datadog use them?
  9. Source maps + transformers (Babel/SWC/esbuild) — do they compose?
  10. Source maps in monorepos.
  11. Gotchas
  12. Related
  13. Further reading
  14. Interview angle

Source Maps — Modes and Trade-offs

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.

How does a source map work?

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.

text
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.

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

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

devtool Speed, fidelity When
eval fastest; low, eval-string positions dev only
eval-cheap-source-map very fast; lines 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
nosources-source-map slow; positions only, no source content prod with stronger source protection
inline-source-map slow; full, data-URL embedded dev without 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).

Vite build.sourcemap options.

Simpler:

ts
// vite.config.ts
export default defineConfig({
  build: {
    // separate .map file with sourceMappingURL comment
    sourcemap: true,
    // 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).

Hidden source maps — why?

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).

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).

How big are source maps?

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.

Can source maps slow down production?

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.

How does Sentry / Datadog use them?

  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.

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

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.

Source maps in monorepos.

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

  • 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.

Further reading

Interview angle 5

  • “How do you configure source maps for dev versus production?” - in development, the fastest variant that still gives usable line and column info. In production, hidden-source-map: the map is generated and uploaded to the error tracker but no sourceMappingURL comment points at it publicly.
  • “What must you not ship?” - inline-source-map in production, which embeds your entire source inside the bundle. And a public sourceMappingURL unless you are comfortable with anyone reading your source in DevTools, which is a business decision rather than a technical one.
  • “Do source maps slow the page down?” - no. The browser only fetches the map when DevTools is open, so there is no runtime cost to a user. The real cost is build time, which is why dev and production use different variants.
  • “What is the production justification?” - symbolication. Without uploaded maps, every production stack trace points into minified code and is effectively unreadable, so the error tracker tells you something broke and nothing about where.
  • “Your stack traces point at the wrong lines. Why?” - the map and the bundle came from different builds. Upload the maps from the same CI run that produced the bundle, keyed by release, or the symbolication is confidently wrong rather than absent.