perf(routing): cache app-route-graph directory reads — 10k routes: build −52%, dev cold start −83%#2389
Open
hyf0 wants to merge 1 commit into
Open
perf(routing): cache app-route-graph directory reads — 10k routes: build −52%, dev cold start −83%#2389hyf0 wants to merge 1 commit into
hyf0 wants to merge 1 commit into
Conversation
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.
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
Adds a per-scan memo for the raw
fs.readdirSync(dir, { withFileTypes: true })calls made while scanning the App Router graph, via a smallreadDirEntriesCachedhelper backed by aWeakMap<ValidFileMatcher, Map<string, Dirent[]>>— the same per-scan-matcher pattern already used byfindFileProbeCacheandfindSlotSubPagesCache.discoverParallelSlotsandfindSlotRootPagenow read through it.Why
discoverInheritedParallelSlotswalks every ancestor directory of each route andreaddirSyncs it to look for@slotdirectories. Because routes share ancestors, a directory with N sibling routes is read once per descendant route — the scan is super-linear (≈ O(routes × ancestor-dir-size)) in the width of a route directory.findFileProbeCachealready memoizesfindFileprobes, but not these rawreaddirSync/existsSynccalls.The App Router graph is scanned both during production build and at dev-server startup, so this affects both.
Measured
Stress fixture with a wide/flat route directory (worst case for this pathology), Apple M-series:
Production build (
vinext build)Dev server cold start (launch → root route serving,
.vitecache cleared)The route scan runs at every dev-server (re)start, independent of the
optimizeDeps/.vitecache — so the route-scan portion of this win applies to warm restarts too, not only cleared-cache cold starts. Output is byte-identical (29,578 module-transforms with and without the cache). The win scales super-linearly with route-directory width: negligible for a handful of routes, dramatic for apps with wide route directories.Memory safety
The cache is a
WeakMapkeyed on the per-scan matcher clone (scanMatcher, created fresh inbuildAppRouteGraph), identical in lifetime to the existingfindFileProbeCache/findSlotSubPagesCache. Its entries are released as soon as the scan's matcher is garbage-collected, so nothing accumulates across rebuilds/restarts in a long-lived dev server — a module-levelMapwould leak here, but theWeakMap-per-scan-matcher does not. Within a single scan the retained set is bounded by the app's directory count, and it replaces the much larger transientDirentchurn the un-cached repeated reads produced.Correctness
readDirEntriesCachedreturns[]for missing/unreadable directories, preserving the priorexistsSync-guard andtry/catchsemantics ofdiscoverParallelSlots/findSlotRootPage.app-route-graph,slot,app-optimistic-routing,app-rsc-route-matching,hybrid-route-priority,app-page-route-wiring,pages-router: 564 tests.vp lintclean.(The 10k stress fixture was generated with the opt-in
VINEXT_BENCH_EXTRA_ROUTESknob from #2388.)