Оптимизация производительности в Next.js
Производительность современных приложений во многом определяется на уровне фреймворка: от способа загрузки скриптов и изображений до стратегии рендеринга. Next.js поставляется с набором встроенных инструментов — next/link, next/image, next/font, next/script, кэшированием и динамическими импортами — которые позволяют ускорить приложение без самописных решений. В статье разберём, как эти механизмы работают, когда их стоит применять и на что обращать внимание при подготовке к собеседованию.
Содержание
- next/link — Оптимизация навигации
- next/image — Оптимизация изображений
- next/font — Оптимизация шрифтов
- next/script — Оптимизация скриптов
- Оптимизация рендеринга
- Кэширование в Next.js
- Metadata API — SEO оптимизация
- Sitemap — Карта сайта для SEO
- Dynamic Imports — Code Splitting
- Оптимизация third-party библиотек
- Bundle Analysis
- Edge Runtime
- Дополнительные оптимизации
- Чек-лист оптимизации
- Ключевые тезисы для интервью
- Заключение
- Полезные ссылки
next/link — Оптимизация навигации
Как работает Link
Компонент <Link> в Next.js автоматически оптимизирует навигацию между страницами через prefetching — предварительную загрузку страниц в фоне.
import Link from 'next/link'
export default function Navigation() {
return (
<nav>
<Link href="/">Главная</Link>
<Link href="/about">О нас</Link>
<Link href="/blog">Блог</Link>
</nav>
)
}
Prefetching в Next.js
Next.js использует два типа prefetching:
1. В viewport prefetching (автоматический)
- Когда ссылка появляется в viewport, Next.js предварительно загружает страницу
- Работает только в production build
- Загружает только код и данные для страницы
2. Hover/Touch prefetching
- При наведении мыши или касании на ссылку
- Происходит мгновенно, улучшает UX
Контроль prefetching
// Отключить prefetching
<Link href="/heavy-page" prefetch={false}>
Тяжелая страница
</Link>
// Условный prefetching
<Link
href="/dashboard"
prefetch={user?.isAuthenticated}
>
Dashboard
</Link>
Атрибут replace
Заменяет текущую запись в истории браузера вместо добавления новой:
// Пользователь не сможет вернуться назад к этой странице
<Link href="/login" replace>
Войти
</Link>
Scroll behavior
// Отключить автоматический скролл вверх
<Link href="/page" scroll={false}>
Перейти без скролла
</Link>
Лучшие практики для Link
- Оборачивайте только
<a>теги — Link уже рендерит<a> - Не оборачивайте
<button>— используйтеrouter.push()вместо этого - Используйте prefetch={false} для тяжелых страниц, которые редко посещаются
- Динамические маршруты — prefetch работает, но загружает только общий код
// Правильно
<Link href="/about">
<a>О нас</a> {/* В Next.js 13+ <a> не нужен */}
</Link>
// Неправильно - двойной <a>
<Link href="/about">
<a>
<a>О нас</a>
</a>
</Link>
next/image — Оптимизация изображений
Зачем нужен Image
Компонент <Image> автоматически оптимизирует изображения:
- Resize — изменение размера для разных устройств
- Format optimization — конвертация в WebP/AVIF
- Lazy loading — загрузка только при появлении в viewport
- Prevention of layout shift — предотвращение сдвигов макета
Базовое использование
import Image from 'next/image'
export default function Avatar() {
return (
<Image
src="/avatar.jpg"
alt="Мой аватар"
width={500}
height={500}
/>
)
}
Ключевые пропсы
Обязательные:
src— путь к изображениюalt— альтернативный текст (accessibility)widthиheight— размеры (для предотвращения layout shift)
Опциональные:
<Image
src="/photo.jpg"
alt="Фото"
width={800}
height={600}
quality={75} // Качество (1-100, по умолчанию 75)
priority={true} // Приоритетная загрузка (для above-the-fold)
placeholder="blur" // Плейсхолдер при загрузке
blurDataURL="data:..." // Blur hash для placeholder
sizes="(max-width: 768px) 100vw, 50vw" // Размеры для responsive
/>
Priority — критически важно
Используйте priority для изображений, которые видны сразу (above-the-fold):
// Hero image - загружается сразу
<Image
src="/hero.jpg"
alt="Hero"
width={1920}
height={1080}
priority
/>
// Остальные изображения загружаются лениво
<Image
src="/content.jpg"
alt="Content"
width={800}
height={600}
/>
Responsive images
<Image
src="/photo.jpg"
alt="Responsive photo"
width={1200}
height={800}
sizes="(max-width: 768px) 100vw,
(max-width: 1200px) 50vw,
33vw"
style={{
width: '100%',
height: 'auto',
}}
/>
Fill — для контейнеров с фиксированным размером
<div style={{ position: 'relative', width: '300px', height: '300px' }}>
<Image
src="/avatar.jpg"
alt="Avatar"
fill
style={{
objectFit: 'cover',
objectPosition: 'center',
}}
/>
</div>
Placeholder blur
<Image
src="/photo.jpg"
alt="Photo"
width={800}
height={600}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRg..."
/>
Оптимизация внешних изображений
Для изображений с внешних доменов нужна конфигурация:
// next.config.js
module.exports = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'images.example.com',
port: '',
pathname: '/account123/**',
},
],
},
}
<Image
src="https://images.example.com/photo.jpg"
alt="External photo"
width={800}
height={600}
/>
Лучшие практики для Image
- Всегда указывайте width и height — предотвращает layout shift
- Используйте priority для above-the-fold изображений
- Настройте sizes для responsive — браузер выбирает оптимальный размер
- Используйте placeholder=“blur” для улучшения UX
- Оптимизируйте исходные изображения — Image не заменяет базовую оптимизацию
- Используйте современные форматы — WebP/AVIF автоматически
next/font — Оптимизация шрифтов
Зачем нужен Font
Компонент next/font автоматически оптимизирует шрифты:
- Self-hosting — шрифты хостятся локально, без внешних запросов
- Zero layout shift — шрифты загружаются до рендеринга
- Automatic font optimization — удаление неиспользуемых символов
Google Fonts
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin', 'cyrillic'],
weight: ['400', '500', '700'],
display: 'swap',
})
export default function RootLayout({ children }) {
return (
<html lang="ru" className={inter.className}>
<body>{children}</body>
</html>
)
}
Локальные шрифты
import localFont from 'next/font/local'
const myFont = localFont({
src: [
{
path: '../fonts/MyFont-Regular.woff2',
weight: '400',
style: 'normal',
},
{
path: '../fonts/MyFont-Bold.woff2',
weight: '700',
style: 'normal',
},
],
})
export default function RootLayout({ children }) {
return (
<html lang="ru" className={myFont.className}>
<body>{children}</body>
</html>
)
}
Применение к конкретным элементам
import { Roboto } from 'next/font/google'
const roboto = Roboto({
subsets: ['latin'],
weight: ['400', '700'],
})
export default function Heading() {
return (
<h1 className={roboto.className}>
Заголовок с Roboto
</h1>
)
}
CSS переменные
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin'],
variable: '--font-inter',
})
export default function RootLayout({ children }) {
return (
<html lang="ru" className={inter.variable}>
<body style={{ fontFamily: 'var(--font-inter)' }}>
{children}
</body>
</html>
)
}
Лучшие практики для Font
- Используйте только нужные веса — не загружайте все варианты
- Указывайте subsets — загружайте только нужные символы
- Используйте display: ‘swap’ — показывает fallback шрифт до загрузки
- Предпочитайте variable fonts — один файл для всех весов
- Self-hosting быстрее — нет внешних запросов
next/script — Оптимизация скриптов
Стратегии загрузки
import Script from 'next/script'
// beforeInteractive - загружается до гидратации (для критических скриптов)
<Script
src="https://example.com/critical.js"
strategy="beforeInteractive"
/>
// afterInteractive - загружается после гидратации (по умолчанию)
<Script
src="https://example.com/analytics.js"
strategy="afterInteractive"
/>
// lazyOnload - загружается когда браузер idle
<Script
src="https://example.com/chat.js"
strategy="lazyOnload"
/>
// worker - загружается в web worker (experimental)
<Script
src="https://example.com/heavy.js"
strategy="worker"
/>
Inline scripts
<Script id="inline-script">
{`
console.log('Inline script loaded');
`}
</Script>
Обработка событий
<Script
src="https://example.com/script.js"
onReady={() => {
console.log('Script ready');
}}
onLoad={() => {
console.log('Script loaded');
}}
onError={(e) => {
console.error('Script error', e);
}}
/>
Лучшие практики для Script
- beforeInteractive — только для критических скриптов (analytics, security)
- afterInteractive — по умолчанию для большинства скриптов
- lazyOnload — для некритичных скриптов (chat widgets, social)
- Не используйте strategy=“beforeInteractive” вне
_app.jsили_document.js - Группируйте скрипты — используйте один Script для нескольких функций
Оптимизация рендеринга
Статическая генерация (SSG)
// Генерируется на build time
export default function BlogPost({ post }) {
return <article>{post.title}</article>
}
export async function getStaticProps() {
const post = await fetchPost()
return {
props: { post },
revalidate: 3600, // ISR - ревалидация каждый час
}
}
Server-Side Rendering (SSR)
// Генерируется на каждый запрос
export default function Dashboard({ data }) {
return <div>{data.value}</div>
}
export async function getServerSideProps(context) {
const data = await fetchData(context.req)
return {
props: { data },
}
}
Streaming и Suspense
import { Suspense } from 'react'
export default function Page() {
return (
<div>
<h1>Главная страница</h1>
<Suspense fallback={<Loading />}>
<SlowComponent />
</Suspense>
</div>
)
}
async function SlowComponent() {
const data = await fetchSlowData()
return <div>{data.content}</div>
}
function Loading() {
return <div>Загрузка...</div>
}
Когда что использовать
SSG (Static Site Generation):
- Контент меняется редко
- Блоги, документация, маркетинговые страницы
- Максимальная производительность
SSR (Server-Side Rendering):
- Динамический контент
- Персонализированные страницы
- Данные меняются на каждый запрос
ISR (Incremental Static Regeneration):
- Контент меняется периодически
- Можно обновлять без пересборки
- Лучшее из обоих миров
Кэширование в Next.js
Data Cache
// Кэширование данных
export async function getStaticProps() {
const data = await fetch('https://api.example.com/data', {
next: { revalidate: 3600 }, // Кэшировать на 1 час
})
return { props: { data } }
}
Router Cache
// Отключить кэширование маршрута
export const dynamic = 'force-dynamic'
export const revalidate = 0
export default function Page() {
return <div>Всегда свежая страница</div>
}
Full Route Cache
// Кэшировать весь маршрут
export const dynamic = 'force-static'
export default function CachedPage() {
return <div>Эта страница кэшируется полностью</div>
}
Управление кэшем
import { revalidatePath, revalidateTag } from 'next/cache'
export async function revalidateData() {
'use server'
// Ре-валидировать конкретный путь
revalidatePath('/blog')
// Ре-валидировать по тегу
revalidateTag('blog-posts')
}
Metadata API — SEO оптимизация
Как работает generateMetadata
Функция generateMetadata() автоматически генерирует <meta> теги в <head> страницы. Вам не нужно вручную писать HTML — Next.js делает это за вас.
Что происходит под капотом:
// app/blog/[slug]/page.tsx
export async function generateMetadata({ params }) {
const post = await getPost(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
images: [post.coverImage],
},
}
}
Next.js автоматически превратит это в:
<head>
<title>Мой пост</title>
<meta name="description" content="Краткое описание поста" />
<meta property="og:title" content="Мой пост" />
<meta property="og:image" content="https://example.com/image.jpg" />
</head>
Статические metadata
Используйте когда данные известны на build time:
export const metadata = {
title: 'Моя страница',
description: 'Описание страницы',
openGraph: {
title: 'Моя страница',
description: 'Описание для соцсетей',
images: ['/og-image.png'],
},
}
export default function Page() {
return <div>Контент</div>
}
Динамические metadata
Используйте когда данные зависят от параметров маршрута:
export async function generateMetadata({ params }) {
const post = await getPost(params.id)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
images: [post.coverImage],
},
}
}
export default function BlogPost({ params }) {
return <div>Контент поста</div>
}
Template pattern — единый формат заголовков
Используйте template в layout для автоматического добавления суффикса ко всем заголовкам:
// app/layout.tsx
export const metadata = {
title: {
template: '%s | Мой сайт',
default: 'Мой сайт',
},
}
// app/about/page.tsx
export const metadata = {
title: 'О нас', // Станет "О нас | Мой сайт"
}
// app/blog/page.tsx
export const metadata = {
title: 'Блог', // Станет "Блог | Мой сайт"
}
Где размещать metadata
В page.tsx — для конкретной страницы:
// app/page.tsx
export const metadata = {
title: 'Главная',
}
export default function HomePage() {
return <div>Главная страница</div>
}
В layout.tsx — применяется ко всем дочерним страницам:
// app/layout.tsx
export const metadata = {
title: 'Мой сайт',
description: 'Общее описание сайта',
}
export default function RootLayout({ children }) {
return (
<html>
<body>{children}</body>
</html>
)
}
Что можно возвращать
return {
// Базовые
title: 'Заголовок страницы',
description: 'Описание',
// Open Graph (для соцсетей — Facebook, VK, LinkedIn)
openGraph: {
title: 'OG Заголовок',
description: 'OG Описание',
images: ['/og-image.png'],
type: 'website',
},
// Twitter Card
twitter: {
card: 'summary_large_image',
title: 'Twitter Заголовок',
description: 'Twitter Описание',
images: ['/twitter-image.png'],
},
// Robots — управление индексацией
robots: {
index: true,
follow: true,
},
// Canonical URL — предотвращает дубликаты
alternates: {
canonical: 'https://example.com/page',
},
// Иконки
icons: {
icon: '/favicon.ico',
},
}
Важные моменты
- Вызывается на сервере — можно делать запросы к БД, API
- Не имеет доступа к
request— используйтеheaders()изnext/headers - Наследуется — metadata из layout применяется ко всем дочерним страницам
- Переопределяется — metadata в page.tsx переопределяет layout.tsx
Пример с контекстом запроса
import { headers } from 'next/headers'
export async function generateMetadata({ params }) {
const headersList = headers()
const host = headersList.get('host')
return {
title: 'Страница',
alternates: {
canonical: `https://${host}/${params.slug}`,
},
}
}
Sitemap — Карта сайта для SEO
Что такое sitemap
Sitemap (sitemap.xml) — это XML-файл, который сообщает поисковым системам о всех страницах сайта, их приоритете и частоте обновления. Next.js предоставляет встроенный способ генерации sitemap через файл app/sitemap.ts.
Базовая генерация sitemap
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'yearly',
priority: 1,
},
{
url: 'https://example.com/about',
lastModified: new Date(),
changeFrequency: 'monthly',
priority: 0.8,
},
{
url: 'https://example.com/blog',
lastModified: new Date(),
changeFrequency: 'weekly',
priority: 0.5,
},
]
}
Next.js автоматически сгенерирует файл sitemap.xml по адресу https://example.com/sitemap.xml.
Динамический sitemap с данными из БД
Для сайтов с динамическим контентом (блог, магазин) генерируйте sitemap из реальных данных:
// app/sitemap.ts
import { MetadataRoute } from 'next'
async function getAllPosts() {
const res = await fetch('https://api.example.com/posts')
return res.json()
}
export default async function sitemap(): MetadataRoute.Sitemap {
const posts = await getAllPosts()
const staticRoutes = [
{
url: 'https://example.com',
lastModified: new Date(),
changeFrequency: 'daily' as const,
priority: 1,
},
{
url: 'https://example.com/about',
lastModified: new Date(),
changeFrequency: 'monthly' as const,
priority: 0.8,
},
]
const blogRoutes = posts.map((post) => ({
url: `https://example.com/blog/${post.slug}`,
lastModified: new Date(post.updatedAt),
changeFrequency: 'weekly' as const,
priority: 0.6,
}))
return [...staticRoutes, ...blogRoutes]
}
Sitemap Index — для больших сайтов
Если у вас более 50 000 URL, нужно разбить sitemap на несколько файлов и создать sitemap index:
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com/sitemap/posts.xml',
lastModified: new Date(),
},
{
url: 'https://example.com/sitemap/pages.xml',
lastModified: new Date(),
},
]
}
Параметры sitemap
Каждый URL в sitemap поддерживает следующие поля:
{
url: string, // Обязательный — полный URL страницы
lastModified?: string | Date, // Когда страница последний раз обновлялась
changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never',
priority?: number, // Приоритет от 0.0 до 1.0 (по умолчанию 0.5)
alternates?: { // Альтернативные версии страницы
languages?: Record<string, string>, // hreflang ссылки
},
}
Sitemap с альтернативными языками (hreflang)
Для мультиязычных сайтов добавьте hreflang ссылки:
// app/sitemap.ts
import { MetadataRoute } from 'next'
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: 'https://example.com/about',
lastModified: new Date(),
alternates: {
languages: {
'en-US': 'https://example.com/en/about',
'ru-RU': 'https://example.com/ru/about',
'de-DE': 'https://example.com/de/about',
},
},
},
]
}
Robots.txt — указание на sitemap
Добавьте ссылку на sitemap в robots.txt:
// app/robots.ts
import { MetadataRoute } from 'next'
export default function robots(): MetadataRoute.Robots {
return {
rules: {
userAgent: '*',
allow: '/',
disallow: '/admin/',
},
sitemap: 'https://example.com/sitemap.xml',
}
}
Лучшие практики для sitemap
- Генерируйте динамически — sitemap должен отражать реальное состояние сайта
- Указывайте lastModified — помогает поисковикам понимать свежесть контента
- Используйте changeFrequency —
dailyдля блога,monthlyдля статических страниц - Приоритеты имеют смысл — главная страница = 1.0, важные разделы = 0.8, остальные = 0.5
- Разбивайте на несколько файлов — если URL больше 50 000
- Добавьте в robots.txt — поисковики найдут sitemap автоматически
- Кэшируйте при необходимости — для больших сайтов используйте ISR
Dynamic Imports — Code Splitting
Что такое Code Splitting
Code splitting — это процесс разделения вашего кода на отдельные части (чанки), которые загружаются по требованию. Вместо одного большого бандла, который загружается сразу, Next.js создает несколько меньших файлов, которые загружаются только когда они нужны.
Как работает под капотом:
// Обычный импорт — весь код загружается сразу
import HeavyChart from './HeavyChart' // Добавляет ~200KB к основному бандлу
// Динамический импорт — код загружается только когда компонент используется
const HeavyChart = dynamic(() => import('./HeavyChart')) // Загружается отдельным чанком
Когда вы используете dynamic(), Webpack создает отдельный файл (чанк) для этого компонента. Этот чанк загружается только когда компонент действительно нужен пользователю.
Базовое использование
import dynamic from 'next/dynamic'
// Динамический импорт компонента
const HeavyComponent = dynamic(() => import('../components/HeavyComponent'))
export default function Page() {
return (
<div>
<h1>Легкая часть</h1>
<HeavyComponent />
</div>
)
}
Что происходит:
- При загрузке страницы загружается только основной бандл
- Когда React достигает
<HeavyComponent />, браузер загружает отдельный чанк - Компонент рендерится
С loading компонентом
Пока чанк загружается, можно показать loading состояние:
const HeavyComponent = dynamic(
() => import('../components/HeavyComponent'),
{
loading: () => <div>Загрузка тяжелого компонента...</div>,
}
)
Отключение SSR (ssr: false)
Некоторые компоненты работают только на клиенте (используют window, document, браузерные API). Для них нужно отключить серверный рендеринг:
const MapComponent = dynamic(
() => import('../components/MapComponent'),
{
ssr: false, // Компонент рендерится только на клиенте
loading: () => <div>Загрузка карты...</div>,
}
)
Когда использовать ssr: false:
- Компоненты используют
window,document,localStorage - Библиотеки которые не поддерживают SSR (Leaflet,某些 chart libraries)
- Компоненты которые зависят от размера окна браузера
Реальные примеры
Карта (Leaflet/Mapbox)
import dynamic from 'next/dynamic'
const Map = dynamic(
() => import('../components/Map'),
{
ssr: false,
loading: () => (
<div style={{ height: '400px', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
Загрузка карты...
</div>
),
}
)
export default function Page() {
return (
<div>
<h1>Наша локация</h1>
<Map />
</div>
)
}
// components/Map.tsx
import { MapContainer, TileLayer, Marker } from 'react-leaflet'
import 'leaflet/dist/leaflet.css'
export default function Map() {
return (
<MapContainer center={[51.505, -0.09]} zoom={13} style={{ height: '400px' }}>
<TileLayer url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png" />
<Marker position={[51.505, -0.09]} />
</MapContainer>
)
}
Rich Text Editor
const RichTextEditor = dynamic(
() => import('../components/RichTextEditor'),
{ ssr: false }
)
export default function BlogEditor() {
const [content, setContent] = useState('')
return (
<div>
<h1>Редактор статьи</h1>
<RichTextEditor value={content} onChange={setContent} />
</div>
)
}
График (Chart.js/Recharts)
const SalesChart = dynamic(
() => import('../components/SalesChart'),
{
loading: () => <div style={{ height: '300px' }}>Загрузка графика...</div>,
}
)
export default function Dashboard() {
return (
<div>
<h2>Продажи за месяц</h2>
<SalesChart data={salesData} />
</div>
)
}
Условная загрузка компонентов
Загружайте компоненты только когда они действительно нужны:
'use client'
import { useState } from 'react'
import dynamic from 'next/dynamic'
const CommentsSection = dynamic(() => import('../components/Comments'))
export default function Article({ article }) {
const [showComments, setShowComments] = useState(false)
return (
<article>
<h1>{article.title}</h1>
<p>{article.content}</p>
{!showComments ? (
<button onClick={() => setShowComments(true)}>
Показать комментарии
</button>
) : (
<CommentsSection articleId={article.id} />
)}
</article>
)
}
Преимущества:
- Комментарии загружаются только если пользователь хочет их читать
- Экономия трафика и ускорение начальной загрузки
Динамический импорт библиотек
Для внешних библиотек используйте useEffect + динамический импорт:
'use client'
import { useEffect, useState } from 'react'
export default function Chart() {
const [Chart, setChart] = useState(null)
useEffect(() => {
// Загружаем библиотеку только на клиенте
import('chart.js/auto').then((mod) => {
setChart(() => mod.Chart)
})
}, [])
useEffect(() => {
if (!Chart) return
const ctx = document.getElementById('myChart')
const chart = new Chart(ctx, {
type: 'bar',
data: { /* ... */ }
})
return () => chart.destroy()
}, [Chart])
if (!Chart) return <div>Загрузка библиотеки графиков...</div>
return <canvas id="myChart" />
}
Загрузка по скроллу (Intersection Observer)
Загружайте компоненты когда они появляются в viewport:
'use client'
import { useEffect, useRef, useState } from 'react'
import dynamic from 'next/dynamic'
const HeavyGallery = dynamic(() => import('../components/Gallery'))
export default function Page() {
const [isVisible, setIsVisible] = useState(false)
const ref = useRef()
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.disconnect()
}
},
{ threshold: 0.1 }
)
if (ref.current) {
observer.observe(ref.current)
}
return () => observer.disconnect()
}, [])
return (
<div>
<h1>Наша галерея</h1>
<p>Прокрутите вниз чтобы увидеть галерею</p>
<div ref={ref} style={{ minHeight: '500px' }}>
{isVisible ? (
<HeavyGallery />
) : (
<div>Галерея появится здесь...</div>
)}
</div>
</div>
)
}
Сравнение с обычным импортом
// Обычный импорт
import HeavyComponent from './HeavyComponent'
export default function Page() {
return <HeavyComponent />
}
// Размер бандла: ~500KB (включая HeavyComponent)
// Время загрузки: сразу при открытии страницы
// Динамический импорт
const HeavyComponent = dynamic(() => import('./HeavyComponent'))
export default function Page() {
return <HeavyComponent />
}
// Размер бандла: ~300KB (без HeavyComponent)
// Размер отдельного чанка: ~200KB
// Время загрузки: HeavyComponent загружается только когда нужен
Когда использовать Dynamic Imports
Используйте когда:
- Компонент большой (>50KB)
- Компонент используется редко (модальные окна, сложные формы)
- Компонент не работает с SSR
- Пользователь может не увидеть компонент сразу
- Библиотека работает только на клиенте
НЕ используйте когда:
- Компонент маленький (<10KB)
- Компонент критичен для первого рендера (header, navigation)
- Компонент всегда виден на странице
- Вы хотите SEO для этого контента
Лучшие практики
- Не динамизируйте всё — мелкие компоненты лучше импортировать статически
- Всегда добавляйте loading состояние — улучшает UX
- Используйте
ssr: falseтолько когда необходимо — теряется SEO - Тестируйте с отключенным JavaScript — убедитесь что базовый контент доступен
- Анализируйте bundle — используйте
@next/bundle-analyzerдля поиска тяжелых компонентов - Группируйте связанные компоненты — если несколько компонентов всегда используются вместе, импортируйте их в одном файле
Пример с группировкой
// components/Editor/index.tsx
import dynamic from 'next/dynamic'
export const RichTextEditor = dynamic(() => import('./RichTextEditor'))
export const MarkdownEditor = dynamic(() => import('./MarkdownEditor'))
export const CodeEditor = dynamic(() => import('./CodeEditor'))
// page.tsx
import { RichTextEditor, MarkdownEditor } from '../components/Editor'
export default function Page() {
return (
<div>
<RichTextEditor />
<MarkdownEditor />
</div>
)
}
Оптимизация third-party библиотек
Избегайте больших зависимостей
// Плохо - загружает всю библиотеку
import { BigNumber } from 'bignumber.js'
// Хорошо - загружает только нужное
import BigNumber from 'bignumber.js/lib/bignumber'
Используйте tree-shaking
// Плохо
import _ from 'lodash'
_.debounce(fn, 300)
// Хорошо
import { debounce } from 'lodash-es'
debounce(fn, 300)
Conditionally load libraries
'use client'
import { useEffect, useState } from 'react'
export function RichTextEditor() {
const [Editor, setEditor] = useState(null)
useEffect(() => {
// Загружаем только когда компонент монтируется
import('react-quill').then((mod) => {
setEditor(() => mod.default)
})
}, [])
if (!Editor) return <textarea />
return <Editor />
}
Bundle Analysis
Анализ бандла
# Установите анализатор
npm install @next/bundle-analyzer
# next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer({
// ваши настройки
})
# Запустите анализ
ANALYZE=true npm run build
Оптимизация на основе анализа
- Identify large bundles — найдите большие модули
- Split code — разделите на чанки
- Remove unused code — удалите неиспользуемые зависимости
- Use lighter alternatives — замените тяжелые библиотеки
Edge Runtime
Использование Edge Runtime
export const runtime = 'edge'
export default function EdgePage() {
return <div>Эта страница выполняется на Edge</div>
}
Когда использовать Edge
Подходит:
- Низкая задержка (latency)
- Геолокация
- A/B тестирование
- Простые страницы
Не подходит:
- Тяжелые вычисления
- Node.js API
- Большие зависимости
Дополнительные оптимизации
Оптимизация API Routes
// Используйте кэширование
export async function GET(request) {
const data = await fetchData()
return new Response(JSON.stringify(data), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, s-maxage=3600',
},
})
}
Middleware оптимизация
import { NextResponse } from 'next/server'
export function middleware(request) {
// Легкая логика - быстрое выполнение
const country = request.geo?.country || 'US'
const response = NextResponse.next()
response.headers.set('x-country', country)
return response
}
export const config = {
matcher: ['/((?!api|_next/static|favicon.ico).*)'],
}
Оптимизация Client Components
'use client'
import { memo, useState } from 'react'
// Мемоизация компонента
const ExpensiveComponent = memo(function ExpensiveComponent({ data }) {
return <div>{/* тяжелый рендеринг */}</div>
})
export default function Page() {
const [count, setCount] = useState(0)
return (
<div>
<button onClick={() => setCount(c => c + 1)}>
Count: {count}
</button>
<ExpensiveComponent data={data} />
</div>
)
}
Чек-лист оптимизации
Производительность
- Использовать
<Link>вместо<a>для внутренней навигации - Настроить
prefetchдля критических ссылок - Использовать
<Image>для всех изображений - Добавить
priorityдля above-the-fold изображений - Оптимизировать шрифты через
next/font - Использовать правильные стратегии для
<Script> - Включить Streaming и Suspense
- Настроить ISR для динамического контента
Bundle Size
- Проанализировать bundle с
@next/bundle-analyzer - Использовать dynamic imports для тяжелых компонентов
- Удалить неиспользуемые зависимости
- Использовать tree-shaking совместимые импорты
- Загружать библиотеки условно
Кэширование
- Настроить Data Cache для API запросов
- Использовать Router Cache appropriately
- Настроить Full Route Cache для статических страниц
- Использовать
revalidatePathиrevalidateTag
SEO
- Настроить Metadata API для всех страниц
- Добавить Open Graph теги
- Оптимизировать title и description
- Создать sitemap.xml через app/sitemap.ts
- Добавить ссылку на sitemap в robots.txt
- Использовать семантический HTML
Edge Cases
- Тестировать на медленных сетях
- Проверять Lighthouse score
- Мониторить Core Web Vitals
- Оптимизировать для мобильных устройств
Ключевые тезисы для интервью
next/linkавтоматически prefetchит страницы при попадании в viewport или при наведении, но для тяжёлых страниц prefetch можно отключить.next/imageоптимизирует размер, формат и порядок загрузки изображений, а явныеwidth/heightпредотвращают layout shift.priorityвnext/imageиспользуется для изображений above-the-fold, остальные загружаются лениво.next/fontсамостоятельно хостит шрифты, убирает внешние запросы и снижает layout shift за счёт правильногоdisplay.next/scriptпредлагает стратегииbeforeInteractive,afterInteractive,lazyOnloadиworker, позволяя контролировать порядок загрузки скриптов.- SSG подходит для редко меняющегося контента, SSR — для персонализированных данных, ISR совмещает преимущества обоих подходов.
- Next.js использует несколько уровней кэша: Data Cache, Router Cache и Full Route Cache; валидировать их можно через
revalidatePathиrevalidateTag. - Metadata API позволяет декларативно задавать
<meta>-теги, Open Graph, Twitter Card и canonical URL на уровне страницы и layout. - Sitemap и robots.txt можно генерировать через
app/sitemap.tsиapp/robots.ts, что упрощает SEO-настройку. dynamic()откладывает загрузку компонентов и библиотек, аssr: falseиспользуется только для компонентов, завязанных на браузерные API.- Tree-shaking, условная загрузка библиотек и анализ бандла помогают уменьшить размер клиентского JavaScript.
- Edge Runtime ускоряет ответ за счёт близости к пользователю, но не подходит для тяжёлых вычислений и Node.js API.
Заключение
Next.js предоставляет мощные инструменты оптимизации из коробки. Ключ к максимальной производительности — понимание того, как работают эти инструменты и когда их применять. Используйте чек-лист выше для аудита вашего приложения и постепенно внедряйте оптимизации.
Помните: измеряйте перед оптимизацией. Используйте Lighthouse, Web Vitals и реальные метрики пользователей для определения узких мест. Не оптимизируйте то, что не измеряли.