How does Vite's HMR system work?
How does Vite's HMR system work?
About vitejs/vite · 2 messages · last activity 1 Oct 2026
How Vite's HMR system works
Vite's HMR flows over a WebSocket transport between the dev server and a client runtime (either a browser client or a module-runner-based runtime like SSR). The core logic is shared between the two, with a client-specific and runner-specific "handler" dispatching the same set of payload types.
1. Establishing the connection
In the browser client, client.ts sets up a WebSocket-based transport via createWebSocketModuleRunnerTransport, pointed at a socket host derived from injected build-time constants (__HMR_HOSTNAME__, __HMR_PORT__, __HMR_BASE__, etc.) [packages/vite/src/client/client.ts:L41-L59]. If the initial connection fails and the port was inferred rather than explicitly configured, it falls back to a "direct" socket target (__HMR_DIRECT_TARGET__) [packages/vite/src/client/client.ts:L60-L104]. Connection/disconnection/send are wrapped into a small transport object normalized via normalizeModuleRunnerTransport [packages/vite/src/client/client.ts:L52-L112].
There's also a liveness-check mechanism: waitForSuccessfulPing either listens to document.visibilitychange directly, or — when SharedWorker is available — spins up a blob-based shared worker that pings the server and reports visibility changes across tabs via pingWorkerContentMain [packages/vite/src/client/client.ts:L360-L413] and [packages/vite/src/client/client.ts:L419-L467]. This lets Vite poll for server restart and reload the page once the server is back [packages/vite/src/client/client.ts:L316-L290].
2. Dispatching HMR payloads
Both the browser client and the module runner use a HotPayload-driven switch statement to react to server messages. The module-runner version, createHMRHandlerForRunner, shows the canonical set of cases [packages/vite/src/module-runner/hmrHandler.ts:L7-L88]:
connected– just logs that the client is connected.update– firesvite:beforeUpdate, then for each update of typejs-updateit normalizes paths (stripping the/@id/prefix viaunwrapId) and queues the update throughhmrClient.queueUpdate; CSS updates are explicitly unsupported in runner mode. Finishes withvite:afterUpdate[packages/vite/src/module-runner/hmrHandler.ts:L16-L31].custom– forwards arbitrary custom events to listeners, used for plugin-specific HMR messages [packages/vite/src/module-runner/hmrHandler.ts:L32-L34].full-reload– determines which entry points to clear and re-import (either based on the module that triggered the reload, or all entrypoints), clearsrunner.evaluatedModules, and re-imports each entry point, swallowingERR_OUTDATED_OPTIMIZED_DEPerrors [packages/vite/src/module-runner/hmrHandler.ts:L35-L58].prune– notifies listeners and removes stale module paths viahmrClient.prunePaths[packages/vite/src/module-runner/hmrHandler.ts:L59-L61].error– surfaces server-side errors to listeners and logs them [packages/vite/src/module-runner/hmrHandler.ts:L62-L68].ping– no-op keepalive.
The exhaustive switch ends with a never check to guarantee all HotPayload variants are handled [packages/vite/src/module-runner/hmrHandler.ts:L70-L73].
3. Browser-specific behavior
The browser client (client.ts) implements analogous cases but with DOM-specific effects:
- CSS updates: rather than mutating a
<link>tag'shrefin place, it clones the tag, loads the new stylesheet in parallel, and removes the old tag only after the new one loads/errors — avoiding a flash of unstyled content [packages/vite/src/client/client.ts:L246-L274]. custom+vite:ws:disconnect: if the WebSocket disconnects unexpectedly (and the page isn't unloading), it strips the token from the URL and polls withwaitForSuccessfulPingbefore
Did this answer what you came for?
Want to ask your own question?
Open chat for vitejs/vitePublic Q&A. Generated by RepoPilot from the actual source of vitejs/vite. AI answers can be incomplete or stale — verify before relying on them.