perf(routing): cache app-route-graph directory reads — 5000-route build −32%#2389
Merged
Merged
Conversation
a28c9ce to
5d668ca
Compare
aa3984b to
7ac72a9
Compare
commit: |
discoverInheritedParallelSlots reads every ancestor directory of each route with fs.readdirSync to find @slot dirs. Because routes share ancestors, a directory with N sibling routes is read once per descendant route, making the scan super-linear in route-directory width. Memoize the reads in a per-scan WeakMap<matcher, Map<dir, Dirent[]>> (same pattern as findFileProbeCache/findSlotSubPagesCache), scoped to one scan so it's collected afterwards. On a 10k-route fixture this cuts build wall time ~52% (~86s -> ~42s) and syscall time ~61%, output byte-identical.
7ac72a9 to
d64ac0b
Compare
This was referenced Jun 28, 2026
Contributor
Performance benchmarksCompared 0 improved · 0 regressed · 6 within ±1.5%
View detailed results and traces 🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head |
james-elicx
approved these changes
Jun 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Memoizes the raw
fs.readdirSync(dir, { withFileTypes: true })calls in the App Router graph scan, via a per-scanWeakMap<ValidFileMatcher, Map<string, Dirent[]>>(the same pattern as the existingfindFileProbeCache/findSlotSubPagesCache).discoverParallelSlotsandfindSlotRootPageread through it.Why
discoverInheritedParallelSlotsreads every ancestor directory of each route to look for@slotdirs; because routes share ancestors, a directory with N sibling routes is re-read once per descendant route — super-linear in route-directory width.findFileProbeCachememoizesfindFile, but not these rawreaddirSync/existsSynccalls. The graph is scanned both during build and at dev-server startup.Measured
Before/after this cache on the generated stress app (#2388 adds the 5000-route scenario), local wall-clock. The effect is super-linear, so it grows with scale:
readDirEntriesCachedreturns exactly the entriesfs.readdirSyncwould for each directory, so the discovered route/file set — and the module transforms that follow — are unchanged (verified by an identical module-transform count). Dev cold start improves because the route scan runs synchronously at every dev (re)start, independent of the.vitecache — so this isn't only a cold-cache effect.At scale vs Next.js (Turbopack)
With this cache in place, vinext's build-time lead over Next.js (Turbopack) widens sharply with route count — same machine, same generated app, clean production builds (median of 3, local wall-clock):
Marginal cost per added route ≈ 3.5 ms (vinext) vs 20 ms (Turbopack). Keeping the route-graph scan near-linear (this PR) is part of why vinext holds its lead as apps grow.
Memory safety
The
WeakMapis keyed on the per-scan matcher clone (likefindSlotSubPagesCache), so entries are released when the scan's matcher is GC'd — nothing accumulates across rebuilds/restarts in a long-lived dev server (a module-levelMapwould leak here; theWeakMap-per-scan-matcher does not).Correctness
readDirEntriesCachedcaches[]only for a missing directory (ENOENT/ENOTDIR) — exactly replacing the priorfs.existsSync(dir)guard — and rethrows any real fault (EACCES,EMFILE/ENFILE, …), matching the original unguardedreaddirSync. A genuine FS error still fails the build loudly rather than silently caching an empty listing and dropping slots. The cache returns exactly the entriesreaddirSyncwould, so the discovered file set and resulting transforms are identical (same module-transform count).app-route-graph,slot,app-optimistic-routing,routing,route-sorting,app-rsc-route-matching,hybrid-route-priority,app-page-route-wiring,pages-router,intercepting-routes-build,app-browser-interception-context: 662 tests.vp lintclean.