Skip to content

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
cloudflare:mainfrom
hyf0:perf/route-graph-dir-cache
Open

perf(routing): cache app-route-graph directory reads — 10k routes: build −52%, dev cold start −83%#2389
hyf0 wants to merge 1 commit into
cloudflare:mainfrom
hyf0:perf/route-graph-dir-cache

Conversation

@hyf0

@hyf0 hyf0 commented Jun 27, 2026

Copy link
Copy Markdown

What

Adds a per-scan memo for the raw fs.readdirSync(dir, { withFileTypes: true }) calls made while scanning the App Router graph, via a small readDirEntriesCached helper backed by a WeakMap<ValidFileMatcher, Map<string, Dirent[]>> — the same per-scan-matcher pattern already used by findFileProbeCache and findSlotSubPagesCache. discoverParallelSlots and findSlotRootPage now read through it.

Why

discoverInheritedParallelSlots walks every ancestor directory of each route and readdirSyncs it to look for @slot directories. 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. findFileProbeCache already memoizes findFile probes, but not these raw readdirSync/existsSync calls.

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)

routes baseline with cache Δ
10,000 ~86 s ~42 s −45 s (−52%)
1,000 (wall within noise) sys/syscall time −0.27 s (−12%); total CPU −0.32 s

Dev server cold start (launch → root route serving, .vite cache cleared)

routes baseline with cache Δ
10,000 ~101 s ~17 s −84 s (−83%)

The route scan runs at every dev-server (re)start, independent of the optimizeDeps/.vite cache — 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 WeakMap keyed on the per-scan matcher clone (scanMatcher, created fresh in buildAppRouteGraph), identical in lifetime to the existing findFileProbeCache / 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-level Map would leak here, but the WeakMap-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 transient Dirent churn the un-cached repeated reads produced.

Correctness

  • readDirEntriesCached returns [] for missing/unreadable directories, preserving the prior existsSync-guard and try/catch semantics of discoverParallelSlots / findSlotRootPage.
  • Route/slot suites pass — app-route-graph, slot, app-optimistic-routing, app-rsc-route-matching, hybrid-route-priority, app-page-route-wiring, pages-router: 564 tests.
  • vp lint clean.

(The 10k stress fixture was generated with the opt-in VINEXT_BENCH_EXTRA_ROUTES knob from #2388.)

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.
@hyf0 hyf0 marked this pull request as ready for review June 27, 2026 19:13
@hyf0 hyf0 changed the title perf(routing): memoize directory reads during app route graph scan perf(routing): cache app-route-graph directory reads — 10k routes: build −52%, dev cold start −83% Jun 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant