В режиме разработки с SSR в Vite одновременно существуют два независимых механизма обновления модулей: клиентский HMR и серверный пересчёт модулей. Клиентская часть отвечает за мгновенное обновление интерфейса в браузере без полной перезагрузки страницы, серверная — за пересборку и повторное выполнение модулей, участвующих в серверном рендеринге.
Ключевая особенность SSR заключается в том, что результат рендера формируется на сервере, а затем гидратируется на клиенте. Это создаёт необходимость синхронизации двух миров:
При изменении исходного кода требуется не только обновить DOM через HMR, но и пересчитать HTML, который возвращается сервером.
Vite строит граф модулей поверх ESM. Каждый импорт рассматривается как узел графа, а связи между ними формируют дерево зависимостей.
В режиме SSR этот граф дублируется логически:
При изменении файла Vite выполняет обход графа зависимостей, определяя:
Особенность SSR-графа заключается в том, что он должен учитывать синхронное выполнение модулей на сервере, включая side-effect импорты.
HMR в Vite основан на WebSocket-соединении между dev server и клиентом. При изменении файла происходит следующий цикл:
Для SSR добавляется дополнительный шаг: пересборка серверного модуля.
При изменении файла, участвующего в SSR-рендере, Vite выполняет:
Типичный SSR-цикл:
// server entry
export async function render(url) {
const app = await createApp()
return renderToString(app)
}
При HMR изменение любого модуля, входящего в createApp(), приводит к повторному выполнению render() при следующем запросе или через триггер dev middleware.
В SSR-контексте используется расширенный HMR API:
if (import.meta.hot) {
import.meta.hot.accept((newModule) => {
// обновление логики без перезапуска сервера
})
import.meta.hot.invalidate(() => {
// принудительный пересчёт SSR
})
}
invalidate играет ключевую роль в SSR, так как не все изменения могут быть корректно применены частичным обновлением.
SSR требует строгого соответствия HTML, сгенерированного сервером, и состояния, используемого клиентом при гидратации.
При HMR возможны ситуации:
Vite решает это через стратегию:
Граница между серверным и клиентским HMR проходит в точке сериализации состояния:
Любой модуль, который влияет на:
считается SSR-значимым и требует серверной инвалидции.
В SSR режиме entry-файл сервера не компилируется заново полностью при каждом изменении. Вместо этого Vite:
Это позволяет сохранять быстрый response time dev server даже при сложных приложениях.
Коммуникация HMR реализована через WebSocket канал, где события имеют типовую структуру:
Для SSR часто используется комбинация:
SSR HMR в Vite не привязан к конкретному UI-фреймворку, но интеграционные слои (React, Vue, Svelte) добавляют собственные правила.
Общий паттерн:
Фреймворк-специфичные плагины добавляют:
Частичное обновление модулей в SSR-контексте осложняется рядом факторов:
Типичный источник ошибок — модули с состоянием, которое живёт между запросами:
let cachedData = null
export async function getData() {
if (!cachedData) {
cachedData = await fetch(...)
}
return cachedData
}
При HMR такой кэш может сохраняться, что приводит к несоответствию обновлённого кода и данных.
Vite использует транзитивную инвалидацию:
Если изменение затрагивает глубинный модуль, например утилиту форматирования, пересчитываются все SSR маршруты, где он используется.
Dev server Vite часто используется как middleware:
В таком режиме SSR HMR требует:
Это позволяет избегать полной перезагрузки backend процесса.
SSR HMR становится сложным при наличии состояния:
Vite не управляет этим состоянием напрямую, но предоставляет механизм:
if (import.meta.hot) {
import.meta.hot.dispose(() => {
cleanupConnections()
})
}
После обновления SSR модуля клиент может получить новый HTML. При этом:
Поэтому порядок критичен:
Нарушение последовательности приводит к DOM mismatch.
Основные оптимизации Vite в SSR HMR:
Это позволяет сохранять быстрый цикл обновлений даже при большом количестве модулей.
В SSR HMR часто применяются следующие подходы:
Такая структура минимизирует необходимость full reload и делает HMR предсказуемым.
Несмотря на гибкость, существуют ограничения:
Эти ограничения вытекают из природы SSR, где результат вычисляется на сервере, а не только в клиентском runtime.