Vite dev: SSR serves stale modules after edits — no reload for shared modules, missing file associations, and dev-worker reload() reuses cached evaluations
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 11.2k
- Forks
- 899
- Avg merge
- 2d 24m
- Merged PRs (30d)
- 40
Description
Environment
- nitro: 3.0.260610-beta (also reproduced against current
mainsources) - vite: 8.0.10
- vite-plugin-solid: 3.0.0-next.15 (solid-js 2.0.0-beta.23, SSR via a
ssrservice environment) - Node.js 22 / macOS
- dev runner: node-worker (default)
Reproduction
Any Vite + Nitro app where SSR renders modules that are also part of the client graph (i.e. every route component). With the dev server running:
- Load a page (SSR renders it).
- Edit the route component (or any module shared between client and server envs).
- Reload the browser: the server still renders the old module — hydration keys/markup no longer match the freshly HMR-updated client, producing hydration mismatches (unclaimed server nodes, and in our case a hard
Cannot read properties of null (reading 'nextSibling')crash during hydration). - Only restarting the dev server picks up the change.
This is the same end-user symptom as #4020 (closed, but the fix never landed — reload() on main still doesn't clear the cache) and #4043. Tracing the full chain with NITRO_DEBUG=1 and instrumented hotUpdate hooks turned up three independent gaps, each of which is sufficient to keep SSR stale:
Describe the bug
1. hotUpdate never sends full-reload for shared modules (src/build/vite/plugin.ts)
Modules that also exist in a client environment go into sharedModules and are neither invalidated in the server graph nor do they trigger env.hot.send({ type: "full-reload" }). Editing a route component therefore never reloads the server runner at all. (This is #4043; PR #4044 addressed the invalidation half but was closed unmerged.)
2. modules is empty for server environments anyway — file→module associations are missing
Even if (1) were fixed, the modules array hotUpdate receives for the server environments is empty: environment.moduleGraph.getModulesByFile(file) returns nothing, although the module is present in idToModuleMap under its resolved id (verified: id /abs/path/src/routes/Index.tsx exists while getModulesByFile('/abs/path/src/routes/Index.tsx') is empty). Since Vite's own file-change invalidation also goes through getModulesByFile, the server env's transform cache is never invalidated for edited files either. Modules that enter the graph via the module-runner fetchModule path apparently never get registered in fileToModulesMap.
3. ViteEnvRunner.reload() reuses stale evaluations (src/runtime/internal/vite/dev-worker.mjs)
When a full-reload does reach the dev worker, reload() calls this.runner.import(this.entryPath) without clearing evaluatedModules. fetchModule answers {cache: true} for any module whose transform is populated on the Vite side at fetch time — and during the reload itself, plugins can repopulate transforms for the whole graph (e.g. vite-plugin-solid's ?assets crawl), so the runner keeps the old evaluations for exactly the modules that changed. Observed with NITRO_DEBUG=1: after a reload the entry re-executes, but shared app modules are "fetching"-only (no "executing") and old code keeps rendering. Vite's own full-reload handler calls runner.evaluatedModules.clear() before re-importing; the worker's reload() skips that step. This is exactly the root cause described in #4020, which is closed but unfixed on main.
Additional context
Workaround we're using today: a userland plugin that, for server-consumer environments, matches the changed file against idToModuleMap ids directly (bypassing the broken file association), calls moduleGraph.invalidateAll(), and sends { type: "full-reload" } — combined with patching reload() to clear evaluatedModules. With both in place, edits to shared modules are reflected in SSR within a couple of seconds and hydration is clean again.
A PR with the one-line fix for item (3) follows.
Logs
# NITRO_DEBUG=1, after editing src/routes/Index.tsx and sending full-reload:
[module runner] fetching /Users/.../src/entry-server.tsx
[module runner] executing file:///Users/.../src/entry-server.tsx <- entry re-evaluates
[module runner] fetching /src/App.tsx <- fetched...
[module runner] fetching /src/HtmlDocument.tsx <- ...but never "executing":
{cache: true} keeps old evaluation
# subsequent SSR requests trigger no module activity and render the old component
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with src/build/vite/plugin.ts and src/runtime/internal/vite/dev-worker.mjs, then reproduce a shared-module edit with NITRO_DEBUG=1. Trace file-to-module associations, hotUpdate handling, and reload evaluation state; the fix is done when edited shared SSR modules are invalidated, reloaded, and rendered without stale evaluations or hydration mismatches.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript, vite
- Domain
- backend, build-system, devtools
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 45/100