From 1d51c55e9d853de5589ad72786899f2522e1a88f Mon Sep 17 00:00:00 2001 From: arlo Date: Thu, 22 Oct 2026 16:57:47 +0800 Subject: [PATCH] perf(vite): speed up inspect module list --- .../node/__tests__/inspect-context.test.ts | 45 +++++++++++++++++++ packages/vite/src/node/inspect/context.ts | 23 ++++++++-- packages/vite/src/node/inspect/module.ts | 18 +++++++- packages/vite/src/node/inspect/store/index.ts | 7 +++ packages/vite/src/node/inspect/store/types.ts | 2 + 5 files changed, 90 insertions(+), 5 deletions(-) diff --git a/packages/vite/src/node/__tests__/inspect-context.test.ts b/packages/vite/src/node/__tests__/inspect-context.test.ts index 9539ca3a9..6b99601c7 100644 --- a/packages/vite/src/node/__tests__/inspect-context.test.ts +++ b/packages/vite/src/node/__tests__/inspect-context.test.ts @@ -1509,3 +1509,48 @@ describe('vite inspect batched writes', () => { ]) }) }) + +describe('vite inspect modules list', () => { + it('reuses the list until a recorded change and then rebuilds it', async () => { + const { ctx, envCtx, vite } = await createFixture() + const plugin = vite.config.plugins[1] + const record = (id: string) => { + envCtx.recordTransform(id, { name: 'plugin-a', result: `export const id = '${id}'`, start: 0, end: 1 }, `const id = '${id}'`, plugin) + } + + record('/src/a.ts') + const first = await envCtx.getModulesList() + expect(first.map(module => module.id)).toEqual(['/src/a.ts']) + expect(await envCtx.getModulesList()).toBe(first) + + // Finishing a transform request doesn't change the recorded modules. + ctx.store.clearPendingLoadMetrics(envCtx.scope, {}) + expect(await envCtx.getModulesList()).toBe(first) + + record('/src/b.ts') + const second = await envCtx.getModulesList() + expect(second.map(module => module.id)).toEqual(['/src/a.ts', '/src/b.ts']) + + envCtx.invalidate('/src/a.ts') + expect((await envCtx.getModulesList()).map(module => module.id)).toEqual(['/src/b.ts']) + }) + + it('builds the full sorted list when it takes several slices', async () => { + const { envCtx, vite } = await createFixture() + const plugin = vite.config.plugins[1] + const ids = Array.from({ length: 3000 }, (_, index) => `/src/m${String(index).padStart(4, '0')}.ts`) + for (const id of ids) + envCtx.recordTransform(id, { name: 'plugin-a', result: 'export {}', start: 0, end: 1 }, 'const a = 1', plugin) + + // Make every module take longer than a slice, so the build yields between modules. + const now = vi.spyOn(performance, 'now') + let clock = 0 + now.mockImplementation(() => (clock += 10)) + try { + expect((await envCtx.getModulesList()).map(module => module.id)).toEqual(ids) + } + finally { + now.mockRestore() + } + }) +}) diff --git a/packages/vite/src/node/inspect/context.ts b/packages/vite/src/node/inspect/context.ts index 71c599aca..55e7b4116 100644 --- a/packages/vite/src/node/inspect/context.ts +++ b/packages/vite/src/node/inspect/context.ts @@ -181,12 +181,14 @@ export class ViteInspectViteContext { } // Ids can carry HMR timestamps, so cap the cache instead of letting it grow for the whole session. -const MAX_PUBLIC_MODULE_ID_CACHE_SIZE = 10_000 +// Keep it well above the module count of large projects, or the cache keeps clearing and never hits. +const MAX_PUBLIC_MODULE_ID_CACHE_SIZE = 100_000 export class ViteInspectEnvironmentContext { readonly scope: string private pluginCallCount = 0 private graphModuleIdResolver?: ReturnType + private modulesList?: { version: number, modules: ViteInspectModuleInfo[] } // Every hook call records its module id, so the same id is normalized once per plugin per request. private readonly publicModuleIds = new Map() private publicModuleIdsRoot?: string @@ -405,12 +407,27 @@ export class ViteInspectEnvironmentContext { } async getModulesList(pluginCtx?: ModuleInfoProvider): Promise { + // The panel refetches the list after every edit, often twice; reuse it until something is recorded. + const store = this.inspectContext.store + await store.flush() + const version = store.getChangeVersion() + if (!pluginCtx && this.modulesList?.version === version) { + this.graphModuleIdResolver ??= this.createGraphModuleIdResolver(this.modulesList.modules) + return this.modulesList.modules + } + const modules = await getEnvironmentModulesList(this, pluginCtx) - this.graphModuleIdResolver = createGraphModuleIdResolver( + this.graphModuleIdResolver = this.createGraphModuleIdResolver(modules) + if (!pluginCtx) + this.modulesList = { version, modules } + return modules + } + + private createGraphModuleIdResolver(modules: ViteInspectModuleInfo[]): ReturnType { + return createGraphModuleIdResolver( modules.map(module => module.id), this.env.getTopLevelConfig().resolve?.extensions ?? [], ) - return modules } async getGraphModuleIdResolver(): Promise> { diff --git a/packages/vite/src/node/inspect/module.ts b/packages/vite/src/node/inspect/module.ts index faae8057e..d08df3c9c 100644 --- a/packages/vite/src/node/inspect/module.ts +++ b/packages/vite/src/node/inspect/module.ts @@ -6,6 +6,8 @@ import type { } from './types' import { DUMMY_LOAD_PLUGIN_NAME } from './utils' +const MODULES_LIST_SLICE_MS = 4 + export interface ModuleInfoProvider { getModuleInfo: (id: string) => { importedIds?: readonly string[] @@ -53,7 +55,7 @@ export async function getModulesList( const ids = new Set(Object.keys(transformsById).concat(Object.keys(transformedIdMap))) - return Array.from(ids).sort().map((id) => { + const toModuleInfo = (id: string): ViteInspectModuleInfo => { let totalTime = 0 const transforms = transformsById[id] || [] const transformPlugins: ViteInspectModulePluginMetric[] = transforms.map((transform) => { @@ -85,7 +87,19 @@ export async function getModulesList( sourceSize: firstTransform?.hasResult ? firstTransform.resultSize : 0, distSize: lastTransform?.hasResult ? lastTransform.resultSize : 0, } - }) + } + + // Large projects take tens of milliseconds here; yield so Vite can keep serving requests meanwhile. + const modules: ViteInspectModuleInfo[] = [] + let sliceStart = performance.now() + for (const id of Array.from(ids).sort()) { + modules.push(toModuleInfo(id)) + if (performance.now() - sliceStart > MODULES_LIST_SLICE_MS) { + await new Promise(resolve => setImmediate(resolve)) + sliceStart = performance.now() + } + } + return modules } export async function getPublicGraphModuleIds(ctx: ViteInspectEnvironmentContext): Promise { diff --git a/packages/vite/src/node/inspect/store/index.ts b/packages/vite/src/node/inspect/store/index.ts index 1346b8fe3..b4ff81503 100644 --- a/packages/vite/src/node/inspect/store/index.ts +++ b/packages/vite/src/node/inspect/store/index.ts @@ -112,6 +112,7 @@ class PayloadViteInspectStore implements ViteInspectStore { private peakQueuedBytes = 0 private writeBatches = 0 + private changeVersion = 0 private activeReaders = 0 private readerWaiters: Array<() => void> = [] @@ -318,6 +319,10 @@ class PayloadViteInspectStore implements ViteInspectStore { return Array.from(moduleIds) } + getChangeVersion(): number { + return this.changeVersion + } + getStats(): ViteInspectStoreStats { return { maxBatchItems: this.maxBatchItems, @@ -354,6 +359,8 @@ class PayloadViteInspectStore implements ViteInspectStore { private enqueue(write: QueuedWrite): void { if (this.closing || this.closed || this.lastWriteError) return + if (write.operation !== 'clearPendingLoadMetrics') + this.changeVersion += 1 const estimatedBytes = estimateWriteBytes(write) this.queue.push({ write, estimatedBytes }) diff --git a/packages/vite/src/node/inspect/store/types.ts b/packages/vite/src/node/inspect/store/types.ts index 8936e0f9b..e6770d79d 100644 --- a/packages/vite/src/node/inspect/store/types.ts +++ b/packages/vite/src/node/inspect/store/types.ts @@ -117,6 +117,8 @@ export interface ViteInspectStore { getFirstResolveResult: (scope: string, sourceId: string) => Promise findModuleId: (scope: string, publicModuleId: string) => Promise getModuleIds: (scope: string) => Promise + /** Increases whenever a queued write can change recorded modules, so callers can reuse derived data until it changes. */ + getChangeVersion: () => number getStats: () => ViteInspectStoreStats flush: () => Promise close: () => Promise