Гидратация: как оживает страница
Сервер отдал статичный 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'ей не будет.