RepoPilot

How does Vite's HMR system work?

About vitejs/vite · 2 messages · last activity 7/26/2026

Question

How does Vite's HMR system work?

RepoPilot

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 dynamic import() and then calls globalThis.__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 — fires vite:beforeUpdate listeners, then processes each update:
    • js-update: queued via hmrClient.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 — fires vite:beforeFullReload, then:
    • In the browser: reloads the page, with special handling for .html files (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.evaluatedModules and 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].
  • custom — dispatches to custom event listeners via hmrClient.notifyListeners(payload.event, payload.data), supporting plugin-defined HMR events [packages/vite/src/module-runner/hmrHandler.ts:L30-L32].
  • prune — calls hmrClient.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 — notifies vite:error listeners 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/vite

Public Q&A. Generated by RepoPilot from the actual source of vitejs/vite. AI answers can be incomplete or stale — verify before relying on them.