From a163920905b9d3f23ccc439d56dfad8214f6e105 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Tue, 7 Jul 2026 11:08:56 +0200 Subject: [PATCH] perf(browser): pre-bundle the vitest runtime in optimizeDeps The browser now fetches a few optimized chunks instead of ~20 separately-served dist chunks. vitest, vitest/internal/browser and @vitest/browser/client are optimized in a single pass so esbuild dedupes their shared stateful chunks, preserving module identity between the test files' import of 'vitest' and the tester. Frames from the optimizer cache (.vite/vitest) are ignored in stack traces like the un-bundled dist ones. --- packages/browser/src/node/index.ts | 30 +++++++++++-------- packages/utils/src/source-map.ts | 4 +++ .../vitest/src/node/plugins/vitestResolver.ts | 12 ++++++++ test/browser/specs/runner.test.ts | 3 ++ test/e2e/test/reporters/merge-reports.test.ts | 7 ++++- 5 files changed, 43 insertions(+), 13 deletions(-) diff --git a/packages/browser/src/node/index.ts b/packages/browser/src/node/index.ts index ece3da329ffd..67c8fb3b8ab9 100644 --- a/packages/browser/src/node/index.ts +++ b/packages/browser/src/node/index.ts @@ -378,23 +378,18 @@ function resolveBrowserOptimizeDeps( ...(testConfig.snapshotSerializers || []), ] + // Keep these external (never pre-bundle by esbuild): + // - vitest/browser, @vitest/browser/context, @vitest/browser/utils are + // VIRTUAL modules generated per-server (see pluginContext.ts) — esbuild + // cannot resolve/run their `load`, it would freeze stale/empty content. + // - vite/module-runner statically imports node:sqlite/sea/test and reads + // process.getBuiltinModule, so it is not bundlable for the browser. + // - msw is a large, side-effectful service-worker library. const exclude = [ - 'vitest', 'vitest/browser', - 'vitest/internal/browser', 'vite/module-runner', '@vitest/browser/utils', '@vitest/browser/context', - '@vitest/browser/client', - '@vitest/utils', - '@vitest/utils/source-map', - '@vitest/spy', - '@vitest/utils/error', - 'std-env', - 'tinybench', - 'tinyspy', - 'tinyrainbow', - 'pathe', 'msw', 'msw/browser', ] @@ -424,10 +419,21 @@ function resolveBrowserOptimizeDeps( } } + // Pre-bundle the vitest runtime so the browser fetches a few optimized + // chunks instead of ~20 separately-served dist chunks (faster startup). + // `vitest`, `vitest/internal/browser` and `@vitest/browser/client` are + // optimized together in a single pass, so esbuild dedupes their shared + // stateful chunks (the test collector, the runner, the RPC client) to a + // single instance — preserving module identity between the test files' + // `import 'vitest'` and the tester. Their transitive deps (@vitest/utils, + // @vitest/spy, pathe, tinyrainbow, …) are inlined into these bundles. const include = [ 'vitest > expect-type', 'vitest > magic-string', 'vitest > chai', + 'vitest', + 'vitest/internal/browser', + '@vitest/browser/client', ] const provider = testConfig.browser?.provider diff --git a/packages/utils/src/source-map.ts b/packages/utils/src/source-map.ts index e57be5cdf32e..16fd379cf598 100644 --- a/packages/utils/src/source-map.ts +++ b/packages/utils/src/source-map.ts @@ -31,6 +31,10 @@ const stackIgnorePatterns: (string | RegExp)[] = [ '/deps/@vitest', '/deps/loupe', '/deps/chai', + // the Vitest runtime is pre-bundled into a dedicated optimizer cache + // (`node_modules/.vite/vitest//deps/`) in browser mode, so its internal + // frames must be ignored the same way as the un-bundled `/vitest/dist/` ones + '/.vite/vitest/', '/browser-playwright/dist/locators.js', '/browser-webdriverio/dist/locators.js', '/browser-preview/dist/locators.js', diff --git a/packages/vitest/src/node/plugins/vitestResolver.ts b/packages/vitest/src/node/plugins/vitestResolver.ts index dfb19cb6580e..bc328e571f07 100644 --- a/packages/vitest/src/node/plugins/vitestResolver.ts +++ b/packages/vitest/src/node/plugins/vitestResolver.ts @@ -48,6 +48,18 @@ export function VitestCoreResolver(): Plugin { }, async resolveId(id) { if (id === 'vitest') { + // in environments that pre-bundle vitest (the browser `client` + // environment), the optimizer's copy must win: the tester and the + // test files have to share a single module instance, and returning + // the dist file directly would bypass the optimized dep resolution + if (this.environment?.config.optimizeDeps.include?.includes('vitest')) { + const resolved = await this.resolve(id, join(root, 'index.html'), { + skipSelf: true, + }) + if (resolved) { + return resolved + } + } return resolve(distDir, 'index.js') } if (id.startsWith('@vitest/') || id.startsWith('vitest/')) { diff --git a/test/browser/specs/runner.test.ts b/test/browser/specs/runner.test.ts index 50b9ee807980..04d7a63989a5 100644 --- a/test/browser/specs/runner.test.ts +++ b/test/browser/specs/runner.test.ts @@ -182,6 +182,9 @@ describe('console logging tests', async () => { stdout, } = await runBrowserTests({ root: './fixtures/print-logs', + // assert on Vitest's own console forwarding rather than Vite's client + // relay, which does not run when the tester loads the stubbed @vite/client + reporters: ['default'], })) }) diff --git a/test/e2e/test/reporters/merge-reports.test.ts b/test/e2e/test/reporters/merge-reports.test.ts index 23b8c5396b99..fe6558f25c03 100644 --- a/test/e2e/test/reporters/merge-reports.test.ts +++ b/test/e2e/test/reporters/merge-reports.test.ts @@ -390,11 +390,14 @@ test.for([ "/sub/subject.ts" ], "/basic.test.ts": [ + "/vitest.js", "/sub/format.ts", "/util.ts" ] }, - "externalized": [], + "externalized": [ + "/vitest.js?v=" + ], "inlined": [ "/basic.test.ts", "/sub/format.ts", @@ -409,11 +412,13 @@ test.for([ "/sub/subject.ts" ], "/second.test.ts": [ + "/vitest.js", "/util.ts", "/obug.js" ] }, "externalized": [ + "/vitest.js?v=", "/obug.js?v=" ], "inlined": [