Hot Module Replacement (HMR) — механизм динамической замены модулей во время работы приложения без полной перезагрузки страницы. Технология позволяет обновлять изменённые части кода в уже запущенном приложении, сохраняя текущее состояние интерфейса, данных и окружения.
В экосистеме Webpack HMR применяется прежде всего в процессе разработки. После изменения файла браузер не выполняет полноценный reload страницы, а получает только обновлённый модуль и внедряет его в текущий runtime.
Основные преимущества:
Без HMR любое изменение требует полной перезагрузки страницы:
Изменение файла → новая сборка → reload страницы
С HMR процесс выглядит иначе:
Изменение файла → пересборка модуля → передача update → замена модуля в runtime
HMR состоит из нескольких компонентов:
Webpack отслеживает изменения файлов через watcher файловой системы. После изменения запускается инкрементальная пересборка.
Compiler определяет:
Результатом становятся специальные update-файлы.
Runtime внедряется в bundle автоматически.
Он отвечает за:
Наиболее часто HMR используется вместе с:
webpack-dev-serverwebpack-hot-middlewarewebpack-plugin-serveDev Server:
Между браузером и dev server создаётся постоянное соединение через WebSocket.
Сервер отправляет событие:
{
"type": "hash",
"data": "newCompilationHash"
}
После этого runtime понимает, что появилась новая версия сборки.
Полный цикл HMR выглядит следующим образом.
Например:
// math.js
export function sum(a, b) {
return a + b;
}
Разработчик изменяет код:
export function sum(a, b) {
return Number(a) + Number(b);
}
Webpack запускает частичную пересборку.
Компилятор определяет:
Webpack создаёт специальные файлы:
main.3f5c.hot-update.js
main.3f5c.hot-update.json
JSON-файл содержит список обновлённых chunk.
JS-файл содержит новый код модулей.
Dev server отправляет уведомление через WebSocket:
[HMR] bundle updated
Runtime получает новый hash и начинает проверку обновлений.
Runtime загружает:
*.hot-update.json
*.hot-update.js
После этого начинается применение обновления.
Webpack заменяет старый модуль новым в runtime-модульном графе.
Старый код удаляется:
modules[moduleId] = newModuleFactory;
После этого модуль может быть переисполнен.
Главный API доступен через объект:
module.hot
Этот объект существует только при включённом HMR.
if (module.hot) {
console.log('HMR enabled');
}
Метод accept сообщает Webpack, что модуль умеет
принимать hot-update.
Пример:
if (module.hot) {
module.hot.accept();
}
Без accept Webpack обычно выполняет полную перезагрузку страницы.
if (module.hot) {
module.hot.accept('./math.js', () => {
console.log('math module updated');
});
}
После изменения math.js callback будет вызван
повторно.
import { sum } from './math';
console.log(sum(2, 3));
if (module.hot) {
module.hot.accept('./math', () => {
const updated = require('./math');
console.log(updated.sum(10, 20));
});
}
Позволяет выполнить очистку перед заменой модуля.
Пример:
if (module.hot) {
module.hot.dispose(() => {
console.log('cleanup');
});
}
Dispose особенно важен для:
Dispose может сохранять данные:
if (module.hot) {
module.hot.dispose((data) => {
data.counter = counter;
});
if (module.hot.data) {
counter = module.hot.data.counter;
}
}
module.hot.data содержит данные предыдущей версии
модуля.
Self-accepted modules обновляют сами себя.
if (module.hot) {
module.hot.accept();
}
Такие модули не требуют участия родительских модулей.
Граница обновления — модуль, который принимает изменения через
accept.
Например:
App
├── Header
├── Sidebar
└── Button
Если изменяется Button, Webpack ищет ближайший
accept-handler.
Если Button не принимает обновление:
Sidebar;App;HMR использует всплытие обновлений вверх по графу зависимостей.
child → parent → parent → root
Если никто не принимает обновление:
[HMR] Update declined
[HMR] Reloading page
Минимальная конфигурация:
const webpack = require('webpack');
module.exports = {
mode: 'development',
devServer: {
hot: true
},
plugins: [
new webpack.HotModuleReplacementPlugin()
]
};
В Webpack 5 webpack-dev-server часто автоматически
включает plugin.
При использовании:
devServer: {
hot: true
}
Webpack:
Изменение → reload страницы
Недостатки:
Изменение → замена модуля
Сохраняются:
CSS — один из лучших сценариев для HMR.
Пример:
import './styles.css';
После изменения CSS:
<style>;style-loader автоматически поддерживает HMR.
module: {
rules: [
{
test: /\.css$/,
use: [
'style-loader',
'css-loader'
]
}
]
}
С извлечением CSS ситуация сложнее.
В production CSS выносится в отдельные файлы:
MiniCssExtractPlugin.loader
HMR для extracted CSS работает ограниченно и зависит от loader/runtime.
React активно использует HMR.
Современный стандарт:
React Fast Refresh
Fast Refresh:
Пример настройки:
const ReactRefreshWebpackPlugin =
require('@pmmmwh/react-refresh-webpack-plugin');
module.exports = {
mode: 'development',
devServer: {
hot: true
},
plugins: [
new ReactRefreshWebpackPlugin()
]
};
Обычный HMR обновляет JS-модули.
Fast Refresh дополнительно:
Во Vue механизм встроен глубоко в runtime.
Поддерживаются:
При изменении template:
<template>
<button>Save</button>
</template>
Vue обновляет render function без reload.
Svelte HMR работает через специальные адаптеры:
Особенность:
Angular historically хуже интегрирован с HMR.
Часто используются:
@angular-devkitПример:
ng serve --hmr
Главная проблема HMR — состояние приложения.
Например:
const state = {
counter: 0
};
После замены модуля состояние может быть потеряно.
Для сохранения состояния используются:
Store обычно выносится отдельно:
export const store = configureStore(...);
Reducer может обновляться динамически:
if (module.hot) {
module.hot.accept('./reducers', () => {
store.replaceReducer(newReducer);
});
}
HMR подходит не для всех сценариев.
Плохо обновляются:
Опасный пример:
window.addEventListener('resize', handler);
После нескольких hot-update listener может дублироваться.
Правильный вариант:
window.addEventListener('resize', handler);
if (module.hot) {
module.hot.dispose(() => {
window.removeEventListener('resize', handler);
});
}
Неправильная очистка приводит к:
Частая ошибка:
const socket = new WebSocket(url);
После hot-update создаются новые подключения.
Корректная очистка:
if (module.hot) {
module.hot.dispose(() => {
socket.close();
});
}
Проблемный код:
setInterval(update, 1000);
После каждого update появляется новый timer.
Правильный вариант:
const timer = setInterval(update, 1000);
if (module.hot) {
module.hot.dispose(() => {
clearInterval(timer);
});
}
Webpack хранит граф модулей:
entry
├── app.js
│ ├── ui.js
│ └── api.js
При обновлении:
Файл:
main.hash.hot-update.json
Содержит:
{
"c": ["main"],
"h": "newHash"
}
Где:
c — обновлённые chunk;h — новый hash.Файл:
main.hash.hot-update.js
Содержит новые module factories.
Пример:
self["webpackHotUpdate"]("main", {
"./src/math.js": function(...) {
}
});
Webpack внедряет runtime примерно такого типа:
function hotCheck() {
}
function hotApply() {
}
Runtime:
Runtime использует внутренние состояния:
idle
check
prepare
dispose
apply
abort
fail
Получить статус:
module.hot.status();
Подписка на изменения:
module.hot.addStatusHandler(status => {
console.log(status);
});
Ошибка применения update:
[HMR] Update failed
Причины:
Модуль может отказаться от обновлений:
module.hot.decline('./module');
В таком случае выполняется reload.
Принудительная инвалидизация:
module.hot.invalidate();
Webpack помечает модуль как устаревший и инициирует новый update cycle.
Если HMR невозможен:
Cannot apply update
Webpack автоматически делает:
window.location.reload()
Webpack 5 поддерживает lazy compilation.
Модули собираются только при обращении:
experiments: {
lazyCompilation: true
}
HMR при этом продолжает работать, но логика update становится сложнее.
При динамическом импорте:
import('./module');
Webpack создаёт отдельный chunk.
HMR способен обновлять:
Для корректной отладки HMR важны source maps.
Оптимальные варианты:
devtool: 'eval-source-map'
или:
devtool: 'cheap-module-source-map'
Скорость HMR зависит от:
Наиболее важные техники:
cache: {
type: 'filesystem'
}
Параллельная обработка:
use: [
'thread-loader',
'babel-loader'
]
exclude: /node_modules/
devtool: 'eval-cheap-module-source-map'
Babel сам по себе не реализует HMR.
Но он участвует через:
TypeScript поддерживает HMR через обычный JS-runtime.
Пример:
if (module.hot) {
module.hot.accept();
}
Для типизации:
declare const module: {
hot?: any;
};
Webpack поддерживает HMR и для ES Modules.
Однако внутренний runtime всё ещё использует собственную систему модулей.
Нативные ESM браузера не имеют встроенного HMR.
Поэтому bundlers реализуют его самостоятельно:
socket.connect();
После update:
connect
connect
connect
После update state сбрасывается.
Причины:
Иногда runtime остаётся в состоянии:
prepare
Причины:
Полезные сообщения:
[HMR] Waiting for update signal
[HMR] Checking for updates
[HMR] Updated modules
[HMR] App is up to date
export function multiply(a, b) {
return a * b;
}
import { multiply } from './math';
function render() {
console.log(multiply(2, 5));
}
render();
if (module.hot) {
module.hot.accept('./math', () => {
console.log('math updated');
render();
});
}
После изменения math.js: