Skip to content

perf(cli): give the Vitest resolver a resolveId hook filter - #3434

Merged
fengmk2 merged 7 commits into
voidzero-dev:mainfrom
jasonkuhrt:perf/vitest-resolver-hook-filter
Nov 12, 2026
Merged

fengmk2 merged 7 commits into
voidzero-dev:mainfrom
jasonkuhrt:perf/vitest-resolver-hook-filter

Conversation

@jasonkuhrt

@jasonkuhrt jasonkuhrt commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

Written by an AI agent (Claude) on behalf of @jasonkuhrt.

vite-plus:vitest-resolver now declares a resolveId hook filter, so its handler is called only for vitest, vitest/*, @vitest/* and vite-plus/test. In a Rolldown build every other import is rejected before the call into JavaScript. Refs #2307.

Today, on v1.6.0

defineConfig injects the plugin into every config, and its resolveId is a plain function, so a build calls it once per import to return null. Each call crosses from Rust into JavaScript and back.

The same build through the published defineConfig and through this branch's, alternating in one process, on a generated graph where each module imports the next two and nothing imports Vitest. Wall time of build(), nine runs each on one machine (script below):

Modules resolveId calls v1.6.0, median (range) This branch, median (range)
6,000 12,000 305 ms (217 to 386) 195 ms (165 to 213)
20,000 40,000 1,927 ms (1,494 to 2,857) 1,364 ms (948 to 1,789)

That is about 9 to 14 µs per skipped call on this graph. The graph is all resolution and no real code, so it shows the cost of a call, not what an app saves. #2307's own A/B on an app with 12,000 calls found no clear difference, which fits a saving of one or two tenths of a second.

The change

The handler body is unchanged. It moves under handler and gains:

filter: { id: /^(?:vitest(?:\/|$)|@vitest\/|vite-plus\/test$)/ },

The filter is a superset of what the handler acts on: isVitestFamilySpecifier accepts vitest and the @vitest/* packages in VITEST_RESOLVER_PACKAGES, and the vite-plus/test branch matches that exact id. The allowlist still decides which packages are redirected, and a new test checks every member of it against the filter, so an entry added outside those prefixes fails a test.

In the dev server and in Vitest, Vite's plugin container applies the same filter in JavaScript before it calls the handler.

Tests

define-config-vitest-resolver.spec.ts gains:

  • a native-build case for each filter alternative (vitest, a vitest/ subpath, @vitest/mocker, vite-plus/test) and one for an unrelated import (react), where the emitted external import shows which resolver answered. The react case shows the handler no longer runs for an unrelated import: its stub would answer if it were entered;
  • a case that checks every VITEST_RESOLVER_PACKAGES member, with and without a subpath, against the filter.

The helpers that call the hook directly read .handler.

I ran the four define-config-* suites (99 tests) on this source against the published 1.6.0 native binding, plus vp fmt and oxlint without its type-aware rules on the two files. I did not run vp check or the full pnpm test:unit locally.

Not claimed

This does not settle #2307: the plugin is still injected, and only its per-import call goes. [PLUGIN_TIMINGS] can't name the resolver on v1.6.0, with or without the filter: Rolldown 1.2.18 times each callback inside JavaScript and lists one only from one second (source), and #2307 measured the resolver's at 2.78 ms.

The benchmark script

Run from a folder two levels below the root of an installed checkout, in a folder with vite-plus@1.6.0 installed, on a Node that strips types, with N=6000 or N=20000.

// Wall time of the same build through the published defineConfig and this
// branch's, alternating, on a generated graph where nothing imports Vitest.
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import path from 'node:path';
import * as Released from 'vite-plus';
import * as Source from '../../packages/cli/src/define-config.ts';

const N = Number(process.env.N ?? 6000);
const ROUNDS = Number(process.env.ROUNDS ?? 9);
const root = await mkdtemp(path.join(tmpdir(), 'resolver-bench-'));
for (let i = 0; i < N; i++) {
  const imports = [i + 1, i + 2].filter((n) => n < N).map((n) => `import './m${n}.js';`);
  await writeFile(path.join(root, `m${i}.js`), `${imports.join('\n')}\nglobalThis.v${i} = ${i};\n`);
}
async function run(defineConfig) {
  const config = defineConfig({
    root,
    configFile: false,
    logLevel: 'silent',
    build: { write: false, minify: false, rolldownOptions: { input: path.join(root, 'm0.js') } },
  });
  const started = performance.now();
  await Released.build(config);
  return Math.round(performance.now() - started);
}
const median = (xs) => [...xs].sort((a, b) => a - b)[Math.floor(xs.length / 2)];
const released = [];
const source = [];
await run(Released.defineConfig); // warm-up, discarded
for (let round = 0; round < ROUNDS; round++) {
  released.push(await run(Released.defineConfig));
  source.push(await run(Source.defineConfig));
}
await rm(root, { recursive: true, force: true });
console.log(JSON.stringify({ modules: N, resolveCalls: 2 * N, released, source, medianReleased: median(released), medianSource: median(source) }));

The two pnpm tsgo errors in the resolver spec are fixed: findPlugin returns Plugin | undefined, so the two casts are gone.

@fengmk2 fengmk2 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@jasonkuhrt Thanks for the contribution!

Before I merge, please fix the typecheck error first. 🤟

@fengmk2 fengmk2 added test: e2e Auto run e2e tests test: install-e2e run vite install e2e test test: create-e2e Run `vp create` e2e tests labels Nov 12, 2026
@fengmk2
fengmk2 merged commit df723c3 into voidzero-dev:main Nov 12, 2026
120 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test: create-e2e Run `vp create` e2e tests test: e2e Auto run e2e tests test: install-e2e run vite install e2e test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants