Skip to content

Source Maps in Development and Production

intermediate16 min read

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

Mental Model

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:

  1. Column in the generated file (relative to previous segment)
  2. Index into the sources array (relative)
  3. Line in the original source (relative)
  4. Column in the original source (relative)
  5. Index into the names array (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
Quiz
Why do source maps use relative (delta) encoding instead of absolute line/column numbers?

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 OptionBuild SpeedRebuild SpeedQualityBest For
evalFastestFastestGenerated code only (no mapping to original)Maximum dev speed when you rarely use DevTools
eval-source-mapSlowFastOriginal source (line + column accurate)Development — best quality with fast rebuilds
cheap-module-source-mapMediumMediumOriginal source (line accurate, no column)Development — good compromise of speed and quality
source-mapSlowestSlowestOriginal source (line + column accurate)Production — full quality, separate .map file
hidden-source-mapSlowestSlowestSame as source-map but no reference in the bundleProduction — maps uploaded to error tracker, not exposed to users
nosources-source-mapSlowestSlowestOriginal positions but no source contentProduction — 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,
};
Quiz
Your production app uses devtool: 'source-map'. This generates a separate .map file alongside each bundle. Who can access these source maps?

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 .map URL
  • 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 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
Quiz
Your CI/CD pipeline generates source maps with hidden-source-map, uploads them to Sentry, then deletes the .map files before deploying. What do users see vs. what your team sees?

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:

  1. Open the Sources panel
  2. Right-click the minified file
  3. Select "Add source map..."
  4. Enter the URL or local path to the .map file

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:

  1. Check the bundle for the //# sourceMappingURL= comment at the end
  2. Verify the .map file exists at the referenced URL
  3. Check for CORS issues — the .map file must be accessible from the page's origin
  4. Check the sources paths 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 cheap devtool optionscheap-* 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-map to exclude source content (positions only)
  • Use cheap-module-source-map in development for faster builds
  • Only generate source maps for the files you need (exclude vendor chunks)
What developers doWhat 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
Key Rules
  1. 1Source maps map positions in generated (minified/bundled) code back to positions in your original source files using VLQ-encoded relative offsets.
  2. 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).
  3. 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.
  4. 4The //# sourceMappingURL comment at the end of a bundle tells the browser where to find the source map. hidden-source-map omits this comment.
  5. 5Source maps are not just for JavaScript — CSS preprocessors, CSS-in-JS tools, and other languages use the same format.
  6. 6If source maps show wrong positions, check for uncomposed multi-step transforms or cheap devtool options that only map to lines.