Command
serve
Is this a regression?
The previous version in which this bug was not present was
No response
Description
Which @angular/* package(s) are the source of the bug?
@angular/build (dev-server, Vite dependency optimizer configuration)
Is this a regression?
No. The behavior exists in the Angular 17 @angular-devkit/build-angular dev server and is unchanged after migrating to @angular/build on Angular 22.
Description
@angular/build:dev-server always lets Vite generate source maps for prebundled dependencies. Neither sourceMap: false nor sourceMap.vendor: false affects them, and the builder exposes no way to change it — even though, on the Vite version this Angular release resolves, the underlying option exists and Angular simply never sets it.
For most dependencies this is unnoticeable. For a large dependency it produces a multi-megabyte map that the browser's DevTools then downloads and parses on every session. In our case that is a 37,990,535-byte map for a single package, and opening Chrome or Edge DevTools makes the DevTools front end unresponsive for five minutes or longer.
Two details make this specific to Angular rather than a general Vite problem:
-
No source-map option reaches the dependency optimizer. The build configuration below sets "sourceMap": false, and the 38 MB dependency map is still generated and linked. The same holds for "sourceMap": { "scripts": true, "vendor": false, "hidden": true } — vendor: false is the option whose plain meaning is "no source maps for third-party code", and a prebundled dependency is third-party code by definition.
-
The dev-server URL for optimized dependencies defeats the browser's default ignore list. Chrome and Edge ship the default DevTools ignore rule /node_modules/|^node:. A plain Vite project serves optimized dependencies from /node_modules/.vite/deps/<dep>.js, which matches, so DevTools never processes those maps. Angular serves them from /.angular/cache/<version>/<project>/vite/deps/<dep>.js, which does not match, so DevTools fetches and parses them. The same map is harmless under Vite's default layout and harmful under Angular's.
Where the option is dropped
On Vite 7 — the version @angular/build@22.0.9 resolves — the dependency optimizer hardcodes sourcemap: true and then spreads the user's optimizeDeps.esbuildOptions over it, so a supplied sourcemap: false does take effect (packages/vite/src/node/optimizer/index.ts, prepareEsbuildOptimizerRun). The option is available; it is simply never set.
Angular constructs that object in packages/angular/build/src/tools/vite/utils.ts, in getDepOptimizationConfig, and never sets sourcemap:
esbuildOptions: {
target,
supported: getFeatureSupport(zoneless),
plugins,
loader,
define: { ...define, 'ngServerMode': `${ssr}` },
resolveExtensions: ['.mjs', '.js', '.cjs'],
},
Note that thirdPartySourcemaps — the value derived from the build's sourceMap.vendor option — is already passed into getDepOptimizationConfig and is already in scope a few lines above this object. Its only current use is to vary the plugin's name for cache differentiation:
name: `angular-vite-optimize-deps${ssr ? '-ssr' : ''}${thirdPartySourcemaps ? '-vendor-sourcemap' : ''}`,
There is no user-side escape hatch either
Since @angular/build:dev-server accepts no Vite configuration, and its (experimental) extension parameter takes only esbuild build plugins, middleware and an index.html transformer, there is no supported path for a user to set this option.
A vite.config.js in the project root does not help. packages/angular/build/src/builders/dev-server/vite/server.js creates the server with config discovery switched off entirely:
const configuration = {
configFile: false,
envFile: false,
cacheDir,
root: virtualProjectRoot,
and virtualProjectRoot is <workspaceRoot>/.angular/vite-root/<project>, a synthetic path, so even with discovery enabled Vite would not look in the project directory. We verified this empirically: with a vite.config.js present in the project root that disables dependency source maps (and which works in a plain Vite project), every measurement below is byte-for-byte unchanged.
This matters because on Vite 8 a plugin-based workaround does exist for plain Vite users, and it is unavailable to Angular users for this reason.
Expected behavior
With "sourceMap": false, or with "sourceMap": { "vendor": false }, the dev server should not generate or link source maps for prebundled dependencies. Failing that, there should be a supported way to disable them.
Actual behavior
.angular/cache/<version>/<project>/vite/deps/ contains:
| File |
Size |
reveal-sdk.js |
24,475,459 bytes |
reveal-sdk.js.map |
37,990,589 bytes |
whole deps folder |
75,283,124 bytes |
The served dependency ends with //# sourceMappingURL=reveal-sdk.js.map, DevTools requests the map, and the DevTools front end becomes unresponsive for five minutes or longer. The application page itself remains responsive.
Behavior differs by Angular version, the problem does not
@angular/build@22.0.9 resolves Vite 7, whose optimizer emits an external, linked map. That is the case measured above: a 24,477,565-byte dependency plus a 37,990,535-byte map that DevTools fetches.
@angular/build@22.1.x resolves Vite 8, whose optimizer emits sourcemap: "hidden" and whose dev server then inlines that map into the dependency response. Measured directly against Vite 8.2.1 with the same package: 21,796,153 bytes on disk served as a 69,037,912-byte response, with the map embedded as base64.
So upgrading Angular changes the shape of the problem but not its existence, and it removes the escape hatch: on Vite 8 there is no optimizeDeps.esbuildOptions.sourcemap to propagate, rolldownOptions.output.sourcemap is explicitly rejected, and build.sourcemap: false was tested and has no effect on the dependency optimizer.
Environment
@angular/cli 22.0.9
@angular/build 22.0.9
@angular/core 22.0.8
vite 7.3.6 (as resolved by @angular/build)
esbuild 0.28.1
Node.js 24.15.0
OS Windows 10
Browsers Chrome, Edge
Things that do not work as a workaround
"sourceMap": { "scripts": true, "vendor": false, "hidden": true } — the dependency map is unaffected; the served dependency still ends with an external sourceMappingURL.
"prebundle": false and optimizeDeps.exclude — moves the dependency into the regular module pipeline, which produced an even larger inline map.
- Disabling source maps in the library's own Rollup build — Vite generates a new map while optimizing the published ESM, independent of whether the input had one.
- A
vite.config.js in the project root — ignored, for the reasons above. Measured with one present: identical bytes.
The only workarounds are per-developer browser settings: a custom DevTools ignore-list rule matching .*\/\.angular\/cache\/.*\/vite\/deps\/<dep>\.js.*, or disabling JavaScript source maps in DevTools globally.
Suggested fix
Primary: do not defeat the browsers' default ignore list
This is the part that is entirely within Angular's control, it requires no upstream change, and it is the only fix that works on both Vite 7 and Vite 8.
Optimized dependencies are served from /.angular/cache/<version>/<project>/vite/deps/, which does not match the default DevTools ignore rule /node_modules/|^node:. Serving them from a path that does match — or otherwise marking them as ignore-listed to DevTools — would restore the behavior every other Vite host already gets, without depending on any Vite change.
Under Vite's own layout this exact dependency and this exact map cause no DevTools problem at all, purely because of where the file is served from.
URL matching is the only mechanism that can prevent the cost. The generated map already contains x_google_ignoreList: [0], so the dependency is correctly declared as third-party — but that field is inside the map, and the browser must download and parse the whole thing before it can read it. Any approach that changes how the map is delivered pays the cost before the ignore signal becomes available. The ignore-list rule is evaluated against the URL, before any fetch, which is why the serving path is the only lever that works.
This is also why the fix has to be here rather than upstream. We verified that the DevTools freeze occurs under both delivery shapes: Vite 8 inlining the map into a 69,037,912-byte response, and Vite 7 serving a 24,477,565-byte dependency alongside a separately fetched 37,990,535-byte map. A pending Vite PR (vitejs/vite#23288) converts the former into the latter, which reduces response size but leaves the DevTools behavior described here unchanged.
Secondary: propagate the vendor source-map setting, while that is still possible
On Vite 7, propagate the build's sourceMap.vendor setting into the dependency optimizer:
esbuildOptions: {
// ...
sourcemap: thirdPartySourcemaps,
},
As noted above, thirdPartySourcemaps is already a parameter of this function and is already referenced in the object literal immediately preceding esbuildOptions, so this is a one-line change using a value that is already in scope. It is also the setting that most directly describes prebundled dependencies.
One coordination detail: setting this alone is not sufficient. Vite's send helper injects a fallback source map into any JavaScript response that carries no map comment, generated with MagicString.generateMap({ includeContent: true }). We measured the same dependency served as a 145,548,097-byte response with an inlined base64 map once the external map was removed — worse than the problem being fixed.
Keeping a sourceMappingURL comment on the optimized dependency avoids that, and the map it points at does not have to exist: Vite's transform middleware already answers .map requests for optimized dependencies with a small dummy map when the file cannot be read. With sourcemap: false plus a retained comment, we measured the dependency served at its normal 24,477,566 bytes and the map request answered with 251 bytes. So this fix is self-contained on Angular's side, provided the emitted dependency still carries a map comment.
This half expires with Vite 8. Its optimizer hardcodes sourcemap: "hidden" after spreading user output options, rolldownOptions.output.sourcemap is explicitly rejected in favor of build.sourcemap, and build.sourcemap: false was tested and has no effect on the dependency optimizer. Once @angular/build moves to Vite 8 there is no option left for Angular to pass, and the primary fix above becomes the only one available. A companion issue has been filed with Vite about the Vite 8 behavior: vitejs/vite#23287. A community PR there, vitejs/vite#23288, changes the optimized-dependency response from an inlined map back to an external sourceMappingURL plus a .map endpoint. That reduces the response size but reproduces the Vite 7 shape described here, so it does not by itself address the DevTools cost — the map is still fetched and parsed.
Minimal Reproduction
Reproduction
Minimal application, no framework bootstrap, using a publicly available large package.
package.json:
{
"dependencies": {
"@angular/common": "22.0.8",
"@angular/compiler": "22.0.8",
"@angular/core": "22.0.8",
"@angular/platform-browser": "22.0.8",
"reveal-sdk": "2.1.0",
"rxjs": "7.8.1",
"tslib": "2.6.2",
"zone.js": "0.15.1"
},
"devDependencies": {
"@angular/build": "22.0.9",
"@angular/cli": "22.0.9",
"@angular/compiler-cli": "22.0.8",
"typescript": "6.0.3"
}
}
angular.json build configuration:
"reveal": {
"browser": "src/main.ts",
"optimization": false,
"sourceMap": false
}
src/main.ts:
import 'reveal-sdk';
document.body.innerHTML = '<p>reveal-sdk is imported.</p>';
Steps:
npm install
- Delete
.angular/cache
ng serve --configuration reveal
- Open the application URL with DevTools closed
- Open Chrome or Edge DevTools
Exception or Error
Your Environment
Angular CLI : 22.0.9
Angular : 22.0.8
Node.js : 24.17.0
Package Manager : npm 11.13.0
Operating System : win32 x64
┌───────────────────────────┬───────────────────┬───────────────────┐
│ Package │ Installed Version │ Requested Version │
├───────────────────────────┼───────────────────┼───────────────────┤
│ @angular/build │ 22.0.9 │ 22.0.9 │
│ @angular/cli │ 22.0.9 │ 22.0.9 │
│ @angular/common │ 22.0.8 │ 22.0.8 │
│ @angular/compiler │ 22.0.8 │ 22.0.8 │
│ @angular/compiler-cli │ 22.0.8 │ 22.0.8 │
│ @angular/core │ 22.0.8 │ 22.0.8 │
│ @angular/platform-browser │ 22.0.8 │ 22.0.8 │
│ rxjs │ 7.8.1 │ 7.8.1 │
│ typescript │ 6.0.3 │ 6.0.3 │
│ zone.js │ 0.15.1 │ 0.15.1 │
Anything else relevant?
No response
Command
serve
Is this a regression?
The previous version in which this bug was not present was
No response
Description
Which @angular/* package(s) are the source of the bug?
@angular/build(dev-server, Vite dependency optimizer configuration)Is this a regression?
No. The behavior exists in the Angular 17
@angular-devkit/build-angulardev server and is unchanged after migrating to@angular/buildon Angular 22.Description
@angular/build:dev-serveralways lets Vite generate source maps for prebundled dependencies. NeithersourceMap: falsenorsourceMap.vendor: falseaffects them, and the builder exposes no way to change it — even though, on the Vite version this Angular release resolves, the underlying option exists and Angular simply never sets it.For most dependencies this is unnoticeable. For a large dependency it produces a multi-megabyte map that the browser's DevTools then downloads and parses on every session. In our case that is a 37,990,535-byte map for a single package, and opening Chrome or Edge DevTools makes the DevTools front end unresponsive for five minutes or longer.
Two details make this specific to Angular rather than a general Vite problem:
No source-map option reaches the dependency optimizer. The build configuration below sets
"sourceMap": false, and the 38 MB dependency map is still generated and linked. The same holds for"sourceMap": { "scripts": true, "vendor": false, "hidden": true }—vendor: falseis the option whose plain meaning is "no source maps for third-party code", and a prebundled dependency is third-party code by definition.The dev-server URL for optimized dependencies defeats the browser's default ignore list. Chrome and Edge ship the default DevTools ignore rule
/node_modules/|^node:. A plain Vite project serves optimized dependencies from/node_modules/.vite/deps/<dep>.js, which matches, so DevTools never processes those maps. Angular serves them from/.angular/cache/<version>/<project>/vite/deps/<dep>.js, which does not match, so DevTools fetches and parses them. The same map is harmless under Vite's default layout and harmful under Angular's.Where the option is dropped
On Vite 7 — the version
@angular/build@22.0.9resolves — the dependency optimizer hardcodessourcemap: trueand then spreads the user'soptimizeDeps.esbuildOptionsover it, so a suppliedsourcemap: falsedoes take effect (packages/vite/src/node/optimizer/index.ts,prepareEsbuildOptimizerRun). The option is available; it is simply never set.Angular constructs that object in
packages/angular/build/src/tools/vite/utils.ts, ingetDepOptimizationConfig, and never setssourcemap:Note that
thirdPartySourcemaps— the value derived from the build'ssourceMap.vendoroption — is already passed intogetDepOptimizationConfigand is already in scope a few lines above this object. Its only current use is to vary the plugin's name for cache differentiation:There is no user-side escape hatch either
Since
@angular/build:dev-serveraccepts no Vite configuration, and its (experimental) extension parameter takes only esbuild build plugins, middleware and an index.html transformer, there is no supported path for a user to set this option.A
vite.config.jsin the project root does not help.packages/angular/build/src/builders/dev-server/vite/server.jscreates the server with config discovery switched off entirely:and
virtualProjectRootis<workspaceRoot>/.angular/vite-root/<project>, a synthetic path, so even with discovery enabled Vite would not look in the project directory. We verified this empirically: with avite.config.jspresent in the project root that disables dependency source maps (and which works in a plain Vite project), every measurement below is byte-for-byte unchanged.This matters because on Vite 8 a plugin-based workaround does exist for plain Vite users, and it is unavailable to Angular users for this reason.
Expected behavior
With
"sourceMap": false, or with"sourceMap": { "vendor": false }, the dev server should not generate or link source maps for prebundled dependencies. Failing that, there should be a supported way to disable them.Actual behavior
.angular/cache/<version>/<project>/vite/deps/contains:reveal-sdk.jsreveal-sdk.js.mapdepsfolderThe served dependency ends with
//# sourceMappingURL=reveal-sdk.js.map, DevTools requests the map, and the DevTools front end becomes unresponsive for five minutes or longer. The application page itself remains responsive.Behavior differs by Angular version, the problem does not
@angular/build@22.0.9resolves Vite 7, whose optimizer emits an external, linked map. That is the case measured above: a 24,477,565-byte dependency plus a 37,990,535-byte map that DevTools fetches.@angular/build@22.1.xresolves Vite 8, whose optimizer emitssourcemap: "hidden"and whose dev server then inlines that map into the dependency response. Measured directly against Vite 8.2.1 with the same package: 21,796,153 bytes on disk served as a 69,037,912-byte response, with the map embedded as base64.So upgrading Angular changes the shape of the problem but not its existence, and it removes the escape hatch: on Vite 8 there is no
optimizeDeps.esbuildOptions.sourcemapto propagate,rolldownOptions.output.sourcemapis explicitly rejected, andbuild.sourcemap: falsewas tested and has no effect on the dependency optimizer.Environment
Things that do not work as a workaround
"sourceMap": { "scripts": true, "vendor": false, "hidden": true }— the dependency map is unaffected; the served dependency still ends with an externalsourceMappingURL."prebundle": falseandoptimizeDeps.exclude— moves the dependency into the regular module pipeline, which produced an even larger inline map.vite.config.jsin the project root — ignored, for the reasons above. Measured with one present: identical bytes.The only workarounds are per-developer browser settings: a custom DevTools ignore-list rule matching
.*\/\.angular\/cache\/.*\/vite\/deps\/<dep>\.js.*, or disabling JavaScript source maps in DevTools globally.Suggested fix
Primary: do not defeat the browsers' default ignore list
This is the part that is entirely within Angular's control, it requires no upstream change, and it is the only fix that works on both Vite 7 and Vite 8.
Optimized dependencies are served from
/.angular/cache/<version>/<project>/vite/deps/, which does not match the default DevTools ignore rule/node_modules/|^node:. Serving them from a path that does match — or otherwise marking them as ignore-listed to DevTools — would restore the behavior every other Vite host already gets, without depending on any Vite change.Under Vite's own layout this exact dependency and this exact map cause no DevTools problem at all, purely because of where the file is served from.
URL matching is the only mechanism that can prevent the cost. The generated map already contains
x_google_ignoreList: [0], so the dependency is correctly declared as third-party — but that field is inside the map, and the browser must download and parse the whole thing before it can read it. Any approach that changes how the map is delivered pays the cost before the ignore signal becomes available. The ignore-list rule is evaluated against the URL, before any fetch, which is why the serving path is the only lever that works.This is also why the fix has to be here rather than upstream. We verified that the DevTools freeze occurs under both delivery shapes: Vite 8 inlining the map into a 69,037,912-byte response, and Vite 7 serving a 24,477,565-byte dependency alongside a separately fetched 37,990,535-byte map. A pending Vite PR (vitejs/vite#23288) converts the former into the latter, which reduces response size but leaves the DevTools behavior described here unchanged.
Secondary: propagate the vendor source-map setting, while that is still possible
On Vite 7, propagate the build's
sourceMap.vendorsetting into the dependency optimizer:As noted above,
thirdPartySourcemapsis already a parameter of this function and is already referenced in the object literal immediately precedingesbuildOptions, so this is a one-line change using a value that is already in scope. It is also the setting that most directly describes prebundled dependencies.One coordination detail: setting this alone is not sufficient. Vite's
sendhelper injects a fallback source map into any JavaScript response that carries no map comment, generated withMagicString.generateMap({ includeContent: true }). We measured the same dependency served as a 145,548,097-byte response with an inlined base64 map once the external map was removed — worse than the problem being fixed.Keeping a
sourceMappingURLcomment on the optimized dependency avoids that, and the map it points at does not have to exist: Vite's transform middleware already answers.maprequests for optimized dependencies with a small dummy map when the file cannot be read. Withsourcemap: falseplus a retained comment, we measured the dependency served at its normal 24,477,566 bytes and the map request answered with 251 bytes. So this fix is self-contained on Angular's side, provided the emitted dependency still carries a map comment.This half expires with Vite 8. Its optimizer hardcodes
sourcemap: "hidden"after spreading user output options,rolldownOptions.output.sourcemapis explicitly rejected in favor ofbuild.sourcemap, andbuild.sourcemap: falsewas tested and has no effect on the dependency optimizer. Once@angular/buildmoves to Vite 8 there is no option left for Angular to pass, and the primary fix above becomes the only one available. A companion issue has been filed with Vite about the Vite 8 behavior: vitejs/vite#23287. A community PR there, vitejs/vite#23288, changes the optimized-dependency response from an inlined map back to an externalsourceMappingURLplus a.mapendpoint. That reduces the response size but reproduces the Vite 7 shape described here, so it does not by itself address the DevTools cost — the map is still fetched and parsed.Minimal Reproduction
Reproduction
Minimal application, no framework bootstrap, using a publicly available large package.
package.json:{ "dependencies": { "@angular/common": "22.0.8", "@angular/compiler": "22.0.8", "@angular/core": "22.0.8", "@angular/platform-browser": "22.0.8", "reveal-sdk": "2.1.0", "rxjs": "7.8.1", "tslib": "2.6.2", "zone.js": "0.15.1" }, "devDependencies": { "@angular/build": "22.0.9", "@angular/cli": "22.0.9", "@angular/compiler-cli": "22.0.8", "typescript": "6.0.3" } }angular.jsonbuild configuration:src/main.ts:Steps:
npm install.angular/cacheng serve --configuration revealException or Error
Your Environment
Anything else relevant?
No response