[GH-ISSUE #786] Dev: RSC HMR updates modules but workerd serves stale module evaluation results #171

Closed
opened 2026-05-06 12:37:51 +02:00 by BreizhHardware · 0 comments

Originally created by @davelindo on GitHub (Apr 7, 2026).
Original GitHub issue: https://github.com/cloudflare/vinext/issues/786

Summary

RSC HMR in dev mode does not reflect file changes. After the first edit post-server-start, subsequent edits are detected by Vite but the dev server continues to serve stale rendered output. A full server restart is required to see changes.

This contradicts the documented behavior in app-rsc-entry.js: "ISR cache is disabled in dev mode — every request re-renders fresh, matching Next.js dev behavior."

Root cause

The issue is in Vite's ModuleRunner (running inside the workerd Durable Object __VITE_RUNNER_OBJECT__ via @cloudflare/vite-plugin), not in vinext's ISR page cache (which is correctly gated by process.env.NODE_ENV === "production").

When a source file changes:

  1. Vite detects the change — logs confirm (ssr) page reload and (rsc) hmr update
  2. The virtual RSC entry (virtual:vinext-rsc-entry) is re-evaluated via import.meta.hot.accept()
  3. But ModuleRunner.cachedRequest() (line ~1100 of module-runner.js) checks mod.evaluated && mod.promise and returns cached evaluation results for transitive dependencies (page components, data files) without re-evaluating them
  4. The js-update HMR path only invalidates the specific changed module and its self-accepting boundary — transitive imports through the module graph are not invalidated

The first edit after server start works because it triggers [vite] program reloadrunner.evaluatedModules.clear() (full cache clear). Subsequent edits only trigger [vite] hot updatedjs-update (partial invalidation that misses transitive deps).

Evidence

Compile timestamps decrease after HMRperformance.now() values go backwards on subsequent requests, confirming stale module evaluations are being served:

After HMR:  200 in 265ms (compile: 1775530058.2s, render: 0ms)   ← fresh evaluation
Then:       200 in   7ms (compile: 1775529975.0s, render: 142ms)  ← stale
Then:       200 in   5ms (compile: 1775529973.0s, render: 142ms)  ← older
Then:       200 in   4ms (compile: 1775529879.4s, render: 142ms)  ← settled on stale

ISR cache eliminated as causereadAppPageCacheResponse in the generated RSC entry is gated by process.env.NODE_ENV === "production" (line ~2059). All isrGet calls are within this guard. The NoOpCacheHandler via instrumentation.ts was also tested and did not resolve the issue, confirming the ISR cache is not involved.

Affects all RSC routes — tested on landing page routes, homepage, and solution pages. The issue is not route-specific.

Reproduction

  1. vinext dev with @cloudflare/vite-plugin (workerd RSC environment)
  2. Load any App Router page
  3. Edit a server-side data file or server component imported by that page
  4. Wait for HMR logs to confirm detection
  5. Request the page — stale content is served
  6. Restart the dev server — fresh content is served

Potential fix direction

The RSC entry's import.meta.hot.accept() handler (set up by @cloudflare/vite-plugin at line ~17765 of index.mjs) could call runner.evaluatedModules.clear() on update to force full re-evaluation of all transitive dependencies, matching the behavior of the full-reload path. This would trade some HMR performance for correctness in dev mode.

Environment

  • vinext: 0.0.39
  • @cloudflare/vite-plugin: 1.31.0
  • @vitejs/plugin-rsc: 0.5.22
  • vite: 8.0.5
  • workerd: 1.20260401.1
Originally created by @davelindo on GitHub (Apr 7, 2026). Original GitHub issue: https://github.com/cloudflare/vinext/issues/786 ## Summary RSC HMR in dev mode does not reflect file changes. After the first edit post-server-start, subsequent edits are detected by Vite but the dev server continues to serve stale rendered output. A full server restart is required to see changes. This contradicts the documented behavior in `app-rsc-entry.js`: *"ISR cache is disabled in dev mode — every request re-renders fresh, matching Next.js dev behavior."* ## Root cause The issue is in Vite's `ModuleRunner` (running inside the workerd Durable Object `__VITE_RUNNER_OBJECT__` via `@cloudflare/vite-plugin`), not in vinext's ISR page cache (which is correctly gated by `process.env.NODE_ENV === "production"`). When a source file changes: 1. Vite **detects** the change — logs confirm `(ssr) page reload` and `(rsc) hmr update` 2. The virtual RSC entry (`virtual:vinext-rsc-entry`) is re-evaluated via `import.meta.hot.accept()` 3. But `ModuleRunner.cachedRequest()` (line ~1100 of `module-runner.js`) checks `mod.evaluated && mod.promise` and **returns cached evaluation results** for transitive dependencies (page components, data files) without re-evaluating them 4. The `js-update` HMR path only invalidates the specific changed module and its self-accepting boundary — transitive imports through the module graph are not invalidated **The first edit after server start works** because it triggers `[vite] program reload` → `runner.evaluatedModules.clear()` (full cache clear). Subsequent edits only trigger `[vite] hot updated` → `js-update` (partial invalidation that misses transitive deps). ## Evidence **Compile timestamps decrease after HMR** — `performance.now()` values go backwards on subsequent requests, confirming stale module evaluations are being served: ``` After HMR: 200 in 265ms (compile: 1775530058.2s, render: 0ms) ← fresh evaluation Then: 200 in 7ms (compile: 1775529975.0s, render: 142ms) ← stale Then: 200 in 5ms (compile: 1775529973.0s, render: 142ms) ← older Then: 200 in 4ms (compile: 1775529879.4s, render: 142ms) ← settled on stale ``` **ISR cache eliminated as cause** — `readAppPageCacheResponse` in the generated RSC entry is gated by `process.env.NODE_ENV === "production"` (line ~2059). All `isrGet` calls are within this guard. The `NoOpCacheHandler` via `instrumentation.ts` was also tested and did not resolve the issue, confirming the ISR cache is not involved. **Affects all RSC routes** — tested on landing page routes, homepage, and solution pages. The issue is not route-specific. ## Reproduction 1. `vinext dev` with `@cloudflare/vite-plugin` (workerd RSC environment) 2. Load any App Router page 3. Edit a server-side data file or server component imported by that page 4. Wait for HMR logs to confirm detection 5. Request the page — stale content is served 6. Restart the dev server — fresh content is served ## Potential fix direction The RSC entry's `import.meta.hot.accept()` handler (set up by `@cloudflare/vite-plugin` at line ~17765 of `index.mjs`) could call `runner.evaluatedModules.clear()` on update to force full re-evaluation of all transitive dependencies, matching the behavior of the `full-reload` path. This would trade some HMR performance for correctness in dev mode. ## Environment - vinext: 0.0.39 - @cloudflare/vite-plugin: 1.31.0 - @vitejs/plugin-rsc: 0.5.22 - vite: 8.0.5 - workerd: 1.20260401.1
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#171
No description provided.