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
.mapfile in your build output. - Doesn’t include the
//# sourceMappingURL=comment in the JS. - You upload the
.mapto 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).
Q: Dev vs prod — recommended settings?
A:
Dev:
- Webpack:
eval-source-map(rich) oreval-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:
- Build produces
app.[hash].js+app.[hash].js.map. - CI uploads the map to Sentry (
sentry-cli releases files upload-sourcemaps). - User encounters an error in prod; the minified stack trace gets sent to Sentry.
- Sentry matches the bundle hash, fetches the uploaded map, symbolicates the stack — you see “at
handleSubmitinLoginForm.tsx:42” instead ofat 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
sourceMappingURLcomment manually doesn’t help if.mapis still served on the same path — bots can guess${file}.map. Usehidden-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-mapper rebuild is slow on big projects → useeval-cheap-source-mapfor fast inner loop. sourcesContent: falsestrips 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-mapor Vite default). Prod:hidden-source-map+ upload to error tracker out-of-band.” - “Never ship
inline-source-mapto prod — embeds source in the bundle. Or exposesourceMappingURLpublicly 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
- Bundler choices: 01_vite_vs_webpack_vs_turbopack.md
- Backend observability for error tracking: ../../backend/15_observability/01_sentry.md
- Production debugging mindset: ../../backend/02_python_core/performance/06_production_debugging.md
Further reading
- Source Maps v3 spec: https://sourcemaps.info/spec.html
- Webpack devtool: https://webpack.js.org/configuration/devtool/
- Vite — Build options: https://vitejs.dev/config/build-options.html
- Sentry — Source Maps: https://docs.sentry.io/platforms/javascript/sourcemaps/