Vue
Гидрация и гибридный рендеринг
Маркетинговый лендинг на Nuxt отдаёт готовый HTML за 40 миллисекунд, и Lighthouse рисует зелёный FCP. Затем в консоли всплывает 'Hydration node mismatch', список товаров на секунду дёргается, а обработчики кликов не срабатывают до полной загрузки бандла. Сервер нарисовал одну картинку, клиент при первом рендере нарисовал другую, и Vue отказался переиспользовать чужую разметку. Этот урок разбирает, что именно происходит между готовым HTML и живым интерфейсом и как через routeRules в Nuxt платить за рендеринг ровно столько, сколько нужно каждому маршруту.
- Nuxt 3 и 4: routeRules задают prerender для лендингов, SWR для каталога и SSR для личного кабинета в одном конфиге
- Vercel и Netlify: SWR-кеш на edge отдаёт устаревший HTML мгновенно и пересобирает страницу в фоне
- Документация Vue.dev и Nuxt.com сами полностью пререндерятся в статику ради мгновенного TTFB
- Интернет-магазины на Nuxt: карточка товара идёт через SWR с ревалидацией, корзина через чистый SSR без кеша
- Дашборды с приватными данными: SSR без пререндера, потому что HTML зависит от текущего пользователя
Предварительные знания
- Понимание серверного рендеринга Vue и базовая работа с Nuxt
- Знание жизненного цикла компонента и что монтирование происходит на клиенте
- Представление о TTFB, FCP и о том, что бандл JavaScript нужно скачать и выполнить
Что такое гидратация
При серверном рендеринге Vue превращает дерево компонентов в строку HTML и отдаёт её браузеру. Браузер показывает эту разметку сразу, ещё до загрузки JavaScript, и пользователь видит контент. Но статический HTML мёртвый: у него нет обработчиков кликов, нет реактивных связей, нет состояния. Гидрация это процесс, в котором уже загруженный клиентский Vue проходит по существующим DOM-узлам и оживляет их: привязывает обработчики событий, восстанавливает реактивное состояние и связывает компоненты с узлами, которые уже есть на странице.
Ключевое отличие гидратации от обычного клиентского рендеринга в том, что DOM не создаётся заново. Vue ожидает, что разметка уже на месте, и переиспользует её. Это даёт быстрый показ контента (HTML пришёл с сервера) и при этом полноценную интерактивность после загрузки бандла. Цена гидратации это время, за которое клиент проходит дерево и навешивает реактивность.
Между приходом HTML и завершением гидратации страница выглядит готовой, но не реагирует на клики. Этот промежуток называют разрывом интерактивности. Чем больше дерево и тяжелее бандл, тем дольше гидратация, поэтому уменьшение объёма JavaScript напрямую ускоряет момент, когда интерфейс начинает отвечать.
Чем гидратация отличается от обычного клиентского рендеринга?
Откуда берётся hydration mismatch
Гидрация работает при одном условии: первый клиентский рендер должен в точности совпасть с тем, что отдал сервер. Если совпадения нет, Vue в режиме разработки выводит предупреждение Hydration mismatch, отбрасывает серверную разметку для проблемного поддерева и перерисовывает его на клиенте. Это и лишняя работа, и визуальный сдвиг, и потенциальная потеря серверного контента.
| Причина | Почему ломается | Решение |
|---|---|---|
| Date.now() или new Date() в рендере | Сервер и клиент отрисовали в разные моменты времени | Зафиксировать время на сервере и передать его как данные |
| Math.random() | Случайное значение разное на сервере и клиенте | Генерировать на сервере, передавать в стейте |
| Обращение к window или localStorage в setup | На сервере window отсутствует, ветка кода расходится | Читать значение в onMounted, только на клиенте |
| Невалидная вложенность HTML | Браузер чинит разметку, и DOM не совпадает с серверной строкой | Не класть div внутрь p, table собирать корректно |
| Разные данные на сервере и клиенте | Стор не сериализован и заполнился заново другим значением | Передавать состояние через payload и не запрашивать повторно |
Когда контент по своей природе зависит от клиента (например, текущее локальное время или данные из localStorage), Nuxt предлагает компонент ClientOnly. Он рендерит содержимое только после монтирования и не участвует в серверной разметке, поэтому несовпадения не возникает.
Почему вызов new Date().toLocaleTimeString() прямо в шаблоне приводит к hydration mismatch?
routeRules: режим рендеринга на каждый маршрут
Не каждой странице нужен один и тот же режим рендеринга. Лендинг не меняется между деплоями и его выгодно пререндерить в статику. Каталог обновляется редко и хорошо живёт на SWR-кеше. Личный кабинет зависит от текущего пользователя и требует чистого SSR без кеша. Nuxt позволяет задать это в одном месте через routeRules: набор правил по шаблону маршрута, каждое со своим режимом.
| Режим | Когда HTML собирается | TTFB и свежесть |
|---|---|---|
| prerender | Один раз во время сборки, отдаётся как статика | Минимальный TTFB, данные на момент сборки |
| swr: N | По запросу, кешируется на N секунд, потом ревалидируется в фоне | Быстрый TTFB из кеша, фоновое обновление |
| cache | По запросу с настраиваемым TTL на ответ или маршрут | Контролируемая свежесть, кеш на edge или сервере |
| ssr: true | На каждый запрос заново на сервере | TTFB зависит от сервера, всегда свежие данные |
| ssr: false | Только клиентский рендер, HTML почти пустой | Быстрый ответ, но контент появляется после JS |
SWR расшифровывается как stale-while-revalidate. Первый запрос рендерит и кеширует страницу. Следующие запросы в течение N секунд получают кеш мгновенно, а после истечения интервала первый запрос отдаёт устаревшую копию и одновременно запускает фоновую пересборку. Так пользователь почти никогда не ждёт рендер, а данные остаются достаточно свежими. Этот режим подходит маршрутам, где небольшая задержка обновления допустима: каталог, статьи, публичные профили.
Кеш недопустим для страниц с персональными данными. Если личный кабинет отдать через swr или cache, edge может вернуть HTML одного пользователя другому. Такие маршруты должны идти только через ssr: true без какого-либо кеширования.
Каталог товаров на Nuxt обновляется раз в час, страниц десятки тысяч, и важен низкий TTFB. Какой routeRule подходит лучше всего?
Связь с другими темами
Урок соединяет серверный рендеринг с клиентской интерактивностью и ведёт к оптимизации:
- Серверный рендеринг и Nuxt — Гидрация это вторая половина SSR: сервер отдал HTML, клиент должен оживить его
- Производительность Vue — После гидратации стоимость интерактивности определяется ре-рендерами и реактивностью
- Vapor Mode — Будущая модель компиляции обещает гидратацию без накладных расходов виртуального DOM
Итог
- Гидрация это процесс, в котором клиентский Vue переиспользует серверный HTML, привязывая к существующим узлам реактивное состояние и обработчики событий, а не пересоздавая DOM
- Hydration mismatch возникает, когда первый клиентский рендер не совпадает с серверным: причины это Date.now(), Math.random(), обращение к window, невалидная вложенность HTML и данные, отличающиеся на сервере и клиенте
- Несовпадение приводит к лишней перерисовке поддерева и потере серверной разметки, поэтому критично делать первый рендер детерминированным
- routeRules в Nuxt назначают режим рендеринга на маршрут: prerender, ssr, swr и cache с разными значениями TTFB и свежести данных
- prerender даёт статику с минимальным TTFB, SWR отдаёт кеш мгновенно и ревалидирует в фоне, чистый SSR нужен для персональных данных
Связанные уроки
- vue-38-performance — Гидрация задаёт стартовую цену интерактивности, а урок про производительность показывает как держать её низкой после загрузки
- vue-35-vapor-mode — Vapor Mode меняет саму модель рендеринга и обещает более дешёвую гидратацию без виртуального DOM