HMR: горячая замена модулей

HMR (Hot Module Replacement) — механизм горячей замены модулей без полной перезагрузки страницы. Во время разработки Vite отслеживает изменения файлов и обновляет только изменённые части приложения. Это резко ускоряет цикл разработки, поскольку браузеру не требуется заново загружать весь документ, пересоздавать DOM и повторно выполнять все модули.

В традиционных сборщиках изменение даже небольшого файла нередко приводило к полной пересборке проекта и перезагрузке страницы. Vite использует нативные ES-модули и отдельный HMR-сервер, благодаря чему обновления происходят практически мгновенно.

Основные преимущества HMR:

  • высокая скорость обновления интерфейса;
  • сохранение состояния приложения;
  • отсутствие полной перезагрузки страницы;
  • уменьшение времени обратной связи;
  • точечное обновление зависимых модулей.

Как работает HMR в Vite

Архитектура HMR в Vite основана на нескольких компонентах:

  1. Dev Server
  2. WebSocket-соединение
  3. Граф модулей
  4. Механизм принятия обновлений
  5. Повторное выполнение изменённых модулей

После запуска dev-сервера браузер устанавливает WebSocket-соединение с сервером Vite. Это соединение используется для передачи уведомлений об изменениях файлов.

Схема работы выглядит следующим образом:

  1. Разработчик изменяет файл.
  2. Vite фиксирует изменение через файловый watcher.
  3. Сервер определяет затронутые модули.
  4. В браузер отправляется HMR-событие.
  5. Браузер загружает только изменённый модуль.
  6. Код модуля повторно выполняется без перезагрузки страницы.

Полная перезагрузка и HMR

Важно различать два механизма:

Механизм Что происходит
Full Reload Полная перезагрузка страницы
HMR Замена отдельных модулей

Если модуль поддерживает HMR, Vite обновляет только его. Если модуль не может быть безопасно обновлён, выполняется full reload.

Например:

console.log('Hello')

Изменение такого файла обычно обрабатывается через HMR.

Однако изменение:

index.html

часто требует полной перезагрузки страницы.


HMR и нативные ES-модули

Vite не создаёт единый bundle в dev-режиме. Каждый модуль загружается браузером отдельно через ES Modules.

Пример:

import { sum } from './math.js'

console.log(sum(2, 3))

При изменении math.js Vite обновит только этот модуль.

Это принципиально отличается от старых систем сборки, где приходилось пересобирать большой bundle.


Граф модулей

Vite поддерживает внутренний граф зависимостей.

Например:

main.js
 ├── app.js
 │    ├── ui.js
 │    └── api.js
 └── styles.css

Если изменяется ui.js, сервер понимает:

  • какие модули импортируют ui.js;
  • какие компоненты должны обновиться;
  • где можно остановить распространение обновления.

Такой подход минимизирует количество повторных загрузок.


HMR Boundary

HMR Boundary — модуль, принимающий обновление.

Пример:

if (import.meta.hot) {
    import.meta.hot.accept()
}

Этот код сообщает Vite, что модуль способен обработать обновление самостоятельно.

Без boundary обновление будет распространяться вверх по графу зависимостей.


Объект import.meta.hot

Vite предоставляет специальный API:

import.meta.hot

Этот объект существует только в dev-режиме.

Проверка:

if (import.meta.hot) {
    console.log('HMR enabled')
}

В production-сборке данный код удаляется.


Метод accept()

Метод accept() принимает обновление модуля.

Простейший вариант

if (import.meta.hot) {
    import.meta.hot.accept()
}

После изменения модуль будет заново выполнен.


Обработка нового модуля

if (import.meta.hot) {
    import.meta.hot.accept((newModule) => {
        console.log('Updated module:', newModule)
    })
}

Аргумент newModule содержит новую версию модуля.


Частичное принятие зависимостей

Можно принимать обновления конкретных зависимостей.

Пример:

import { value } from './store.js'

console.log(value)

if (import.meta.hot) {
    import.meta.hot.accept('./store.js', (module) => {
        console.log(module.value)
    })
}

В этом случае обновляется только store.js.


dispose(): очистка старого состояния

Перед заменой модуля может потребоваться освобождение ресурсов.

Например:

  • удаление таймеров;
  • закрытие WebSocket;
  • уничтожение подписок;
  • очистка DOM-событий.

Пример:

const timer = setInterval(() => {
    console.log('tick')
}, 1000)

if (import.meta.hot) {
    import.meta.hot.dispose(() => {
        clearInterval(timer)
    })
}

Без dispose старые ресурсы будут накапливаться.


Передача состояния между обновлениями

Vite позволяет сохранять данные между заменами модулей.

Используется объект:

import.meta.hot.data

Пример:

let counter = import.meta.hot?.data.counter || 0

counter++

console.log(counter)

if (import.meta.hot) {
    import.meta.hot.dispose((data) => {
        data.counter = counter
    })
}

Значение сохранится между обновлениями.


invalidate(): принудительная инвалидизация

Иногда модуль не способен корректно обработать HMR.

В таком случае можно вызвать:

import.meta.hot.invalidate()

Пример:

if (import.meta.hot) {
    import.meta.hot.accept((module) => {
        if (!module) {
            import.meta.hot.invalidate()
        }
    })
}

После invalidate Vite инициирует полную перезагрузку.


HMR для CSS

Одно из самых быстрых обновлений в Vite — CSS HMR.

При изменении:

body {
    background: black;
}

браузер обновляет стили без перезагрузки страницы и без потери состояния JavaScript.

Это достигается через динамическую замену <style> или <link>.


CSS Modules и HMR

Пример CSS Module:

.title {
    color: red;
}

Импорт:

import styles from './App.module.css'

При изменении CSS Vite обновит только соответствующие стили.

Компонент не будет полностью пересоздан.


HMR и React

В React Vite использует Fast Refresh.

Fast Refresh:

  • обновляет компонент;
  • сохраняет состояние hooks;
  • не перезагружает всё приложение.

Пример:

export default function Counter() {
    const [count, setCount] = useState(0)

    return (
        <button onCl ick={() => setCount(count + 1)}>
            {count}
        </button>
    )
}

После изменения JSX состояние count сохраняется.


Когда React Fast Refresh сбрасывает состояние

Состояние может быть потеряно:

  • при изменении порядка hooks;
  • при изменении сигнатуры компонента;
  • при экспорте нестабильных значений;
  • при ошибках выполнения.

Например:

if (condition) {
    useEffect(() => {})
}

Такой код нарушает правила hooks и может ломать Fast Refresh.


HMR и Vue

В Vue Vite обновляет:

  • template;
  • script;
  • styles.

Изменение шаблона:

<template>
    <h1>Hello</h1>
</template>

обычно не уничтожает состояние компонента.

Но изменение логики компонента может потребовать его пересоздания.


HMR и Svelte

Svelte использует собственную интеграцию HMR.

Поддерживаются:

  • обновление компонентов;
  • сохранение локального состояния;
  • частичная замена модулей.

Однако некоторые изменения всё равно вызывают полную перезагрузку компонента.


WebSocket-соединение

Vite использует WebSocket для передачи HMR-событий.

Пример внутреннего события:

{
  "type": "update",
  "updates": [
    {
      "path": "/src/App.jsx",
      "type": "js-update"
    }
  ]
}

Браузер получает сообщение и динамически импортирует новую версию модуля.


Механизм повторного импорта

После обновления Vite выполняет повторный import:

import('/src/App.js?t=1710000000000')

Timestamp используется для обхода HTTP-кеша.


HMR и кэш браузера

Vite активно использует HTTP-кэширование.

Но HMR должен загружать свежие версии файлов.

Для этого:

  • добавляются query-параметры;
  • используется ETag;
  • применяется timestamp invalidation.

HMR для JSON

JSON-файлы тоже поддерживают горячую замену.

Пример:

{
  "title": "Hello"
}

Импорт:

import data from './data.json'

Изменение JSON обновит модуль без полной перезагрузки.


HMR для Markdown

Через плагины Vite способен обновлять:

  • Markdown;
  • YAML;
  • GraphQL;
  • SVG;
  • пользовательские форматы.

Если плагин реализует HMR API, обновления происходят автоматически.


HMR в пользовательских плагинах

Плагин может управлять HMR самостоятельно.

Пример:

export default function myPlugin() {
    return {
        name: 'my-plugin',

        handleHotUpdate(ctx) {
            console.log(ctx.file)
        }
    }
}

handleHotUpdate() вызывается при изменении файлов.


Контекст handleHotUpdate

Объект ctx содержит:

{
    file,
    modules,
    server,
    timestamp,
    read
}

Описание полей:

Поле Назначение
file изменённый файл
modules связанные модули
server экземпляр dev server
timestamp время обновления
read чтение содержимого файла

Ручная отправка HMR-событий

Плагин может отправлять обновления вручную.

Пример:

server.ws.send({
    type: 'custom',
    event: 'my:update',
    data: {
        message: 'updated'
    }
})

Подписка на пользовательские события

На клиенте:

if (import.meta.hot) {
    import.meta.hot.on('my:update', (data) => {
        console.log(data)
    })
}

Это позволяет создавать собственные dev-инструменты.


HMR и производительность

Высокая скорость HMR в Vite достигается благодаря:

  • отсутствию полной сборки;
  • ES-модулям;
  • точечному обновлению;
  • быстрому анализу зависимостей;
  • esbuild pre-bundling;
  • кэшированию зависимостей.

В больших проектах разница особенно заметна.


Ограничения HMR

Не все изменения можно безопасно заменить «на лету».

Проблемные случаи:

  • глобальные side effects;
  • изменение структуры exports;
  • нестабильные singleton-объекты;
  • изменение runtime API;
  • модификация HTML-шаблонов;
  • изменения Vite config.

Некоторые изменения требуют полной перезагрузки сервера.


Side Effects и HMR

Плохой пример:

window.globalState = {}

После HMR состояние может стать непредсказуемым.

Лучше изолировать side effects:

export function createState() {
    return {}
}

HMR и singleton-модули

Singleton часто создаёт проблемы.

Пример:

export const socket = new WebSocket(url)

После HMR могут появиться дублирующиеся соединения.

Правильнее:

let socket

export function getSocket() {
    if (!socket) {
        socket = new WebSocket(url)
    }

    return socket
}

И очищать соединение через dispose.


Ошибки HMR

Если модуль содержит синтаксическую ошибку:

const =

Vite:

  • покажет overlay с ошибкой;
  • не перезагрузит страницу;
  • восстановит приложение после исправления.

Это делает процесс разработки значительно удобнее.


Error Overlay

Встроенный overlay отображает:

  • stack trace;
  • файл ошибки;
  • номер строки;
  • фрагмент кода;
  • сообщение компилятора.

После исправления overlay исчезает автоматически.


Настройка HMR

Раздел server.hmr в конфигурации:

export default defineConfig({
    server: {
        hmr: {
            protocol: 'ws',
            host: 'localhost',
            port: 24678
        }
    }
})

Основные параметры HMR

Параметр Назначение
protocol ws или wss
host адрес сервера
port порт WebSocket
clientPort порт клиента
overlay показ overlay

Отключение overlay

export default defineConfig({
    server: {
        hmr: {
            overlay: false
        }
    }
})

HMR через HTTPS

При использовании HTTPS:

server: {
    https: true,
    hmr: {
        protocol: 'wss'
    }
}

Используется защищённое WebSocket-соединение.


HMR в Docker

В контейнерах часто возникают проблемы с file watching.

Решение:

server: {
    watch: {
        usePolling: true
    }
}

Polling менее эффективен, но стабильнее в виртуализированных окружениях.


HMR и WSL

В WSL возможны:

  • задержки обновлений;
  • проблемы с inode;
  • пропуск событий watcher.

Иногда помогает polling:

watch: {
    usePolling: true
}

HMR и монорепозитории

В monorepo HMR особенно полезен.

Vite способен обновлять:

  • локальные пакеты;
  • shared libraries;
  • workspace modules.

Пример:

packages/
    ui/
    core/
apps/
    dashboard/

Изменение ui может мгновенно обновить dashboard.


Отличие HMR от Live Reload

HMR Live Reload
обновляет модуль перезагружает страницу
сохраняет состояние теряет состояние
быстрее медленнее
точечные изменения полная перезагрузка

Live Reload — более простой механизм.

HMR значительно сложнее, но намного эффективнее.


Почему HMR в Vite работает быстро

Ключевая причина — отсутствие необходимости пересобирать bundle при каждом изменении.

Вместо этого:

  • обновляется конкретный модуль;
  • браузер сам управляет импортами;
  • Vite работает как интеллектуальный сервер модулей;
  • HMR распространяется только по нужным зависимостям.

Именно эта архитектура делает Vite одним из самых быстрых инструментов разработки frontend-приложений.