Skip to content

Dev server generates source maps for prebundled dependencies even when sourceMap is false, with no way to opt out #33874

Description

@hanastasov

Command

serve

Is this a regression?

  • Yes, this behavior used to work in the previous version

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:

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

  2. 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:

  1. npm install
  2. Delete .angular/cache
  3. ng serve --configuration reveal
  4. Open the application URL with DevTools closed
  5. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions