Nuxt и гидратация

Почему важно устранять проблемы гидратации

В разработке вы можете столкнуться с ошибками гидратации. Не игнорируйте эти предупреждения.

Зачем их исправлять?

Расхождения при гидратации — не просто предупреждения: это признак серьёзных проблем, которые могут сломать приложение:

Влияние на производительность

  • Дольше до интерактивности: ошибки гидратации заставляют Vue перерисовывать всё дерево компонентов, из‑за чего приложение дольше становится интерактивным.
  • Плохой пользовательский опыт: пользователь может видеть мигание контента или неожиданные сдвиги вёрстки.

Проблемы с поведением

  • Ломается интерактивность: обработчики могут не навеситься — кнопки и формы перестают работать.
  • Рассинхрон состояния: то, что видит пользователь, и то, что «думает» приложение, расходятся.
  • Проблемы с поисковой оптимизацией: поисковик может индексировать не то, что видит пользователь.

Как обнаружить

Предупреждения в консоли разработки

Vue выводит предупреждения о несовпадении гидратации в консоли браузера в режиме разработки:

Типичные причины

Браузерные API в контексте сервера

Проблема: использование API, доступных только в браузере, во время SSR.

<template>
  <div>Предпочтение пользователя: {{ userTheme }}</div>
</template>

<script setup>
// вызовет несовпадение при гидратации!
// на сервере нет localStorage
const userTheme = localStorage.getItem('theme') || 'light'
</script>

Решение: можно использовать useCookie:

<template>
  <div>Предпочтение пользователя: {{ userTheme }}</div>
</template>

<script setup>
// работает и на сервере, и на клиенте
const userTheme = useCookie('theme', { default: () => 'light' })
</script>

Разные данные на сервере и клиенте

Проблема: данные на сервере и клиенте не совпадают.

<template>
  <div>{{ Math.random() }}</div>
</template>

Решение: состояние, дружественное к SSR:

<template>
  <div>{{ state }}</div>
</template>

<script setup>
const state = useState('random', () => Math.random())
</script>

Условный рендер по состоянию клиента

Проблема: условия, зависящие только от клиента, при SSR.

<template>
  <div v-if="window?.innerWidth > 768">
    Контент для десктопа
  </div>
</template>

Решение: медиазапросы или обработка только на клиенте:

<template>
  <div class="responsive-content">
    <div class="hidden md:block">Контент для десктопа</div>
    <div class="md:hidden">Контент для мобильных</div>
  </div>
</template>

Сторонние библиотеки с побочными эффектами

Проблема: библиотеки, меняющие DOM или завязанные на браузер (часто — менеджеры тегов).

<script setup>
if (import.meta.client) {
    const { default: SomeBrowserLibrary } = await import('browser-only-lib')
    SomeBrowserLibrary.init()
}
</script>

Решение: инициализировать после завершения гидратации:

<script setup>
onMounted(async () => {
  const { default: SomeBrowserLibrary } = await import('browser-only-lib')
  SomeBrowserLibrary.init()
})
</script>

Динамический контент по времени

Проблема: контент зависит от текущего времени.

<template>
  <div>{{ greeting }}</div>
</template>

<script setup>
const hour = new Date().getHours()
const greeting = hour < 12 ? 'Доброе утро' : 'Добрый день'
</script>

Решение: компонент NuxtTime или логика на клиенте:

<template>
  <div>
    <NuxtTime :date="new Date()" format="HH:mm" />
  </div>
</template>
<template>
  <div>
    <ClientOnly>
      {{ greeting }}
      <template #fallback>
        Здравствуйте!
      </template>
    </ClientOnly>
  </div>
</template>

<script setup>
const greeting = ref('Здравствуйте!')

onMounted(() => {
  const hour = new Date().getHours()
  greeting.value = hour < 12 ? 'Доброе утро' : 'Добрый день'
})
</script>

Кратко

  1. Композаблы, дружественные к SSR: useFetch, useAsyncData, useState.
  2. Код только для браузера: компонент ClientOnly.
  3. Одинаковые источники данных: сервер и клиент должны получать согласованные данные.
  4. Без побочных эффектов в setup: браузерный код — в onMounted.
Подробнее о несовпадении гидратации при SSR — в документации Vue.