Skip to content

Commit 6aefb3d

Browse files
authored
perf(browser): stop serving framework sourcemaps in headless runs (#10728)
1 parent 62b8d3b commit 6aefb3d

7 files changed

Lines changed: 128 additions & 5 deletions

File tree

docs/.vitepress/config.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -654,6 +654,10 @@ export default ({ mode }: { mode: string }) => {
654654
text: 'browser.screenshotFailures',
655655
link: '/config/browser/screenshotfailures',
656656
},
657+
{
658+
text: 'browser.dependencySourcemaps',
659+
link: '/config/browser/dependencysourcemaps',
660+
},
657661
{
658662
text: 'browser.orchestratorScripts',
659663
link: '/config/browser/orchestratorscripts',
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
title: browser.dependencySourcemaps | Config
3+
outline: deep
4+
---
5+
6+
# browser.dependencySourcemaps
7+
8+
- **Type:** `boolean`
9+
- **Default:** `true`
10+
11+
Serve sourcemaps of your dependencies (files in `node_modules`) to the browser during headless test runs.
12+
13+
These sourcemaps are used by browser devtools: with `dependencySourcemaps: false`, pausing inside dependency code shows the compiled code the browser actually runs instead of the dependency's original sources. If you don't debug into your dependencies this way, disabling them makes test runs faster: the server doesn't generate and inline the maps, and every browser tab downloads several times fewer bytes.
14+
15+
Reported test errors are not affected: when an error is thrown inside a pre-bundled dependency, Vitest maps its stack frames using the sourcemaps stored on disk even when this option is disabled. Frames from dependencies that are served without pre-bundling (for example, [linked packages](https://vite.dev/guide/dep-pre-bundling#monorepos-and-linked-dependencies)) that don't ship their own sourcemaps fall back to the position in the served code, which usually matches the original file.
16+
17+
Vitest never serves sourcemaps of its own pre-built modules in headless runs (unless [`--inspect`](/guide/cli#inspect) is used) — their frames are hidden from stack traces anyway. Sourcemaps of your own source files are always served.
18+
19+
::: tip
20+
If some of your workspace code resolves to a `node_modules` path (for example, with `resolve.preserveSymlinks`), set [`server.sourcemapIgnoreList`](https://vite.dev/config/server-options#server-sourcemapignorelist) to keep its sourcemaps even when this option is disabled.
21+
:::

docs/guide/cli-generated.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,13 @@ Default position for the details panel in browser mode. Either `right` (horizont
381381

382382
If connection to the browser takes longer, the test suite will fail (default: `60_000`)
383383

384+
### browser.dependencySourcemaps
385+
386+
- **CLI:** `--browser.dependencySourcemaps`
387+
- **Config:** [browser.dependencySourcemaps](/config/browser/dependencysourcemaps)
388+
389+
Serve sourcemaps of dependencies to the browser in headless runs, used by devtools when debugging into `node_modules`. Reported test errors are source-mapped either way. Use `--browser.dependencySourcemaps=false` to speed up test runs if you don't step into dependency code (default: `true`)
390+
384391
### browser.trackUnhandledErrors
385392

386393
- **CLI:** `--browser.trackUnhandledErrors`

packages/browser/src/node/index.ts

Lines changed: 55 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,11 @@ import { createRequire } from 'node:module'
55
import { MockerRegistry } from '@vitest/mocker'
66
import { interceptorPlugin } from '@vitest/mocker/node'
77
import { distClientRoot as uiClientRoot } from '@vitest/ui'
8-
import { toArray } from '@vitest/utils/helpers'
8+
import { cleanUrl, toArray } from '@vitest/utils/helpers'
99
import { join, resolve } from 'pathe'
1010
import sirv from 'sirv'
1111
import c from 'tinyrainbow'
12-
import { isFileServingAllowed, isValidApiRequest, rolldownVersion, distDir as vitestDist } from 'vitest/node'
12+
import { isCSSRequest, isFileServingAllowed, isValidApiRequest, rolldownVersion, distDir as vitestDist } from 'vitest/node'
1313
import { version } from '../../package.json'
1414
import { distRoot } from './constants'
1515
import { createOrchestratorMiddleware } from './middlewares/orchestratorMiddleware'
@@ -347,11 +347,64 @@ body {
347347
...BrowserPlugin(contribution),
348348
// this plugin's `configureServer` is ignored since it's added through `applyToEnvironment`
349349
interceptorPlugin({ registry: mockerRegistry }),
350+
{
351+
name: 'vitest:browser:framework-sourcemaps',
352+
enforce: 'post',
353+
transform(code, id) {
354+
const parentServer = contribution.parent as ParentBrowserProject | undefined
355+
// In a headless run nothing can open devtools, so sourcemaps of
356+
// Vitest's own pre-built modules are never consumed: their stack
357+
// frames are filtered by stackIgnorePatterns. Generating and
358+
// inlining these maps costs server CPU and multiplies the bytes
359+
// the browser downloads by ~5 for every fresh browser context.
360+
// Sourcemaps of user files and (by default) their dependencies are
361+
// kept — they point error stacks and devtools at original sources.
362+
if (
363+
!parentServer
364+
|| !isHeadlessServer(parentServer)
365+
|| parentServer.vitest.config.inspector.enabled
366+
) {
367+
return null
368+
}
369+
if (isCSSRequest(id)) {
370+
return null
371+
}
372+
const path = cleanUrl(id)
373+
if (path.startsWith(distRoot) || path.startsWith(vitestDist)) {
374+
return { code, map: { mappings: '' } as any }
375+
}
376+
// users that never debug into node_modules can drop dependency
377+
// sourcemaps entirely; `server.sourcemapIgnoreList` (default:
378+
// node_modules) can opt paths back in even then, e.g. with
379+
// `preserveSymlinks` where workspace code keeps its node_modules
380+
// path and would be wrongly treated as a dependency
381+
if (
382+
parentServer.config.browser.dependencySourcemaps === false
383+
&& path.includes('/node_modules/')
384+
&& (parentServer.vite.config.server.sourcemapIgnoreList(path, path) ?? true)
385+
) {
386+
return { code, map: { mappings: '' } as any }
387+
}
388+
return null
389+
},
390+
},
350391
]
351392

352393
return contribution
353394
}
354395

396+
function isHeadlessServer(parentServer: ParentBrowserProject): boolean {
397+
if (!parentServer.config.browser.headless) {
398+
return false
399+
}
400+
// sibling instances share this server (and its module graph cache, so a
401+
// late-spawned instance would receive already-cached transforms) and can
402+
// override `headless` — check the static instance options instead of the
403+
// lazily populated `children` to stay deterministic across runs
404+
const instances = parentServer.config.browser.instances ?? []
405+
return instances.every(instance => instance.headless !== false)
406+
}
407+
355408
function resolveBrowserOptimizeDeps(
356409
projectConfigs: ResolvedConfig[],
357410
testFiles: string[],

packages/browser/src/node/projectParent.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,22 @@ export class ParentBrowserProject {
6464
}
6565

6666
const result = this.vite.moduleGraph.getModuleById(id)?.transformResult
67-
// handle non-inline source map such as pre-bundled deps in node_modules/.vite
68-
if (result && !result.map) {
69-
const filePath = id.split('?')[0]
67+
const filePath = id.split('?')[0]
68+
// prefer the map stored on disk when the transform pipeline can't
69+
// provide a usable one:
70+
// - pre-bundled deps: the pipeline map resolves back into the
71+
// optimizer cache, while the map esbuild wrote next to the file
72+
// points at the real package sources
73+
// - an empty `mappings` means the map was intentionally not served
74+
// to the browser (`vitest:browser:framework-sourcemaps`), but disk
75+
// is still the source of truth for error stack traces
76+
if (
77+
result && (
78+
!result.map
79+
|| result.map.mappings === ''
80+
|| filePath.startsWith(this.vite.config.cacheDir)
81+
)
82+
) {
7083
const extracted = extractSourcemapFromFile(result.code, filePath)
7184
this.sourceMapCache.set(id, extracted?.map)
7285
return extracted?.map

packages/vitest/src/node/cli/cli-config.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,9 @@ export const cliOptionsConfig: VitestCLIOptions = {
397397
description: 'If connection to the browser takes longer, the test suite will fail (default: `60_000`)',
398398
argument: '<timeout>',
399399
},
400+
dependencySourcemaps: {
401+
description: 'Serve sourcemaps of dependencies to the browser in headless runs, used by devtools when debugging into `node_modules`. Reported test errors are source-mapped either way. Use `--browser.dependencySourcemaps=false` to speed up test runs if you don\'t step into dependency code (default: `true`)',
402+
},
400403
trackUnhandledErrors: {
401404
description: 'Control if Vitest catches uncaught exceptions so they can be reported (default: `true`)',
402405
},

packages/vitest/src/node/types/browser.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,28 @@ export interface BrowserConfigOptions {
270270
*/
271271
screenshotFailures?: boolean
272272

273+
/**
274+
* Serve sourcemaps of your dependencies (files in `node_modules`) to the
275+
* browser during headless test runs.
276+
*
277+
* These sourcemaps are used by browser devtools: when disabled, pausing
278+
* inside dependency code shows the compiled code the browser actually
279+
* runs instead of the dependency's original sources. If you don't debug
280+
* into your dependencies this way, disabling them makes test runs faster:
281+
* the server doesn't generate and inline the maps, and every browser tab
282+
* downloads several times fewer bytes.
283+
*
284+
* Reported test errors are not affected: stack frames pointing into a
285+
* pre-bundled dependency are mapped using the sourcemaps stored on disk
286+
* even when this option is disabled.
287+
*
288+
* Vitest never serves sourcemaps of its own pre-built modules in headless
289+
* runs (unless `--inspect` is used) — their frames are hidden from stack
290+
* traces anyway. Sourcemaps of your own source files are always served.
291+
* @default true
292+
*/
293+
dependencySourcemaps?: boolean
294+
273295
/**
274296
* Path to the index.html file that will be used to run tests.
275297
*/

0 commit comments

Comments
 (0)
Sponsor
SponsoredKunjungi sekarang
Promo