Skip to content

perf(metadata): cache scanMetadataFiles to skip the redundant 2nd per-build app-tree walk#2394

Merged
james-elicx merged 3 commits into
cloudflare:mainfrom
hyf0:perf/cache-metadata-scan
Jun 28, 2026
Merged

perf(metadata): cache scanMetadataFiles to skip the redundant 2nd per-build app-tree walk#2394
james-elicx merged 3 commits into
cloudflare:mainfrom
hyf0:perf/cache-metadata-scan

Conversation

@hyf0

@hyf0 hyf0 commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

No dependencies — reviewable and mergeable on its own. Touches only server/metadata-routes.ts + index.ts.

What

Memoizes scanMetadataFiles(appDir) per appDir (mirroring app-router's cachedGraph), and invalidates it alongside the route graph when app files change in dev.

Why

scanMetadataFiles walks the entire app tree with recursive fs.readdirSync to find file-based metadata routes (favicon, icon, opengraph-image, sitemap, robots, manifest, …). It is called from load(RESOLVED_RSC_ENTRY) (index.ts), which fires once per build pass that roots at the RSC entry — measured at exactly 2× per vinext build on the 5000-route fixture — and it had no cache, so the whole tree was walked twice. The sibling call right next to it, appRouter(...), is already memoized via cachedGraph; this just gives the metadata scan the same treatment.

Measured

Direct timing of scanMetadataFiles on the 5000-route fixture (5,037 dirs):

call before after
1st (miss) ~100 ms ~100 ms
2nd (per build) ~100 ms ~0 ms

So it removes one full app-tree walk per build: ~100 ms at 5,000 routes, scaling ~linearly with directory count (~200 ms at 10,000). Modest, but it's provably-redundant work and the change is tiny.

Correctness

  • The cache is invalidated in invalidateAppRoutingModules() — the same path the route-graph cache uses. Metadata-file add/remove already routes there via shouldInvalidateAppRouteFileisMetadataRouteFile, so a new favicon/sitemap/opengraph-image in dev clears the cache and re-scans (no staleness).
  • Metadata files don't change within a build, so the per-build cache is always correct there.
  • Callers treat the result read-only (no push/sort/splice), so returning the cached array is safe — same as cachedGraph.
  • Metadata suites pass — metadata-routes, file-based-metadata, app-router-metadata-routes, metadata-route-build-data, metadata-route-response, streaming-metadata: 174 tests. vp lint clean.

@pkg-pr-new

pkg-pr-new Bot commented Jun 28, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@vinext/cloudflare@2394
npm i https://pkg.pr.new/vinext@2394

commit: cea2b29

@github-actions

github-actions Bot commented Jun 28, 2026

Copy link
Copy Markdown
Contributor

Performance benchmarks

Compared 50c18c8 against base 91c9c18 using alternating same-runner rounds. Next.js was unchanged and skipped.

0 improved · 0 regressed · 6 within ±1.5%

Scenario Framework Baseline Current Change
Client bundle size (gzip) vinext 122.0 KB 122.0 KB ⚫ +0.0%
Client entry size (gzip) vinext 117.0 KB 117.0 KB ⚫ +0.0%
Dev server cold start vinext 2.55 s 2.53 s ⚫ -0.7%
Production build time vinext 3.06 s 3.04 s ⚫ -0.7%
RSC entry closure size (gzip) vinext 94.3 KB 94.3 KB ⚫ -0.0%
Server bundle size (gzip) vinext 160.0 KB 160.0 KB ⚫ -0.0%

View detailed results and traces

🟢 improvement · 🔴 regression · ⚫ change below 1.5% · paired base/head

scanMetadataFiles folds `*.alt.txt` sidecars into a static social image
route's altFilePath via an existsSync probe, but isMetadataRouteFile did
not treat them as route files. With the new per-appDir scan cache, adding
or removing an alt sidecar in dev no longer triggered invalidation, so
the cached scan kept a stale altFilePath. Match the sidecars in the dev
invalidation predicate so the metadata cache clears and re-scans.
@james-elicx james-elicx enabled auto-merge (squash) June 28, 2026 19:27
@james-elicx james-elicx force-pushed the perf/cache-metadata-scan branch from 50c18c8 to cea2b29 Compare June 28, 2026 19:27
@james-elicx james-elicx merged commit 2b61f3f into cloudflare:main Jun 28, 2026
48 checks passed
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.

2 participants