Web Vitals, bundle анализ, lazy-компоненты, NuxtImage, кэширование server routes, ISR и Lighthouse CI
Производительность — это не финальный шаг, а архитектурное решение. Правильные инструменты Nuxt позволяют достичь отличных показателей Web Vitals без глубоких оптимизаций, если использовать их с самого начала.
Google использует Core Web Vitals для ранжирования. Три ключевых метрики:
LCP (Largest Contentful Paint) — время до отрисовки самого большого элемента на странице (обычно hero-изображение или заголовок). Цель: менее 2.5 секунды. Основные причины плохого LCP: большие неоптимизированные изображения, медленные server routes, блокирующий JS.
INP (Interaction to Next Paint) — задержка от действия пользователя до визуального ответа. Заменил FID в 2024 году. Цель: менее 200 мс. Причины плохого INP: тяжёлые event handlers, блокировка главного потока.
CLS (Cumulative Layout Shift) — суммарное смещение элементов во время загрузки. Цель: менее 0.1. Основные причины: изображения без размеров, шрифты без font-display swap, динамически вставляемый контент.
Инструменты измерения: Lighthouse в Chrome DevTools, Web Vitals расширение, npm run build && npx lighthouse https://yoursite.com.
Перед оптимизацией нужно понять что занимает место:
# Запускает сборку с анализом бандла
npx nuxi analyze
# Или добавьте в package.json
# "analyze": "nuxt analyze"После сборки откроется интерактивный граф (rollup-plugin-visualizer). Ищите:
// nuxt.config.ts — подключение анализатора вручную
import { visualizer } from 'rollup-plugin-visualizer'
export default defineNuxtConfig({
vite: {
plugins: [
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true
})
]
}
})Nuxt автоматически разбивает код по маршрутам. Каждая страница — отдельный чанк, загружаемый по запросу. Но компоненты на странице по умолчанию включаются в её чанк.
Для тяжёлых компонентов используйте defineAsyncComponent или префикс Lazy:
<script setup lang="ts">
import { defineAsyncComponent } from 'vue'
// Явный async component с loading/error состояниями
const HeavyChart = defineAsyncComponent({
loader: () => import('@/components/HeavyChart.vue'),
loadingComponent: ChartSkeleton, // Пока грузится
errorComponent: ErrorBoundary, // Если ошибка
delay: 200, // Задержка перед показом loading
timeout: 10000 // Таймаут в мс
})
</script>
<template>
<!-- Prefix Lazy — то же самое, но автоматически -->
<LazyHeavyChart v-if="showChart" :data="chartData" />
<!-- Или явно -->
<HeavyChart v-if="showChart" :data="chartData" />
</template>Хорошие кандидаты для lazy loading: графики и диаграммы, редакторы (markdown, rich text), модальные окна и диалоги, компоненты ниже fold (не видны без прокрутки).
@nuxt/image — модуль для автоматической оптимизации изображений: конвертация в WebP/AVIF, изменение размера, lazy loading:
npm install @nuxt/image// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/image'],
image: {
// Провайдеры для внешних источников
providers: {
cloudinary: {
baseURL: 'https://res.cloudinary.com/your-cloud/'
}
},
// Разрешённые внешние домены
domains: ['images.unsplash.com', 'avatars.githubusercontent.com']
}
})Использование в шаблоне:
<template>
<!-- NuxtImg автоматически: WebP, lazy, правильный srcset -->
<NuxtImg
src="/images/hero.jpg"
alt="Hero изображение"
width="1200"
height="600"
sizes="sm:100vw md:80vw lg:1200px"
format="webp"
quality="80"
loading="eager"
/>
<!-- Для изображений ниже fold — lazy по умолчанию -->
<NuxtImg
src="/images/product.jpg"
alt="Продукт"
width="400"
height="300"
/>
</template>Критически важно указывать width и height — это предотвращает CLS (прыжок лейаута при загрузке).
npm install @nuxt/fonts@nuxt/fonts автоматически загружает шрифты, добавляет font-display: swap и генерирует font-size fallback для предотвращения CLS:
// nuxt.config.ts
export default defineNuxtConfig({
modules: ['@nuxt/fonts'],
fonts: {
defaults: {
weights: [400, 600, 700],
styles: ['normal'],
subsets: ['cyrillic', 'latin']
}
}
})/* В CSS просто используйте шрифт — модуль найдёт его автоматически */
body {
font-family: 'Inter', sans-serif;
}Модуль автоматически загружает Inter с Google Fonts, оптимизирует его и добавляет preload.
Один из самых эффективных способов ускорить API — кэшировать результаты тяжёлых запросов:
// server/api/popular-courses.get.ts
// cachedEventHandler кэширует результат handler функции
export default cachedEventHandler(
async (event) => {
// Эта функция выполнится только при cache miss
const courses = await db.courses.findMany({
where: { isPublished: true },
orderBy: { enrollmentCount: 'desc' },
take: 10,
include: { author: true, tags: true }
})
return courses
},
{
maxAge: 60 * 5, // TTL 5 минут
name: 'popular-courses', // Уникальный ключ кэша
getKey: () => 'all', // Фиксированный ключ (не зависит от запроса)
}
)Для кэширования отдельных функций (не обработчиков):
// server/utils/courses.ts
export const getCourseBySlug = defineCachedFunction(
async (slug: string) => {
return await db.courses.findBySlug(slug)
},
{
maxAge: 60 * 10, // 10 минут
name: 'course-by-slug',
getKey: (slug: string) => slug // Ключ зависит от аргумента
}
)Инвалидация кэша при обновлении данных:
// server/api/admin/courses/[id].patch.ts
import { useStorage } from 'nitropack/runtime'
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, 'id')
const body = await readBody(event)
await db.courses.update({ where: { id }, data: body })
// Инвалидируем кэш
const storage = useStorage()
await storage.removeItem(`nitro:functions:course-by-slug:${body.slug}.json`)
return { success: true }
})routeRules позволяет задать стратегию рендеринга для каждого маршрута:
// nuxt.config.ts
export default defineNuxtConfig({
routeRules: {
// Главная — ISR, перегенерируется раз в 60 секунд
'/': { isr: 60 },
// Статические маркетинговые страницы
'/about': { static: true },
'/pricing': { static: true },
// Блог — ISR с большим TTL
'/blog/**': { isr: 3600 },
// Dashboard — только SSR, никакого кэша
'/dashboard/**': { ssr: true },
// API — CORS и кэш заголовки
'/api/**': {
cors: true,
headers: { 'cache-control': 's-maxage=60' }
},
// Редирект
'/old-page': { redirect: '/new-page' }
}
})ISR (Incremental Static Regeneration) — лучшее из двух миров: страница генерируется статически (быстро, как CDN), но перегенерируется с заданным интервалом. Идеально для блогов, каталогов, лендингов.
swr (Stale While Revalidate) аналогичен ISR, но использует HTTP-заголовки вместо серверной логики — работает с CDN без специальной поддержки платформы.
<NuxtLink prefetch> предзагружает страницу когда ссылка появляется в viewport:
<template>
<!-- Prefetch — загрузить чанк когда ссылка видима -->
<NuxtLink to="/courses" prefetch>Курсы</NuxtLink>
<!-- Preload — загрузить немедленно (для критических ссылок) -->
<NuxtLink to="/dashboard" preload>Дашборд</NuxtLink>
</template>Глобальная настройка prefetch:
// nuxt.config.ts
export default defineNuxtConfig({
router: {
options: {
// Prefetch всех ссылок при наведении
linkActiveClass: 'active-link'
}
},
experimental: {
// Включить prefetch по умолчанию
renderJsonPayloads: true
}
})useFetch возвращает весь ответ API, но часто нужна только часть данных. pick и transform уменьшают размер payload:
// Без оптимизации — весь объект курса
const { data: course } = await useFetch(`/api/courses/${slug}`)
// С pick — только нужные поля
const { data: course } = await useFetch(`/api/courses/${slug}`, {
pick: ['title', 'description', 'author', 'price']
})
// С transform — произвольная трансформация на клиенте
const { data: courses } = await useFetch('/api/courses', {
transform: (courses) => courses.map(c => ({
id: c.id,
title: c.title,
slug: c.slug,
price: c.price
}))
})transform выполняется перед сохранением в payload. Это уменьшает как размер передаваемых данных, так и объём памяти на клиенте.
Nuxt DevTools показывает в реальном времени: какие компоненты используются, какие запросы делает приложение, состояние Pinia, размер payload:
# DevTools включены по умолчанию в dev режиме
npm run dev
# Откройте http://localhost:3000 и нажмите иконку Nuxt снизу страницыLighthouse CI в GitHub Actions:
# .github/workflows/lighthouse.yml
- name: Audit with Lighthouse CI
run: |
npm install -g @lhci/cli
npm run build
npm run preview &
sleep 5
lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}Конфигурация порогов в lighthouserc.json:
{
"ci": {
"assert": {
"preset": "lighthouse:no-pwa",
"assertions": {
"categories:performance": ["error", {"minScore": 0.9}],
"categories:seo": ["error", {"minScore": 0.9}],
"first-contentful-paint": ["warn", {"maxNumericValue": 2000}],
"largest-contentful-paint": ["error", {"maxNumericValue": 2500}]
}
}
}
}Настройте эти пороги так, чтобы они соответствовали реальным требованиям — слишком строгие пороги создают false negatives, слишком мягкие не защищают от регрессий производительности.
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.