Source Maps in Development and Production
The Problem Source Maps Solve
Your production JavaScript looks like this:
function a(b){if(!b)throw new Error("Missing");return b.split(",").map(c=>c.trim())}
A user reports a crash: "Error at a.js:1:42". Which line of your source code is that? Without source maps, you're reading minified code and guessing. With source maps, Chrome DevTools shows you the original TypeScript, the exact line, the variable names you wrote — as if the minifier never ran.
Source maps are the bridge between what the browser executes (minified, bundled, transpiled) and what you wrote (TypeScript, JSX, readable code).
Think of a source map as a translation key between two versions of a book. The original book (your source code) is well-formatted with chapter titles and paragraph breaks. The published version (minified bundle) is compressed into a single wall of text to save paper. The translation key (source map) says: "character 847 in the published version corresponds to line 23, column 5 of Chapter 3 in the original." This lets you read the published version but reference the original when you need context.
How Source Maps Work
A source map is a JSON file that maps positions in the generated (output) file back to positions in the original source files.
{
"version": 3,
"file": "main.min.js",
"sources": ["src/utils.ts", "src/app.ts"],
"sourcesContent": ["const greet = ...", "import { greet } ..."],
"names": ["greet", "name", "message"],
"mappings": "AAAA,SAASA,EAAMC,..."
}
Key fields:
- sources — list of original source files
- sourcesContent — the original source code (optional, but enables DevTools to show source without fetching the files)
- names — original variable/function names before minification
- mappings — the actual position mappings, encoded in Base64 VLQ
VLQ Encoding: The Clever Compression
The mappings field is the heart of a source map. Each segment maps a position in the output to a position in the source. A naive approach (storing line/column pairs for every character) would make source maps enormous.
Instead, source maps use Variable-Length Quantity (VLQ) encoding with Base64. Each mapping segment encodes 4-5 values as relative offsets:
- Column in the generated file (relative to previous segment)
- Index into the
sourcesarray (relative) - Line in the original source (relative)
- Column in the original source (relative)
- Index into the
namesarray (relative, optional)
Using relative values (deltas) instead of absolute positions keeps the numbers small. VLQ then encodes small numbers in fewer characters. The result: a source map for a 100KB bundle might be 200-300KB — large, but far smaller than the alternative.
Mappings string: "AAAA,SAASA,EAAMC"
Decoded:
A A A A → gen col +0, source #0, src line +0, src col +0
S A A S A → gen col +9, source #0, src line +0, src col +9, name #0
E A A M C → gen col +2, source #0, src line +0, src col +6, name #1
webpack devtool Options
webpack's devtool option controls how source maps are generated. The options are a matrix of quality vs. build speed:
module.exports = {
devtool: 'source-map', // pick one
};
| devtool Option | Build Speed | Rebuild Speed | Quality | Best For |
|---|---|---|---|---|
| eval | Fastest | Fastest | Generated code only (no mapping to original) | Maximum dev speed when you rarely use DevTools |
| eval-source-map | Slow | Fast | Original source (line + column accurate) | Development — best quality with fast rebuilds |
| cheap-module-source-map | Medium | Medium | Original source (line accurate, no column) | Development — good compromise of speed and quality |
| source-map | Slowest | Slowest | Original source (line + column accurate) | Production — full quality, separate .map file |
| hidden-source-map | Slowest | Slowest | Same as source-map but no reference in the bundle | Production — maps uploaded to error tracker, not exposed to users |
| nosources-source-map | Slowest | Slowest | Original positions but no source content | Production — error stack traces without exposing source code |
Development Recommendation
// Development — fast rebuilds, full source quality
module.exports = {
devtool: 'eval-source-map',
};
eval-source-map wraps each module in eval() with an inline source map. Rebuilds are fast because webpack only re-evaluates the changed module. Quality is excellent — you see your original TypeScript/JSX in DevTools.
Production Options
For production, the decision is more nuanced:
// Option 1: Full source maps (separate .map file)
module.exports = {
devtool: 'source-map',
};
// Option 2: Hidden source maps (for error tracking services)
module.exports = {
devtool: 'hidden-source-map',
};
// Option 3: No source maps
module.exports = {
devtool: false,
};
Vite Source Maps
Vite handles source maps differently in dev vs. build:
Development: Source maps are always enabled (you're serving the original source files via ESM, so mapping is trivial).
Production: Controlled via the build.sourcemap option:
// vite.config.ts
export default defineConfig({
build: {
sourcemap: true, // generates .map files (like webpack 'source-map')
sourcemap: 'hidden', // generates .map files without URL comment
sourcemap: 'inline', // embeds map in the bundle (not recommended for production)
sourcemap: false, // no source maps
},
});
Production Source Maps: The Tradeoff
Using source maps in production involves a real tension:
Pros
- Error tracking — services like Sentry, DataDog, and Bugsnag use source maps to show original file names, line numbers, and code context in error reports. Without source maps, you get "Error at a.js:1:42" — useless.
- Production debugging — when a user reports an issue, you can reproduce it in DevTools and see the original source.
Cons
- Code exposure — source maps reveal your original source code, file structure, variable names, and comments to anyone who finds the
.mapURL - Increased deploy size — source maps are typically 2-3x the size of the code they map
- Build time — generating high-quality source maps adds to build time
The Recommended Strategy
The industry best practice: generate hidden source maps and upload them to your error tracking service.
// webpack
module.exports = {
devtool: 'hidden-source-map',
};
Then upload the .map files to Sentry (or similar) during your CI/CD pipeline:
# Upload source maps to Sentry
npx @sentry/cli sourcemaps upload \
--release="1.0.0" \
--url-prefix="~/" \
./dist
After uploading, delete the .map files from the deploy. They never reach your CDN. Sentry has them for error resolution, but users can't access them.
# Remove .map files from deploy directory
rm ./dist/**/*.map
Debugging with Source Maps in Chrome DevTools
When source maps are available, Chrome DevTools transforms your debugging experience:
Sources Panel
With source maps loaded, the Sources panel shows your original file tree instead of bundled chunks. You see src/components/Button.tsx instead of chunk-abc123.js.
You can:
- Set breakpoints in your original TypeScript/JSX
- Step through code line by line in the original source
- Inspect variables with their original names
- See the call stack with original function names
Console Error Traces
Without source maps:
Error: Cannot read property 'name' of undefined
at a (main.abc123.js:1:4523)
at b (main.abc123.js:1:4891)
With source maps:
Error: Cannot read property 'name' of undefined
at getUserName (src/utils/user.ts:23:15)
at renderProfile (src/components/Profile.tsx:45:22)
Manually Loading Source Maps
If you have hidden source maps (not linked in the bundle), you can manually add them in DevTools:
- Open the Sources panel
- Right-click the minified file
- Select "Add source map..."
- Enter the URL or local path to the
.mapfile
This is useful for debugging production issues locally with source maps that aren't publicly deployed.
Source maps for CSS and other languages
Source maps aren't just for JavaScript. CSS preprocessors (Sass, Less, PostCSS) generate source maps that let you see the original .scss or .less file in DevTools instead of the compiled CSS. The same format and same DevTools support applies.
Even CSS-in-JS solutions that generate stylesheets at build time (like vanilla-extract or Linaria) can produce source maps that link generated CSS rules back to the JavaScript file where they were defined.
Common Source Map Issues
Source Maps Not Loading
If DevTools isn't showing your original source:
- Check the bundle for the
//# sourceMappingURL=comment at the end - Verify the
.mapfile exists at the referenced URL - Check for CORS issues — the
.mapfile must be accessible from the page's origin - Check the
sourcespaths in the map — relative paths are resolved from the map's URL
Wrong Line Numbers
Sometimes source maps point to slightly wrong positions:
- After CSS-in-JS transforms — template literal transforms can shift positions
- After multiple transformation steps — each step (TypeScript, JSX, minification) generates its own map. If they're not properly composed, accuracy suffers
- With
cheapdevtool options —cheap-*variants only map to lines, not columns. You get the right line but not the exact character position
Large Source Map Files
If source maps are slowing your build or bloating your deploy:
- Use
nosources-source-mapto exclude source content (positions only) - Use
cheap-module-source-mapin development for faster builds - Only generate source maps for the files you need (exclude vendor chunks)
| What developers do | What they should do |
|---|---|
| Deploying .map files to your public CDN Public source maps expose your entire codebase — file structure, business logic, comments, internal API patterns. Competitors and attackers can read your source code. | Use hidden-source-map, upload to error tracker, delete .map files before deploy |
| Using eval devtool in production eval wraps each module in eval() which is slower, triggers CSP violations, and the inline source maps bloat the bundle. It's designed for fast development rebuilds, not production. | Use source-map or hidden-source-map for production |
| Disabling source maps entirely because they're 'too complex' Without source maps, every production error becomes a mystery. 'Error at a.js:1:4523' tells you nothing. Source maps in your error tracker give you original file names, line numbers, and code context — the difference between minutes and hours of debugging. | At minimum, generate hidden source maps and upload to your error tracking service |
| Using inline source maps (devtool: 'inline-source-map') in production Inline source maps embed the entire map in the bundle as a base64 data URL. This roughly doubles or triples your bundle size since users download the source map data even if they never open DevTools. | Use separate .map files for production |
- 1Source maps map positions in generated (minified/bundled) code back to positions in your original source files using VLQ-encoded relative offsets.
- 2Use eval-source-map for development (fast rebuilds, full quality). Use hidden-source-map for production (upload to error tracker, don't expose to users).
- 3Always upload production source maps to your error tracking service (Sentry, DataDog). Delete .map files from the deploy. This gives you full debugging without code exposure.
- 4The //# sourceMappingURL comment at the end of a bundle tells the browser where to find the source map. hidden-source-map omits this comment.
- 5Source maps are not just for JavaScript — CSS preprocessors, CSS-in-JS tools, and other languages use the same format.
- 6If source maps show wrong positions, check for uncomposed multi-step transforms or cheap devtool options that only map to lines.