Nuxt 3 из коробки делает много правильных вещей, но дефолтная конфигурация — это компромисс для большинства проектов, а не оптимум для вашего. Если Lighthouse показывает жёлтые или красные цифры, а пользователи видят белый экран дольше секунды — разберём что с этим делать.
Lazy loading компонентов
Nuxt автоматически делает компоненты с префиксом Lazy асинхронными. Используйте это везде, где компонент не нужен при первом рендере:
<template>
<!-- Загрузится только когда попадёт в DOM -->
<LazyModalFeedback v-if="showModal" />
<LazyTheFooter />
<!-- Для тяжёлых виджетов — отличный вариант -->
<LazyChartDashboard v-if="dataLoaded" />
</template>
Правило простое: всё что ниже fold, все модалки, все вкладки которые не активны по умолчанию — делайте Lazy.
Отключаем ненужные модули и плагины
Частая причина раздутого бандла — подключённые плагины которые работают и на сервере и на клиенте, хотя нужны только в одном месте.
// nuxt.config.ts
export default defineNuxtConfig({
plugins: [
// Аналитика нужна только на клиенте
{ src: '~/plugins/analytics.client.ts' },
// Этот плагин только для SSR
{ src: '~/plugins/cache.server.ts' },
]
})
Суффиксы .client.ts и .server.ts — ваши лучшие друзья. Аналитика, работа с localStorage, сторонние виджеты — всё это .client.
Оптимизация hydration: <ClientOnly>
Hydration — процесс когда Vue на клиенте «оживляет» HTML пришедший с сервера. Чем меньше компонентов нужно гидрировать — тем быстрее страница становится интерактивной.
<template>
<!— Этот блок не будет гидрироваться вообще —>
<ClientOnly>
<UserDashboard />
<template #fallback>
<!— Покажем скелетон пока JS не загрузился —>
<DashboardSkeleton />
</template>
</ClientOnly>
</template>
Кандидаты на <ClientOnly>: виджеты с персонализацией, блоки зависящие от localStorage/cookies, тяжёлые интерактивные компоненты которые не влияют на SEO.
Разбиваем бандл через динамические импорты
Для тяжёлых библиотек (графики, редакторы, карты) — только динамический импорт:
// composables/useChart.ts
export const useChart = async () => {
// Библиотека загрузится только когда вызовем функцию
const { Chart } = await import('chart.js')
return Chart
}
<script setup>
// Компонент с тяжёлой зависимостью — грузим лениво
const HeavyEditor = defineAsyncComponent(() =>
import('~/components/HeavyEditor.vue')
)
</script>
Анализируем что весит в бандле
Прежде чем оптимизировать — смотрим что именно тяжёлое:
# Установить анализатор
npm i -D rollup-plugin-visualizer
# nuxt.config.ts
export default defineNuxtConfig({
vite: {
plugins: [
visualizer({
open: true, // откроет браузер автоматически
gzipSize: true, // показывает размер после gzip
filename: 'stats.html'
})
]
}
})
# Запустить сборку с анализом
nuxt build
После этого увидите интерактивную карту бандла — сразу понятно что занимает место.
Настройка chunk splitting
// nuxt.config.ts
export default defineNuxtConfig({
vite: {
build: {
rollupOptions: {
output: {
// Выносим вендоры в отдельные чанки
manualChunks: {
'vue-vendor': ['vue', 'vue-router'],
'ui-vendor': ['@headlessui/vue'],
}
}
}
}
}
})
Это позволяет браузеру кешировать вендорные чанки отдельно — при обновлении вашего кода пользователи не перекачивают весь бандл заново.
Итог
Быстрый чеклист для старта: расставьте Lazy-префиксы на компоненты ниже fold, оберните персонализированные блоки в <ClientOnly>, проставьте суффиксы .client плагинам с аналитикой и сторонними виджетами. Уже эти три шага дадут ощутимый результат без глубокого рефакторинга. Для следующего уровня — запустите visualizer и разберитесь что именно тянет бандл вниз в вашем конкретном проекте.
