Skip to content

perf(void): reuse unchanged source analysis during dev rescans - #543

Draft
yangshun wants to merge 4 commits into
voidzero-dev:mainfrom
yangshun:perf/incremental-source-analysis
Draft

yangshun wants to merge 4 commits into
voidzero-dev:mainfrom
yangshun:perf/incremental-source-analysis

Conversation

@yangshun

@yangshun yangshun commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Issue

Saving a page currently re-reads and analyzes unchanged source files, then regenerates routing output even when only the page body changed. This adds unnecessary work as the app grows.

Fix

This change caches file-derived analysis, invalidates edited files through the watcher, and skips route-entry regeneration and route-module invalidation when routing inputs remain unchanged. Recent writes are conservatively re-analyzed to preserve freshness. Failed generation or module invalidation keeps the routing checkpoint pending so a later save retries, including forced updates with unchanged routing facts.

The implementation is shared across React, Vue, Svelte, and Solid Pages. The benchmarks below measure React only.

Results

Measured from file write to the updated heading appearing in Chrome, using Cloudflare dev with local workerd on an Apple M5:

Generated pages Update Before After Time saved Change
1 Component HMR 97.0 ms 96.4 ms 0.6 ms −0.7%
1 Server-page reload 132.9 ms 142.9 ms −10.0 ms +7.5%
100 Component HMR 95.2 ms 96.8 ms −1.6 ms +1.7%
100 Server-page reload 119.5 ms 141.6 ms −22.2 ms +18.5%
1,000 Component HMR 350.1 ms 98.6 ms 251.5 ms −71.8%
1,000 Server-page reload 329.8 ms 167.2 ms 162.6 ms −49.3%

Each fixture includes two additional fixed pages. Results average session medians across ten paired sessions per size, with 360 timed saves overall and seeded, sequential variant ordering after warmup.

Both 1,000-page improvements are statistically significant after Holm correction across six comparisons (p = 0.0117). The 95% savings intervals are 235–268 ms for HMR and 123–197 ms for reloads. Smaller fixtures show no statistically clear improvement or regression.

Validation:

  • 158 focused tests and workspace typechecking.

  • Four three-page Cloudflare SSR/SSG builds.

  • Browser checks confirming visible updates, preserved component state, fresh documents for server-page reloads, and no runtime errors.

  • Regression tests cover retry after generated-file write failures, forced updates with unchanged routing facts, and module invalidation failures. Each case also verifies that successful recovery restores the body-only save fast path.

Optional runtime availability probes now time out after 10 seconds so an unresponsive runtime cannot hang test discovery.

Verification of the retry follow-up: all 21 routing tests, workspace typechecking (33 tasks), and the affected suite (10,789 tests) passed. The full suite passed 14,226 tests but hit the Astro dev server's 20-second startup deadline; all eight Astro tests passed on an isolated rerun. Optional Bun and Deno coverage was skipped because those runtimes were unavailable or unresponsive.

yangshun and others added 4 commits October 21, 2026 20:59
## Problem

Every file event in a source convention directory reran the whole
application analysis. Binding inference and the page scanner each read
every source file synchronously, and the page scanner parsed every
`*.server.ts` with Oxc again. In a 5,000-page app (the many-pages
benchmark), one rescan made 20,006 `readFileSync` calls, two per page
file, and took about 800 ms on an idle machine.

After the rescan, the routing plugin regenerated `virtual:void-routes`
even when the save changed nothing the scan reports. That module lists
every page, so Vite re-transformed and re-ran it on every save.

In a CPU profile of `vp dev` while editing one page of that app, the
rescan took 1.0-1.6 s of each 2.0-2.7 s HMR update, and re-transforming
and re-running the route table took most of the rest. The benchmark's
HMR time grew from 209 ms at 100 pages to 2.21 s at 5,000.

The React adapter's pages and islands plugins also scan pages on their
own when files are added or removed and at build start, so the same
files were read and parsed several times.

## Fix

- `deriveFromSource` (`shared/source-analysis-cache.ts`) caches values
  derived from a file's contents, keyed by path and analysis kind, and
  reuses them while the file's inode, size, mtime and ctime are
  unchanged. Binding inference, page server metadata, component
  `layout` exports and Markdown frontmatter go through it. The cache
  is module-level, so the core plugin and the framework adapters share
  it.
- Coarse file system timestamps and inode reuse can leave a rewritten
  file's stat unchanged. As Git does for racily clean index entries, a
  file that changed within the last 3 s is always read again, and the
  dev watcher drops a file's entries on every event.
- On a `change` event, the routing plugin compares the scan before and
  after the rescan. When nothing changed, it skips regenerating route
  types and entries and leaves `virtual:void-routes` alone; Vite still
  invalidates the saved module. Adds, deletes, auth config, env files
  and SSR entries keep the previous behavior.

## Impact

Measured on the 5,000-page benchmark app:

- A warm rescan reads 2 files instead of 20,006 and takes about 100 ms
  instead of 530-640 ms.
- HMR after editing a page (median of five edits, two runs each):
  1,166 and 1,216 ms before, 299 and 248 ms after. The benchmark
  measured 209 ms at 100 pages, so HMR now barely grows with the page
  count.
- Loader and route handler edits also reach the next SSR response
  sooner: about 1.6 s to 0.5 s for a loader, and 0.8-1.2 s to 0.1 s for
  a route handler.

## Verification

- New `source-analysis-cache.test.ts`. `pages-scan.test.ts` covers
  metadata edits between cached scans and fails if the cache ignores
  the file signature. `plugin-routing.test.ts` covers saves that do
  and do not change scanned facts.
- `vp run check` (33/33, after generating playground codegen in a
  fresh worktree).
- `vp run test --base origin/main`: 511 affected files pass, including
  the Pages HMR integration suites.
- On `vp dev` with the 5,000-page app, edits to a page component, its
  loader and a route handler all reach the next SSR response.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Remove redundant cache guidance from AGENTS.md while keeping the implementation unchanged.
Keep the generated routing checkpoint pending until writes and module invalidation succeed, including forced updates with unchanged scan facts. Cover write failures, forced updates, invalidation failures, and recovery to the body-only save fast path.
Stop an unresponsive runtime version probe after ten seconds so test discovery can skip unavailable optional runtimes instead of hanging before Vitest timeouts apply.

This branch has not been deployed

No deployments
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