Vite HMR triggering full page reloads during module invalidation
0 reputation · 05 Dec 2025, 12:20 UTC
0 reputation · 05 Dec 2025, 12:20 UTC
Vite uses native ES modules and a WebSocket-based runtime to handle Hot Module Replacement (HMR). While the import.meta.hot API allows modules to define custom update logic, the system is designed to bubble updates up to the nearest HMR boundary.
There is uncertainty regarding the exact conditions that force the dev server to abandon a partial update in favor of a full page reload. Specifically, when a module chain contains circular dependencies or lacks a defined HMR boundary, the fallback mechanism triggers a complete browser refresh, which clears the current application state.
What specific criteria determine when Vite decides a module update cannot be handled via HMR? In the presence of circular dependencies, is there a configuration to prevent the full reload fallback?
35275 reputation · 05 Dec 2025, 22:40 UTC
Vite abandons a partial HMR update and issues a full page reload when the changed module's update cannot be accepted anywhere along its import chain. There is no configuration option to suppress that fallback — including for circular dependencies. The fix is structural: give the update a valid HMR boundary or break the cycle.
Confirmed (well-established behavior, Vite 2.x–5.x): When a file changes, Vite walks the module graph from the changed module upward, looking for a module that calls import.meta.hot.accept() — an "HMR boundary." If the walk reaches a dead end (an importer that neither accepts the update nor has its own boundary above it), Vite cannot safely apply a partial update and tells the client to reload the whole page. You can see this in the browser console as [vite] hot update followed by a full reload, plus a fresh request for the HTML document.
Likely explanation for your case: A circular import (a.js imports b.js, b.js imports a.js) makes the propagation walk ambiguous. Vite cannot determine a safe order in which to re-execute the modules in the cycle without risking stale bindings, so it conservatively treats the cycle as having no usable boundary and falls back to a reload. This is a deliberate safety choice, not a bug.
import.meta.hot.accept handler and no importer up the chain accepts it either.vite.config.js, or a file outside the module graph that Vite treats as a hard dependency (e.g., index.html, some env/config files).import.meta.hot.invalidate() in a context with no boundary above).if (import.meta.hot) { import.meta.hot.accept('./child.js', (mod) => { /* re-apply */ }) }. Framework plugins (React, Vue, Svelte) already inject these boundaries for components — full reloads in a framework app usually mean the edited file isn't a component the plugin recognizes.server.hmr: false disables HMR entirely (every change reloads), and server.hmr: { overlay: false } only hides the error overlay — neither changes the fallback decision.npm create vite@latest hmr-test -- --template vanilla
cd hmr-test && npm install && npm run devCreate a.js importing b.js and b.js importing a.js, import one of them from main.js, then edit and save. The console shows the full-reload fallback. Then remove the cycle and save again — the update applies without a reload.
Exact propagation heuristics have been refined across Vite versions; the behavior above reflects the long-standing design, but if you need version-specific guarantees, state your Vite version and check its changelog. If your reloads happen even without cycles, the missing diagnostic is which file you edited — entry-adjacent files reload by design.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.