NuxtSeniorExperience

Какие production-риски есть у Nuxt: hydration, bundle size, caching, accessibility, browser compatibility или observability?

Ключевые production-риски Nuxt: hydration mismatch при SSR из-за различий server/client окружений, раздутый bundle без code splitting, неверная стратегия кеширования API, проблемы доступности динамического контента.

Production-риски Nuxt: глубокий разбор

1. Hydration Mismatch

Самая частая ошибка в Nuxt — несоответствие между HTML, сгенерированным на сервере, и результатом hydration на клиенте.

// ПРОБЛЕМА: Date.now() разный на сервере и клиенте
const timestamp = ref(Date.now())
// Vue выбросит: [Vue warn] Hydration node mismatch

// РЕШЕНИЕ 1: ClientOnly wrapper
<ClientOnly>
  <TimestampWidget />
  <template #fallback>
    <div>Loading...</div>
  </template>
</ClientOnly>
// РЕШЕНИЕ 2: useHydration / onMounted
const timestamp = ref<number | null>(null)
onMounted(() => { timestamp.value = Date.now() })

// РЕШЕНИЕ 3: nuxt plugin с mode: 'client'
// plugins/analytics.client.ts
export default defineNuxtPlugin(() => {
  // выполняется только на клиенте
  window.analytics?.init()
})

2. Bundle Size

# Анализ бандла
npx nuxi build --analyze
# или
NUXT_ANALYZE=true npx nuxi build
// nuxt.config.ts — оптимизация
export default defineNuxtConfig({
  // Автоматический import только используемых компонентов
  components: {
    dirs: [{ path: '~/components', pathPrefix: false }],
  },

  // Lazy-load тяжёлых компонентов
  // В шаблоне: <LazyHeavyChart /> — загружается только при необходимости

  vite: {
    build: {
      rollupOptions: {
        output: {
          manualChunks: {
            'chart-libs': ['chart.js', 'vue-chartjs'],
            'editor': ['@tiptap/core', '@tiptap/vue-3'],
          },
        },
      },
    },
  },

  // Внешние зависимости не включаем в server bundle
  nitro: {
    externals: {
      inline: ['some-large-lib'],
    },
  },
});

3. Кеширование и ISR

// server/api/products.get.ts
export default defineCachedEventHandler(async (event) => {
  const products = await fetchFromDB()
  return products
}, {
  maxAge: 60,      // кешировать 60 секунд
  name: 'products-list',
  getKey: (event) => {
    const query = getQuery(event)
    return `products-${query.category}-${query.page}`
  },
  // Инвалидация при изменении данных:
  // await useStorage().removeItem('nitro:handlers:products-list')
})

// nuxt.config.ts — route rules
export default defineNuxtConfig({
  routeRules: {
    '/': { prerender: true },
    '/blog/**': { isr: 3600 },
    '/admin/**': { ssr: false, headers: { 'Cache-Control': 'no-store' } },
    '/api/**': { cors: true },
  },
})

4. Accessibility

<!-- Проблема: SPA-навигация не анонсирует смену страницы screen reader'у -->

<!-- Решение: aria-live region для анонсов навигации -->
<!-- layouts/default.vue -->
<template>
  <div>
    <div
      aria-live="polite"
      aria-atomic="true"
      class="sr-only"
    >
      {{ pageTitle }} — загружено
    </div>
    <NuxtPage />
  </div>
</template>
// Обновляем заголовок при навигации
const route = useRoute()
const pageTitle = computed(() => route.meta.title as string || 'Страница')

useHead({ title: pageTitle })

5. Browser Compatibility

// nuxt.config.ts
export default defineNuxtConfig({
  vite: {
    build: {
      target: 'es2015',  // поддержка старых браузеров
    },
  },
  // Polyfills через @vitejs/plugin-legacy
})

Подводные камни

  • Hydration mismatch от Math.random(), Date.now(), localStorage в шаблоне — любые значения, разные на сервере и клиенте, ломают hydration.
  • useAsyncData без явного key: Nuxt генерирует ключ по имени файла и строке вызова — при минификации или переименовании файлов кеш стает невалидным.
  • Неверный maxAge в Cache-Control для авторизованных страниц: CDN кеширует персональный ответ и отдаёт другим пользователям.
  • Lazy-компоненты (<LazyX />) без #fallback слота показывают пустое место во время загрузки — CLS (Cumulative Layout Shift) в Core Web Vitals.
  • Server routes без валидации входных данных: Nuxt не добавляет автоматическую санитизацию — передайте getValidatedQuery / readValidatedBody с zod-схемами.
  • useFetch на клиенте делает HTTP-запрос на сервер снова (не переиспользует SSR-данные) если ключи не совпадают — двойной запрос и мерцание UI.
  • Отсутствие error.vue кастомной страницы ошибок: при 500 пользователь видит стандартный Nitro error page без брендинга.
  • Observable производительность: без явного логирования useServerTiming() невозможно определить, на каком этапе SSR тратится время — добавьте server timing headers в production.

What hurts your answer

  • Говорить только о запуске Nuxt, но не об эксплуатации
  • Не упоминать observability, обновления, безопасность и rollback
  • Описывать риски абстрактно, без способов их снижать

What they're listening for

  • Видит production-риски Nuxt
  • Говорит про monitoring, rollout, rollback и безопасность
  • Умеет ранжировать риски по вероятности и влиянию

Related topics