Гидратация: как оживает страница

Сервер отдал статичный HTML — гидратация навешивает на него реактивность Vue, не перерисовывая разметку заново.
Суть: гидратация — это процесс, в котором браузерный Vue «подключается» к уже готовому серверному HTML, переиспользует существующие DOM-узлы и навешивает на них обработчики и реактивность. Если серверная и клиентская разметка не совпадают, возникает hydration mismatch.

В прошлом уроке мы дошли до момента, когда сервер отдал готовый HTML, а браузер начинает его «оживлять». Этот шаг называется гидратацией (от англ. hydration — насыщение водой: сухой HTML «напитывается» интерактивностью). Это сердце SSR и одновременно источник самых коварных багов новичка.

Зачем вообще нужна гидратация? Серверный HTML — это статичная картинка. Кнопки нарисованы, но не нажимаются; данные показаны, но не реактивны. Чтобы страница ожила, Vue должен в браузере построить своё виртуальное дерево и сопоставить его с уже существующим DOM. Вместо того чтобы стирать серверный HTML и рисовать заново (это было бы расточительно и вызвало бы мигание), Vue аккуратно переиспользует имеющиеся узлы и лишь навешивает на них слушатели событий и связи реактивности.

   Жизненный цикл гидратации

   1. Сервер: Vue рендерит -> HTML строка
   2. Сервер: состояние -> payload (сериализация)
   3. Браузер: показывает HTML (контент виден, кнопки "мертвы")
   4. Браузер: грузит JS, читает payload
   5. Vue строит vDOM и СОПОСТАВЛЯЕТ с готовым DOM
   6. Навешивает обработчики -> страница интерактивна

   Если шаг 5 находит расхождение -> HYDRATION MISMATCH

Чтобы Vue в браузере получил ровно то же состояние, что было на сервере, Nuxt сериализует это состояние в так называемый payload — JSON, встроенный в HTML-страницу. Браузер читает payload и восстанавливает данные без повторного запроса к серверу. Именно поэтому в Nuxt так важно использовать useFetch и useAsyncData: они кладут результат в payload, и клиент не запрашивает те же данные второй раз.

Важно не воспринимать выбор как идеологический. SSR и SPA — это инструменты под задачу, а не лагеря. Контентному сайту, где важны индексация и скорость первого экрана, нужен SSR. Внутреннему дашборду за логином, который всё равно никто не индексирует, SPA подойдёт лучше: меньше нагрузка на сервер и проще инфраструктура. Сила Nuxt в том, что он не заставляет выбирать раз и навсегда — режим можно задать даже отдельным маршрутам, и об этом будет отдельный урок в финале курса.

Как работает под капотом

Главное правило гидратации: HTML, который сгенерировал сервер, должен в точности совпасть с тем, что Vue ожидает увидеть на клиенте на первом рендере. Если они различаются — Vue не сможет корректно сопоставить узлы, выбросит предупреждение о hydration mismatch и в худшем случае перерисует кусок дерева, теряя серверные преимущества.

Откуда берутся расхождения? Чаще всего — из недетерминированного кода. Если компонент при рендере вызывает Date.now(), Math.random() или читает window.innerWidth, то на сервере и в браузере он получит разные значения, и разметка разойдётся. Смоделируем это сравнением «серверного» и «клиентского» вывода:

// Почему недетерминированный рендер ломает гидратацию.
function renderWidget(getValue) {
  return "<span>Значение: " + getValue() + "</span>";
}

// Имитируем расхождение: случайное число на "сервере" и "клиенте"
const serverHtml = renderWidget(() => Math.floor(Math.random() * 1000));
const clientHtml = renderWidget(() => Math.floor(Math.random() * 1000));

console.log("Сервер отрендерил:", serverHtml);
console.log("Клиент ожидал:   ", clientHtml);
console.log("Совпадают?", serverHtml === clientHtml);
console.log("Несовпадение -> hydration mismatch, Vue ругается в консоли.");

// Детерминированный вариант — одинаков везде:
const stable = renderWidget(() => 777);
console.log("\nСтабильный рендер всегда даёт:", stable);

Попробуй сам ▶ — случайные значения почти никогда не совпадут, а стабильное 777 совпадёт всегда. Это и есть разница между «ломает гидратацию» и «безопасно».

Частые ошибки

  • Обращение к window / document в рендере. На сервере их нет. Используйте onMounted или проверку import.meta.client.
  • Случайные значения и время в шаблоне. Math.random(), new Date() в разметке гарантируют mismatch. Выносите их за рендер.
  • Невалидный HTML. Например, <p> внутри <p> — браузер «чинит» разметку, и она перестаёт совпадать с серверной.

Best practices

  • Браузер-зависимый код — только в onMounted или за флагом import.meta.client.
  • Для заведомо клиентских виджетов используйте обёртку <ClientOnly> — Nuxt не станет рендерить их на сервере.
  • Доверяйте данным из payload: не запрашивайте на клиенте то, что уже пришло с сервера.

Итог: гидратация — мост между серверным HTML и живым Vue. Она экономит работу браузера, но требует, чтобы серверный и клиентский рендер совпадали. Держите рендер детерминированным — и mismatch'ей не будет.

Проверьте себя
1. Что такое hydration mismatch?
AОшибка сети при загрузке страницы
BРасхождение между HTML, сгенерированным на сервере, и тем, что Vue ожидает на клиенте при первом рендере
CКонфликт версий Vue и Nuxt
DСитуация, когда сервер не отдал CSS
2. Зачем Nuxt передаёт payload клиенту?
AЧтобы заново запросить все данные в браузере
BЧтобы сериализовать состояние с сервера и восстановить его на клиенте без повторного запроса
CЧтобы хранить пароли пользователя
DPayload нужен только для стилей