HMR при разработке с SSR

Разделение клиентского и серверного HMR-контуров

В режиме разработки с SSR в Vite одновременно существуют два независимых механизма обновления модулей: клиентский HMR и серверный пересчёт модулей. Клиентская часть отвечает за мгновенное обновление интерфейса в браузере без полной перезагрузки страницы, серверная — за пересборку и повторное выполнение модулей, участвующих в серверном рендеринге.

Ключевая особенность SSR заключается в том, что результат рендера формируется на сервере, а затем гидратируется на клиенте. Это создаёт необходимость синхронизации двух миров:

  • серверного состояния модулей
  • клиентского состояния приложения

При изменении исходного кода требуется не только обновить DOM через HMR, но и пересчитать HTML, который возвращается сервером.


Модель модульного графа Vite

Vite строит граф модулей поверх ESM. Каждый импорт рассматривается как узел графа, а связи между ними формируют дерево зависимостей.

В режиме SSR этот граф дублируется логически:

  • клиентский граф — используется браузером
  • серверный граф — используется Node.js процессом Vite dev server

При изменении файла Vite выполняет обход графа зависимостей, определяя:

  • какие модули должны быть инвалидированы
  • какие HMR-обновления могут быть применены частично
  • где требуется полный пересчёт SSR

Особенность SSR-графа заключается в том, что он должен учитывать синхронное выполнение модулей на сервере, включая side-effect импорты.


Механизм HMR в Vite

HMR в Vite основан на WebSocket-соединении между dev server и клиентом. При изменении файла происходит следующий цикл:

  1. Файловая система фиксирует изменение
  2. Vite определяет затронутые модули
  3. Выполняется инвалидирование кэша модулей
  4. Отправляется HMR-событие через WebSocket
  5. Клиент принимает обновление и применяет патч

Для SSR добавляется дополнительный шаг: пересборка серверного модуля.


SSR-инвалидация и пересчёт модулей

При изменении файла, участвующего в SSR-рендере, Vite выполняет:

  • сброс кэша модуля в Node.js окружении
  • повторный импорт изменённых модулей
  • пересчёт функции render()

Типичный SSR-цикл:

// server entry
export async function render(url) {
  const app = await createApp()
  return renderToString(app)
}

При HMR изменение любого модуля, входящего в createApp(), приводит к повторному выполнению render() при следующем запросе или через триггер dev middleware.


Инвалидация через import.meta.hot

В SSR-контексте используется расширенный HMR API:

if (import.meta.hot) {
  import.meta.hot.accept((newModule) => {
    // обновление логики без перезапуска сервера
  })

  import.meta.hot.invalidate(() => {
    // принудительный пересчёт SSR
  })
}

invalidate играет ключевую роль в SSR, так как не все изменения могут быть корректно применены частичным обновлением.


Клиентская гидратация и риск рассинхронизации

SSR требует строгого соответствия HTML, сгенерированного сервером, и состояния, используемого клиентом при гидратации.

При HMR возможны ситуации:

  • сервер уже отрендерил новую версию
  • клиент ещё работает со старой гидратацией
  • DOM отличается от ожидаемого виртуального дерева

Vite решает это через стратегию:

  • частичный HMR для UI-компонентов
  • полная перезагрузка страницы при нарушении консистентности

SSR HMR boundary: где происходит переключение

Граница между серверным и клиентским HMR проходит в точке сериализации состояния:

  • сервер генерирует HTML
  • клиент принимает payload и выполняет hydration

Любой модуль, который влияет на:

  • маршрутизацию
  • данные страницы
  • layout-композицию

считается SSR-значимым и требует серверной инвалидции.


Пересборка server entry

В SSR режиме entry-файл сервера не компилируется заново полностью при каждом изменении. Вместо этого Vite:

  • отслеживает зависимые модули server entry
  • пересобирает только изменённые части графа
  • обновляет кэшированный модульный контекст

Это позволяет сохранять быстрый response time dev server даже при сложных приложениях.


WebSocket протокол обновлений

Коммуникация HMR реализована через WebSocket канал, где события имеют типовую структуру:

  • update
  • full-reload
  • prune
  • error

Для SSR часто используется комбинация:

  • update (для клиентских компонентов)
  • full-reload (при нарушении SSR согласованности)

Интеграция SSR с фреймворками

SSR HMR в Vite не привязан к конкретному UI-фреймворку, но интеграционные слои (React, Vue, Svelte) добавляют собственные правила.

Общий паттерн:

  • сервер экспортирует render функцию
  • клиент экспортирует createApp / hydrate
  • Vite связывает их через shared module graph

Фреймворк-специфичные плагины добавляют:

  • точечную замену компонентов
  • пересборку tree-shaking зависимостей
  • оптимизированные HMR boundary

Проблемы частичного HMR в SSR

Частичное обновление модулей в SSR-контексте осложняется рядом факторов:

  • side effects в модулях
  • кеширование данных на сервере
  • асинхронные зависимости
  • singleton-сервисы

Типичный источник ошибок — модули с состоянием, которое живёт между запросами:

let cachedData = null

export async function getData() {
  if (!cachedData) {
    cachedData = await fetch(...)
  }
  return cachedData
}

При HMR такой кэш может сохраняться, что приводит к несоответствию обновлённого кода и данных.


Инвалидация цепочек зависимостей

Vite использует транзитивную инвалидацию:

  • изменённый модуль
  • все импортеры
  • все SSR entry точки

Если изменение затрагивает глубинный модуль, например утилиту форматирования, пересчитываются все SSR маршруты, где он используется.


SSR middleware и горячие обновления

Dev server Vite часто используется как middleware:

  • Express
  • Koa
  • Fastify

В таком режиме SSR HMR требует:

  • удержания server instance между обновлениями
  • пересоздания только render pipeline
  • сохранения HTTP сервера активным

Это позволяет избегать полной перезагрузки backend процесса.


Состояние сервера и HMR границы

SSR HMR становится сложным при наличии состояния:

  • подключение к базе данных
  • in-memory кэши
  • singleton сервисы

Vite не управляет этим состоянием напрямую, но предоставляет механизм:

  • hot.dispose для очистки ресурсов
  • hot.accept для замены логики без рестарта
if (import.meta.hot) {
  import.meta.hot.dispose(() => {
    cleanupConnections()
  })
}

Гидратация после SSR обновлений

После обновления SSR модуля клиент может получить новый HTML. При этом:

  • старая виртуальная структура может конфликтовать
  • события HMR могут примениться до hydration

Поэтому порядок критичен:

  1. SSR обновление
  2. генерация HTML
  3. клиентский HMR
  4. hydration

Нарушение последовательности приводит к DOM mismatch.


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

Основные оптимизации Vite в SSR HMR:

  • ESM-native загрузка без бандлинга
  • кэширование трансформированных модулей
  • точечная инвалидизация
  • lazy re-execution SSR entry

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


Типовые архитектурные паттерны

В SSR HMR часто применяются следующие подходы:

  • разделение server/client/shared кода
  • изоляция stateful сервисов
  • чистые render-функции без побочных эффектов
  • dependency injection для пересоздания контекста

Такая структура минимизирует необходимость full reload и делает HMR предсказуемым.


Ограничения HMR в SSR окружении

Несмотря на гибкость, существуют ограничения:

  • невозможность безопасного hot swap для всех server modules
  • необходимость full reload при изменении entry graph
  • риск рассинхронизации hydration
  • сложность работы с глобальными синглтонами

Эти ограничения вытекают из природы SSR, где результат вычисляется на сервере, а не только в клиентском runtime.