How does Vite's HMR system work?
How does Vite's HMR system work?
About vitejs/vite · 2 messages · last activity 7/26/2026
Based on the retrieved source, Vite's HMR (Hot Module Replacement) system works through a client-server WebSocket connection that pushes update payloads to the browser (or module runner), which then decides how to apply them. Here's the flow:
1. Connection setup
The browser client establishes a WebSocket connection to the dev server. It builds a socket host from injected globals (__HMR_HOSTNAME__, __HMR_PORT__, __HMR_BASE__, etc.) and creates a transport via createWebSocketModuleRunnerTransport [packages/vite/src/client/client.ts:L41-L80]. There's a fallback mechanism: if the inferred port fails to connect, it retries using a directSocketHost target, but only when the port was inferred (to avoid confusing behavior) [packages/vite/src/client/client.ts:L60-L80].
2. HMR Client abstraction
A shared HMRClient (from ../shared/hmr) is instantiated with a logger and the transport, plus an importUpdatedModule function that differs depending on mode:
- In bundle mode (
isBundleMode/Rolldown dev runtime), it does a dynamicimport()and then callsglobalThis.__rolldown_runtime__.loadExports(acceptedPath)[packages/vite/src/client/client.ts:L144-L158]. - In normal mode, it constructs a URL with a cache-busting timestamp query (
?t=${timestamp}) and imports it directly [packages/vite/src/client/client.ts:L159-L177].
Both paths handle circular imports: if isWithinCircularImport is true and the import fails, it logs guidance to run vite --debug hmr and triggers a debounced full page reload via pageReload() [packages/vite/src/client/client.ts:L149-L156, L169-L176].
3. Message handling — payload types
The transport connects with createHMRHandler(handleMessage) [packages/vite/src/client/client.ts:L180], and handleMessage/the runner equivalent switches on payload.type:
connected— just logs a debug message [packages/vite/src/client/client.ts:L208-L210; packages/vite/src/module-runner/hmrHandler.ts:L13-L15].update— firesvite:beforeUpdatelisteners, then processes each update:js-update: queued viahmrClient.queueUpdate(update)for module-level hot replacement [packages/vite/src/client/client.ts:L228-L231].css-update: finds the matching<link>tag by URL and swaps it (browser-only, not supported in the module runner, which just logs an error) [packages/vite/src/client/client.ts:L233-L245; packages/vite/src/module-runner/hmrHandler.ts:L20-L22].- Special case: if it's the very first update and an error overlay is already showing (meaning the page loaded with a compile error), it forces a full
location.reload()instead of a normal patch [packages/vite/src/client/client.ts:L213-L220].
full-reload— firesvite:beforeFullReload, then:- In the browser: reloads the page, with special handling for
.htmlfiles (only reload if the current page path matches the edited HTML file) [packages/vite/src/client/client.ts:L292-L309]. - In the module runner: clears
runner.evaluatedModulesand re-imports all entrypoints (either the entrypoints reachable from the file that triggered the change, or all entrypoints) [packages/vite/src/module-runner/hmrHandler.ts:L36-L58].
- In the browser: reloads the page, with special handling for
custom— dispatches to custom event listeners viahmrClient.notifyListeners(payload.event, payload.data), supporting plugin-defined HMR events [packages/vite/src/module-runner/hmrHandler.ts:L30-L32].prune— callshmrClient.prunePaths(payload.paths)to clean up modules no longer part of the graph [packages/vite/src/client/client.ts:L312-L314; packages/vite/src/module-runner/hmrHandler.ts:L60-L62].error— notifiesvite:errorlisteners and either shows an error overlay (browser) or logs the error (module runner) [packages/vite/src/client/client.ts:L316-L325; packages/vite
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.