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.
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:
// 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
.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).
Dev vs prod — recommended settings?
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).
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?
- 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.
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
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.
Related
Further reading
- Source Maps v3 spec: https://tc39.es/ecma426/
- 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/
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 nosourceMappingURLcomment points at it publicly. - “What must you not ship?” -
inline-source-mapin production, which embeds your entire source inside the bundle. And a publicsourceMappingURLunless 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.