Repository navigation
Description
Have you used AI?
Yes
Bug Description
SourceMapDevToolPlugin causes excessive memory consumption on large production builds due to its two-phase architecture accumulating all source map task data in memory simultaneously.
In Phase 1, getTaskForFile() calls asset.sourceAndMap() for every matching file and stores the full source map object and source string in a tasks[] array. All tasks must exist before Phase 2 begins because of the module name resolution step between phases (conflict detection across all source names). Phase 2 then iterates the tasks to serialize and emit .map files.
For builds with many locale-variant chunks sharing the same modules, this means thousands of tasks — each holding multi-megabyte source map objects as V8 heap data — coexist in memory. Additionally, webpack-sources CachedSource objects retain their composed map results internally, so shared module sources accumulate duplicate cached map data across chunks.
In our production monorepo, a single large project (5,275 unique modules, 52 locale-variant chunks, ~14,000 source map tasks) spikes from ~6 GB RSS at the end of minification to 25.5 GB RSS after SourceMapDevToolPlugin completes — an increase of nearly 20 GB attributable entirely to this plugin. This causes OOM failures in standard CI environments.
Phase-by-phase profiling confirms the minification phase stays well under budget. The memory spike occurs exclusively within the processAssets hook at PROCESS_ASSETS_STAGE_DEV_TOOLING.
Link to Minimal Reproduction and step to reproduce
A minimal reproduction is difficult because the issue manifests at scale — it requires thousands of source map tasks with shared modules across many chunks. Below is a detailed description of the conditions that trigger it.
Project structure:
- 5,275 unique modules bundled by
webpack5-module-minifier-plugin(RushStack) - 52 locale-variant chunks, each containing ~2,060 source references
- Each chunk produces a source map of ~9.1 MB as JSON
- Total: ~14,000 source map tasks processed by
SourceMapDevToolPlugin - Production mode with
SourceMapDevToolPluginconfigured for separate.mapfiles (filenameset,append: false,module: true,noSources: false)
Steps to reproduce:
- Build a large project with many locale variants (or any configuration producing thousands of chunks with heavily overlapping module sets)
- Enable production source maps via
SourceMapDevToolPluginwith external.mapfiles - Monitor peak RSS during the build (e.g., via
/usr/bin/time -vorprocess.memoryUsage.rss()) - Observe that RSS spikes dramatically during the
processAssetsstage, not during minification
Metrics collected (single-process, --expose-gc, GNU time):
| Scenario | Peak RSS |
|---|---|
| Production build, no source maps | 6,523 MB |
| Production build + SourceMapDevToolPlugin | 25,532 MB |
| After minification phase (before SMDTP) | 5,593 MB |
| After SMDTP processAssets completes | 21,140 MB |
The ~19 GB delta between "no source maps" and "with source maps" is entirely within SourceMapDevToolPlugin.
Expected Behavior
SourceMapDevToolPlugin should be able to produce source maps for large builds without holding all task data in memory simultaneously. The module name resolution step (conflict detection) only requires module identifiers and source names — not the full composed maps or source strings. Memory-proportional-to-one-task-at-a-time processing should be achievable.
For our build, we expect peak RSS during source map generation to stay under 14 GB — roughly double the minification baseline of ~6 GB, accounting for the inherent cost of one chunk's map composition at a time plus serialization overhead.
Experimental patches applied to SourceMapDevToolPlugin confirm this is feasible:
- Serializing maps to Buffers (1 byte/char) instead of holding V8 string objects (2 bytes/char) reduced peak RSS by ~5 GB (20%)
- Adding recursive
CachedSourcecache clearing after each chunk'ssourceAndMap()call reduced peak RSS by ~8.2 GB (32%), bringing the total from 25.5 GB down to 17.3 GB
With these combined, a target of ~14 GB for our scenario appears achievable
Actual Behavior
SourceMapDevToolPlugin accumulates all source map task data (composed maps + source strings) in memory during Phase 1 before processing any of it in Phase 2. For our build, this causes peak RSS to reach 25,532 MB — a 19 GB increase over the same build without source maps (6,523 MB).
The breakdown:
- Phase 1 collects ~14,000 tasks. Each task stores the full
sourceMapobject (~9.1 MB JSON per large chunk) and source string (~1.8 MB) as V8 heap objects. V8 strings use 2 bytes/char (UTF-16 internal encoding), so a 9.1 MB JSON string occupies ~18 MB of V8 heap. CachedSourceobjects shared across locale-variant chunks retain composed map results internally, adding further GBs of redundant cached data.- By the time Phase 2 begins, RSS has already climbed to ~16–21 GB. Phase 2 then re-serializes each map, adding another transient spike.
This reliably causes OOM failures in CI environments with 16 GB memory limits, preventing source map generation for large projects.
Environment
System:
OS: Linux 6.8 (Ubuntu 22.04 - GitHub Codespaces)
CPU: (4) x64 AMD EPYC (Zen 4)
Memory: 32 GB
Binaries:
Node: 22.x
npm: 10.x
pnpm: 9.x (via Rush)
Packages:
webpack: 5.105.4
webpack-sources: 3.3.4
neo-async: 2.6.2Is this a regression?
No
Last Working Version
No response
Additional Context
No response
Metadata
Metadata
Assignees
Labels
Type
Fields
Priority
Activity
perf(SourceMapDevToolPlugin): emit .map asset as Buffer to reduce V8 …
perf(SourceMapDevToolPlugin): release CachedSource map caches via cle…
alexander-akait commentedon May 14, 2026
Hello, can you try #20963?
BPapp-MS commentedon May 14, 2026
@alexander-akait , that gives us a ~19% memory savings so I definitely think it's worth it but if we find ways to make it slimmer still, or a setting that would configure some kind of heap limit check to kick off garbage collection, that would be ideal
perf(cache): release LazyHashedEtag's captured object after hash is m…
alexander-akait commentedon May 15, 2026
Can you try again - #20963, I am afraid we can't do more here without breaking something, the only way to reduce it more using - https://github.com/webpack/webpack-sources/blob/main/lib/index.js#L116 and https://github.com/webpack/webpack-sources/blob/main/lib/helpers/stringBufferUtils.js#L111 (just create a small own plugin)
BPapp-MS commentedon May 18, 2026
The updates to the branch still work about the same, and I do endorse them! We'll just have to keep modifying things to shrink it some more. For posterity, @alexander-akait can you be a bit more specific with what you'd recommend a plugin do at those lines you linked?
alexander-akait commentedon May 18, 2026
@BPapp-MS we have a program way to disable buffer/string caches, it is possible to create a small plugin which disable caches and so you will have less memory consummation, we don't have a special option to do it
perf(SourceMapDevToolPlugin): restore shared WeakSet in clearCache
bench: add webpack-20961 case modeling the PR #20963 scenario
perf(SourceMapDevToolPlugin): emit .map asset as Buffer to reduce V8 …
perf(SourceMapDevToolPlugin): release CachedSource map caches via cle…
perf(cache): release LazyHashedEtag's captured object after hash is m…
perf(SourceMapDevToolPlugin): emit .map asset as Buffer to reduce V8 …
perf(SourceMapDevToolPlugin): release CachedSource map caches via cle…
perf(cache): release LazyHashedEtag's captured object after hash is m…
perf(SourceMapDevToolPlugin): emit .map asset as Buffer to reduce V8 …
perf(SourceMapDevToolPlugin): release CachedSource map caches via cle…
perf(cache): release LazyHashedEtag's captured object after hash is m…
perf(SourceMapDevToolPlugin): release CachedSource map caches via cle…
perf(cache): release LazyHashedEtag's captured object after hash is m…