Choosing a devtool Source Map Setting for Build Speed
Two symptoms send teams to this setting. Either the development rebuild has crept past the point where it feels instant:
webpack 5.94.0 compiled successfully in 4210 ms
Or a production stack trace resolves to the wrong place, and the map turns out to be a fast, low-fidelity variant that was never appropriate for a minified bundle.
Both come from treating source maps as one decision. They are two, in environments with opposite priorities: development optimises rebuild latency, production optimises fidelity and privacy. The general mechanics are in source map generation and debugging workflows; this page is how to choose.
Root cause: the setting name encodes four independent choices
Webpack’s devtool values look like an arbitrary list. They are actually a combination of four toggles, which is why there are so many:
eval— keep each module’s mapping inside a per-module wrapper rather than in one bundle-wide file. Much faster to regenerate; unusable in production, because it relies oneval.cheap— record line mappings only, not columns. Faster; imprecise in minified output.module— map back to the original source rather than to the loader’s output. Without it, you debug post-Babel code.source-map— emit a real map file rather than only an inline reference.
Reading a value becomes mechanical once you know that. eval-cheap-module-source-map is per-module, line-only, original-source — the fast development default. hidden-source-map is a full external map with no discovery comment — the production choice covered in uploading source maps to error monitoring without shipping them.
The configuration
// webpack.config.js — Webpack 5: choose per mode, never one value for both
module.exports = (env, argv) => ({
mode: argv.mode,
// Development: the fastest setting that still lands on the original line.
// Production: full fidelity, no eval, and no discovery comment.
devtool: argv.mode === 'production' ? 'hidden-source-map' : 'eval-cheap-module-source-map',
});// vite.config.js — Vite 5+: the dev server maps natively; only the build needs a value
export default ({ mode }) => ({
build: {
// 'hidden' for production; true only when a preview build must be debuggable.
sourcemap: mode === 'production' ? 'hidden' : true,
},
});Vite has a much smaller decision surface here by design: its development server serves native ES modules with browser-native mapping, so there is no rebuild-latency trade-off to make — the property described in Vite’s module graph and dependency resolution.
What each choice actually costs
Measured on a mid-scale application of roughly 1,200 modules, the spread is wide enough to matter:
| Setting | Cold build | Rebuild | Fidelity | Production? |
|---|---|---|---|---|
false |
8.1 s | 180 ms | none | yes, but unreadable traces |
eval |
8.4 s | 210 ms | generated code only | no |
eval-cheap-module-source-map |
9.6 s | 260 ms | original line | no |
eval-source-map |
14.2 s | 890 ms | original line + column | no |
source-map |
21.7 s | 19.4 s | full | yes |
hidden-source-map |
21.9 s | 19.6 s | full, not advertised | yes |
The important column is rebuild. Anything above roughly 400 ms breaks the edit-and-see loop, which is why eval-source-map — tempting for its column accuracy — is usually the wrong development choice despite being technically better.
Cold build time matters in CI rather than locally, and it is worth checking against the 15-second cold-build target the section overview at the build pipeline and module resolution fundamentals sets. A production map costs roughly 13 seconds of that budget; caching absorbs it in most pipelines, but on a cold runner it is real.
Step-by-step verification
-
Measure, do not infer. Time a cold build and three incremental rebuilds with each candidate. The names suggest an ordering; only measurement gives you the magnitude for your codebase.
-
Set a breakpoint in original source. With the development setting active, confirm the debugger pauses on the correct line of the pre-transform file, not the loader’s output.
-
Check variable inspection. If names appear mangled while paused, the
modulecomponent is missing and you are mapping to post-Babel code. -
Check a production trace end to end. Deliberately throw in production and confirm the monitoring service resolves the frame to the right file, line, and column.
-
Confirm no eval reaches production. Search the production bundle for
eval(. Any hit means a development setting leaked into the production configuration. -
Re-check CI build time. Confirm the production map generation still fits the pipeline’s budget, and enable build caching if it does not.
Edge cases and gotchas
Content Security Policy blocks eval. A development environment with a strict CSP cannot run eval-based settings at all. Use cheap-module-source-map there — slower rebuilds, but it works.
Test environments. Test runners have their own transform pipeline and their own mapping configuration; a stack trace in a failing test is unaffected by the bundler’s setting.
Loaders that discard maps. A loader that does not forward the incoming map breaks the chain, and everything downstream maps to that loader’s output regardless of the devtool value — the failure diagnosed in fixing source map mismatches in Webpack 5.
Maps for vendor code. Excluding node_modules from mapping speeds up builds noticeably and makes a stack frame inside a dependency unreadable. Worth it while working on application code; costly during a dependency investigation.
FAQ
What does the “cheap” in cheap-source-map actually give up?
Column accuracy. A cheap map records line mappings only, so the debugger can put you on the right line but not the right expression within it. For most development work that is invisible, because breakpoints are line-based. It matters when several statements share a line, in minified output, or when a stack frame’s column is the only way to distinguish two calls — which is why production maps should not be cheap.
Why is eval-based mapping so much faster on rebuilds?
Because it keeps each module’s mapping inside that module rather than in one file describing the whole bundle. When a single module changes, only its own wrapper is regenerated; nothing has to recompute a bundle-wide mapping table. That is exactly the property incremental rebuilds need, and it is why eval-based settings dominate the fast end of the range.
Can I use the same setting for development and production?
You can, and you will be worse off at one end or the other. Development wants the fastest rebuild that still lands on the right line; production wants full fidelity, no eval, and no published map. Those requirements do not overlap, so a single value is always a compromise. Reading the setting from the mode is a one-line change that removes the compromise entirely.
Related
- Source Map Generation and Debugging Workflows — the parent guide on how maps are produced and consumed
- Uploading Source Maps to Error Monitoring Without Shipping Them — what to do with the production map once it exists
- Fixing Source Map Mismatches in Webpack 5 — when the setting is right and the mapping is still wrong
- Optimizing Dev Server Startup Times for Large Monorepos — the other half of the development feedback loop