Hot Module Replacement (HMR)
Learn how Hot Module Replacement (HMR) swaps modules at runtime without a full reload, how it works, which bundlers support it, and best practices.
What is Hot Module Replacement (HMR)?
Hot Module Replacement (HMR) is a development-server feature that swaps, adds, or removes JavaScript and CSS modules in a running application without a full page reload. When you save a file, only the changed module and its immediate dependents are re-evaluated and injected into the live page, while the rest of the application keeps running. The result is a near-instant feedback loop: you see the effect of an edit in a fraction of a second, and the application state (open modals, form input, router position, in-memory data) usually survives the update.
HMR is a build-time and dev-server concept, not something that ships to production. It exists to make the inner development loop faster and less disruptive. Because it preserves state and avoids the flash of a full reload, HMR keeps developers in flow and shortens the time between writing code and seeing the outcome. That short feedback loop is one of the quiet forces behind good web performance work: when trying a layout change or a rendering tweak is cheap, you experiment more, and you catch regressions earlier.
Why HMR exists: full reload vs live reload vs HMR
Before HMR, the common workflow was a full page reload on every save. A full reload throws away the entire page, re-downloads and re-parses every asset, re-runs all startup code, and resets every piece of state. On a large single-page application that startup can take several seconds, and any state you carefully set up to reproduce a bug is gone.
Live reload improved on manual refreshing by automatically reloading the browser tab when a file changed, but it is still a full reload underneath, so state is still lost. HMR goes a step further: instead of reloading the tab, it patches only the modules that changed. A CSS edit updates styles with no reload at all; a component edit re-renders just that component and its subtree. This is why HMR feels qualitatively different from live reload, not just faster.
How HMR works under the hood
HMR relies on a few cooperating pieces that the bundler and dev server wire together for you.
The HMR runtime
When you run a dev build, the bundler injects a small HMR runtime into the page. This runtime keeps a map of the modules that make up the application and knows how to replace an individual module in memory at runtime.
The update channel
The dev server watches your source files. When a file changes, it recompiles the affected module and pushes an update notification to the browser, typically over a websocket connection. The runtime then fetches the new module code and prepares to apply it.
Module boundaries and the accept and dispose API
HMR needs to know how far an update should propagate. Each module can declare that it is willing to accept a new version of itself or of its dependencies using an HMR API, commonly exposed as import.meta.hot (in Vite and the ES module world) or module.hot (in Webpack). A module calls accept to say I can be swapped without a reload, and it can register a dispose handler to clean up side effects (timers, event listeners, subscriptions) before the old version is discarded. If a changed module does not accept the update, the runtime walks up the dependency graph looking for an ancestor that does. If it reaches the entry point without finding an accepting boundary, HMR gives up and falls back to a full reload.
Which bundlers and frameworks support HMR
HMR is a standard feature of modern JavaScript tooling.
- Webpack popularized HMR for the wider ecosystem and exposes it through the
module.hotAPI and the HotModuleReplacementPlugin. - Vite provides very fast HMR built on native ES modules and the
import.meta.hotAPI, updating modules without rebundling the whole app. - Parcel ships HMR with zero configuration for most projects.
- esbuild and Rollup based setups add HMR through plugins or the dev servers built on top of them.
On top of the bundler layer, frameworks add state-aware HMR integrations. React Fast Refresh re-renders edited components while keeping their hooks state where possible. Vue has first-class single-file-component HMR. Frameworks like Svelte, Angular, and SolidJS ship their own HMR handling so that editing a component updates the view without wiping the whole tree.
State preservation
The headline benefit of HMR is that it keeps application state across an edit. If you are three steps into a checkout flow and tweak the styling of the payment form, HMR updates the form in place and you stay on that step. Framework integrations extend this to component-level state: React Fast Refresh, for example, tries to preserve a component's useState and useRef values as long as its signature is compatible. This preservation is best-effort, not a guarantee. Editing a file that holds module-level state, changing a component's exported shape, or touching code that runs at import time can force the runtime to reset that module or trigger a full reload.
HMR compared
| Aspect | HMR | Live reload | Full reload |
|---|---|---|---|
| State preserved | Usually yes | No | No |
| Scope of update | Changed module and dependents | Whole page | Whole page |
| Speed of feedback | Fastest | Medium | Slowest |
| Setup effort | Built into modern tooling | Minimal | None |
| Reliability on any change | Falls back to reload when needed | Always works | Always works |
Limitations and common gotchas
HMR is powerful but not magic. A few situations regularly cause confusion.
- Non-accepting modules force a reload. If a changed module has no accepting boundary above it, you get a full reload and lose state anyway.
- Side effects need disposal. Modules that start timers, open connections, or add global listeners must clean up in a
disposehandler, or each edit leaks another instance and behavior drifts. - Module-level state is fragile. Values stored at module scope may reset on update, so state kept there can behave inconsistently during a session.
- Stale closures and duplicated singletons can appear when only part of the graph is swapped.
- HMR is dev-only. It never reflects production performance, so timings you see with HMR active are not representative of what users experience.
Best practices for reliable HMR
- Keep modules focused and free of import-time side effects so update boundaries stay clean.
- Register
disposehandlers for anything long-lived: intervals, sockets, observers, and event listeners. - Lean on framework integrations like React Fast Refresh or Vue SFC HMR rather than hand-writing accept logic where you can.
- Treat an unexpected full reload as a signal that a module boundary or a side effect needs attention, not just an annoyance.
- Validate real performance separately from the dev loop. HMR shortens the feedback cycle, but final judgments about load time, rendering, and stability belong in production-like builds and load testing, not in the HMR-enabled dev server.
Troubleshooting HMR
When HMR misbehaves, work through it methodically. If edits do nothing, confirm the dev server is running and the websocket update channel is connected (the browser console usually logs HMR connect and update messages). If every edit triggers a full reload, a module in the changed path is not accepting updates, so check for import-time side effects or an unsupported export change. If old behavior lingers after an edit, suspect a leaked side effect from a missing dispose handler and restart the dev server to get a clean baseline. When results still look wrong, a hard refresh clears any stale runtime state so you can tell an HMR issue apart from an application bug.
FAQ about Hot Module Replacement
Is HMR the same as live reload?
No. Live reload refreshes the entire page and loses all state, while HMR swaps only the changed modules and usually keeps application state intact. HMR falls back to a full reload only when it cannot patch a change safely.
Does HMR run in production?
No. HMR is a development-only feature of the dev server and build tooling. Production bundles do not include the HMR runtime, so it never affects the code your users actually run.
Why does my app sometimes do a full reload with HMR enabled?
A full reload happens when a changed module, or any of its ancestors, does not accept the update. The runtime walks up the dependency graph, and if it finds no accepting boundary before the entry point, it reloads the whole page to stay correct.
Which tools support HMR out of the box?
Vite, Webpack, and Parcel all provide HMR, and frameworks add state-aware layers on top such as React Fast Refresh and Vue single-file-component HMR. Most modern setups enable HMR automatically in development.
Does HMR preserve component state?
Often, yes, thanks to framework integrations like React Fast Refresh, which tries to keep a component's local state across edits. Preservation is best-effort, so changing a component's shape or module-level state can still reset it.
Can I rely on HMR timings to judge performance?
No. HMR reflects the development experience, not production performance. Measure real load time, rendering, and stability with production-like builds and dedicated load testing rather than the HMR-enabled dev server.
Related terms
Related LoadFocus Tools
Put this concept into practice with LoadFocus, the same platform that powers everything you just read about.