Почему PageSpeed важен для бизнеса
Скорость загрузки напрямую влияет на конверсию и выручку. По данным Google, при увеличении времени загрузки с 1 до 3
секунд вероятность отказа растёт на 32%. При увеличении до 5 секунд — на 90%. Для e-commerce каждая секунда задержки
снижает конверсию на 7%.
Google использует Core Web Vitals как фактор ранжирования. Сайты с плохими показателями получают меньше органического
трафика. Подробнее о влиянии технических факторов на позиции в поиске читайте в нашей статье
о SEO-продвижении.
Next.js — один из лучших фреймворков для производительных сайтов. Но из коробки он не гарантирует 100 баллов. Нужна
осознанная оптимизация. В этой статье мы покажем пошаговый процесс, который мы применяли на реальном проекте, подняв
PageSpeed с 50 до 98 баллов за один рабочий день.
Шаг 0: Аудит текущего состояния
Инструменты для аудита
Прежде чем что-то оптимизировать, нужно точно измерить текущее состояние. Мы используем три инструмента:
Google Lighthouse (встроен в Chrome DevTools) — даёт общую оценку и рекомендации. Запускайте в режиме Incognito,
чтобы расширения браузера не влияли на результат.
WebPageTest (webpagetest.org) — показывает waterfall загрузки, размеры ресурсов, порядок их загрузки. Позволяет
тестировать с разных локаций и устройств.
PageSpeed Insights (pagespeed.web.dev) — использует реальные данные пользователей (CrUX) вместе с лабораторными
тестами. Показывает, как сайт работает у настоящих пользователей.
Core Web Vitals: что измеряем
| Метрика |
Что измеряет |
Хорошо |
Нужно улучшить |
Плохо |
| LCP (Largest Contentful Paint) |
Загрузка главного контента |
< 2.5 сек |
2.5–4 сек |
> 4 сек |
| INP (Interaction to Next Paint) |
Отзывчивость интерфейса |
< 200 мс |
200–500 мс |
> 500 мс |
| CLS (Cumulative Layout Shift) |
Визуальная стабильность |
< 0.1 |
0.1–0.25 |
> 0.25 |
Наш исходный результат
На проекте, о котором пойдёт речь, начальные показатели были следующими:
- PageSpeed Mobile: 48/100
- LCP: 5.2 секунды
- INP: 380 мс
- CLS: 0.34
- Общий размер JS: 412 KB (gzip)
- Время до полной загрузки: 8.7 секунд (3G)
Шаг 1: Оптимизация изображений
Изображения — обычно самый большой ресурс на странице и главная причина медленного LCP. Next.js предоставляет мощный
компонент next/image, но его нужно использовать правильно.
Используйте next/image для всех изображений
// ПЛОХО: обычный img-тег
<img src="/hero.jpg" alt="Hero"/>;
// ХОРОШО: next/image с оптимизацией
import Image from "next/image";
<Image
src="/hero.jpg"
alt="Hero"
width={1200}
height={600}
priority // для LCP-изображения
sizes="(max-width: 768px) 100vw, 1200px"
/>;
Настройка форматов и качества
Next.js автоматически конвертирует изображения в WebP. Для ещё лучшего сжатия включите AVIF:
// next.config.js
const nextConfig = {
images: {
formats: ["image/avif", "image/webp"], // Настраиваем quality — баланс между размером и качеством
quality: 80, // Определяем размеры для responsive
deviceSizes: [640, 750, 828, 1080, 1200, 1920], imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
},
};
Blur Placeholder для мгновенного отображения
Blur placeholder показывает размытую версию изображения, пока оригинал загружается. Это убирает «прыжки» контента (CLS)
и создаёт ощущение мгновенной загрузки:
import Image from 'next/image';
import heroImage from '@/public/hero.jpg'; // статический импорт
// Для статических изображений blur генерируется автоматически
<Image
src={heroImage}
alt="Hero"
placeholder="blur"
priority
/>
// Для динамических — укажите blurDataURL вручную
<Image
src={product.imageUrl}
alt={product.name}
width={400}
height={300}
placeholder="blur"
blurDataURL="data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAAQABAAD..."
/>
Lazy Loading
По умолчанию next/image использует lazy loading — изображения загружаются только при приближении к viewport. Для
LCP-изображения (первое большое изображение на странице) отключите lazy loading через priority:
// LCP-изображение — загружается сразу
<Image src="/hero.jpg" alt="Hero" priority/>
// Остальные изображения — загружаются лениво (по умолчанию)
<Image src="/product.jpg" alt="Product"/>
Результат шага
- Размер изображений: уменьшился с 2.4 MB до 380 KB
- LCP: улучшился с 5.2 до 3.1 секунды
- CLS: снизился с 0.34 до 0.08
Шаг 2: Оптимизация шрифтов
Шрифты — вторая по значимости причина плохого LCP и CLS. Неоптимизированные шрифты вызывают FOUT (Flash of Unstyled
Text) или FOIT (Flash of Invisible Text), а их загрузка блокирует рендеринг.
Используйте next/font
next/font загружает шрифты на этапе сборки и self-hosting'ит их. Никаких запросов к Google Fonts в рантайме:
// app/layout.tsx
import {Inter, Roboto_Mono} from "next/font/google";
const inter = Inter({
subsets: ["latin", "cyrillic"],
display: "swap",
variable: "--font-inter",
});
const robotoMono = Roboto_Mono({
subsets: ["latin"],
display: "swap",
variable: "--font-roboto-mono",
});
export default function RootLayout({children}) {
return (
<html lang="ru" className={`${inter.variable} ${robotoMono.variable}`}>
<body className={inter.className}>{children}</body>
</html>
);
}
Локальные шрифты
Если используете кастомные шрифты, загрузите их через next/font/local:
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"},
],
display: "swap",
variable: "--font-my",
});
Подключение только нужных подмножеств
Кириллический подмножество (cyrillic) значительно меньше полного набора символов. Всегда указывайте только нужные
subsets.
Результат шага
- Размер шрифтов: с 180 KB до 45 KB
- CLS от шрифтов: 0 (font-display: swap + size-adjust)
Шаг 3: Оптимизация JavaScript-бандла
Анализ бандла
Первый шаг — понять, что занимает больше всего места. Установите bundle-analyzer:
npm install -D @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
Это откроет интерактивную карту бандла в браузере. Обычно самые большие «виновники» — это UI-библиотеки, библиотеки
дат (moment.js, date-fns), иконки и аналитика.
Dynamic Imports (Code Splitting)
Тяжёлые компоненты, которые не видны при первой загрузке, загружайте динамически:
import dynamic from "next/dynamic";
// Модальное окно — загружается только при открытии
const Modal = dynamic(() => import("@/components/modal"), {
loading: () => <div className="animate-pulse h-64 bg-gray-200 rounded"/>,
});
// Редактор — загружается только на странице редактирования
const RichEditor = dynamic(() => import("@/components/rich-editor"), {
ssr: false, // клиентский компонент, не нужен SSR
loading: () => <EditorSkeleton/>,
});
// Компонент графиков — тяжёлая библиотека
const Chart = dynamic(() => import("@/components/chart"), {
ssr: false,
});
Tree Shaking
Убедитесь, что используете именованные импорты вместо импорта всего модуля:
// ПЛОХО: импортирует всю библиотеку (200+ KB)
import _ from "lodash";
const sorted = _.sortBy(items, "name");
// ХОРОШО: импортирует только одну функцию (2 KB)
import sortBy from "lodash/sortBy";
const sorted = sortBy(items, "name");
// ЕЩЁ ЛУЧШЕ: нативный JS
const sorted = [...items].sort((a, b) => a.name.localeCompare(b.name));
Перенос вычислений на сервер
Если компонент использует тяжёлую библиотеку только для рендера, сделайте его Server Component. Например, подсветка
синтаксиса, рендеринг Markdown, форматирование дат. Код библиотеки не попадёт в клиентский бандл. Подробнее об этом
подходе читайте в нашей статье Server Components vs Client Components.
Результат шага
- Размер JS (gzip): с 412 KB до 127 KB
- INP: улучшился с 380 мс до 160 мс
Шаг 4: Оптимизация CSS
Tailwind CSS: минимизация
Если используете Tailwind CSS, убедитесь, что в production включён purge неиспользуемых классов. В Tailwind v3+ это
работает автоматически через content конфигурацию:
// tailwind.config.js
module.exports = {
content: ["./app/**/*.{js,ts,jsx,tsx,mdx}", "./components/**/*.{js,ts,jsx,tsx,mdx}", // Не добавляйте node_modules!
], theme: {
extend: {},
},
};
Critical CSS
Next.js автоматически инлайнит критический CSS при использовании CSS Modules или Tailwind. Но если вы подключаете
глобальные стили, они могут блокировать рендеринг.
// ПЛОХО: тяжёлый глобальный CSS-файл
import "@/styles/everything.css"; // 150 KB
// ХОРОШО: разделите стили по маршрутам
// app/layout.tsx
import "@/styles/base.css"; // 5 KB — только базовые стили
// app/dashboard/layout.tsx
import "@/styles/dashboard.css"; // 20 KB — только для дашборда
Удаление неиспользуемых CSS-библиотек
Проверьте, не подключены ли CSS-фреймворки, которые вы уже не используете. Часто в проектах остаётся подключённый
Bootstrap или Material UI CSS при переходе на Tailwind.
Результат шага
- Размер CSS: с 89 KB до 18 KB
- Время блокировки рендера CSS: с 800 мс до 120 мс
Шаг 5: Оптимизация сторонних скриптов
Проблема
Аналитика, чат-виджеты, рекламные скрипты и пиксели ретаргетинга — всё это загружается синхронно и блокирует рендеринг.
Один Google Tag Manager может добавить 200+ мс к LCP.
Используйте next/script
Next.js предоставляет компонент Script с разными стратегиями загрузки:
import Script from "next/script";
export default function RootLayout({children}) {
return (
<html lang="ru">
<body>
{children}
{/* afterInteractive — загружается после гидрации (по умолчанию) */}
<Script
src="https://www.googletagmanager.com/gtag/js?id=G-XXXXX"
strategy="afterInteractive"
/>
{/* lazyOnload — загружается в фоне после полной загрузки */}
<Script
src="https://widget.intercom.io/widget/xxxxx"
strategy="lazyOnload"
/>
{/* worker — выполняется в Web Worker (через Partytown) */}
<Script
src="https://www.google-analytics.com/analytics.js"
strategy="worker"
/>
</body>
</html>
);
}
Рекомендации по стратегиям
| Скрипт |
Стратегия |
Почему |
| Google Analytics / GTM |
afterInteractive |
Нужен для трекинга навигации |
| Яндекс.Метрика |
afterInteractive |
Аналогично GA |
| Чат-виджеты |
lazyOnload |
Не критичны для первой загрузки |
| Рекламные пиксели |
lazyOnload |
Не влияют на UX |
| A/B-тестирование |
beforeInteractive |
Должен загрузиться до рендера |
Результат шага
- LCP: улучшился с 2.4 до 1.6 секунды
- Total Blocking Time: с 450 мс до 90 мс
Шаг 6: Настройка кэширования
Заголовки кэширования
Настройте правильные cache-control заголовки для статических ресурсов:
// next.config.js
const nextConfig = {
async headers() {
return [{
// Статические ассеты (JS, CSS, шрифты) — кэшируем надолго
source: "/_next/static/:path*", headers: [{
key: "Cache-Control", value: "public, max-age=31536000, immutable",
},],
}, {
// Изображения — кэшируем на месяц
source: "/images/:path*", headers: [{
key: "Cache-Control", value: "public, max-age=2592000, stale-while-revalidate=86400",
},],
},];
},
};
ISR (Incremental Static Regeneration)
Для страниц с контентом, который обновляется нечасто, используйте ISR:
// app/blog/[slug]/page.tsx
export const revalidate = 3600; // ревалидация каждый час
export default async function BlogPost({params}) {
const {slug} = await params;
const post = await getPost(slug);
return <article>{/* ... */}</article>;
}
Обсудим ваш проект?
Оставьте контакты — перезвоним и обсудим задачу
Шаг 7: Дополнительные оптимизации
Prefetch ссылок
Next.js автоматически prefetch'ит ссылки, которые видны во viewport. Убедитесь, что используете next/link:
import Link from 'next/link';
// Prefetch включён по умолчанию для статических маршрутов
<Link href="/about">О компании</Link>
// Отключите для маршрутов, куда пользователь вряд ли перейдёт
<Link href="/terms" prefetch={false}>Условия использования</Link>
Используйте Metadata API Next.js для управления тегами head без клиентского JavaScript:
// app/layout.tsx
import type {Metadata} from "next";
export const metadata: Metadata = {
title: {
default: "My Site",
template: "%s | My Site",
},
description: "Описание сайта",
openGraph: {
type: "website",
locale: "ru_RU",
},
};
Compression
Убедитесь, что на уровне хостинга включена Gzip или Brotli-компрессия. Vercel и большинство CDN делают это
автоматически. Если используете собственный сервер:
// next.config.js
const nextConfig = {
compress: true, // включает gzip-сжатие (по умолчанию true)
};
Итоговый кейс: до и после
Вот сводная таблица результатов оптимизации нашего проекта — интернет-магазина на Next.js 14 (App Router):
| Метрика |
До оптимизации |
После оптимизации |
Изменение |
| PageSpeed Mobile |
48 |
98 |
+50 |
| PageSpeed Desktop |
72 |
100 |
+28 |
| LCP |
5.2 сек |
1.1 сек |
-79% |
| INP |
380 мс |
95 мс |
-75% |
| CLS |
0.34 |
0.02 |
-94% |
| JS размер (gzip) |
412 KB |
127 KB |
-69% |
| CSS размер |
89 KB |
18 KB |
-80% |
| Изображения |
2.4 MB |
380 KB |
-84% |
| Время полной загрузки (3G) |
8.7 сек |
2.8 сек |
-68% |
Распределение времени по шагам
| Шаг |
Время |
Вклад в PageSpeed |
| Изображения |
2 часа |
+18 баллов |
| Шрифты |
30 мин |
+6 баллов |
| JS-бандл |
3 часа |
+14 баллов |
| CSS |
1 час |
+4 балла |
| Скрипты |
1 час |
+5 баллов |
| Кэширование |
30 мин |
+3 балла |
Итого — около 8 часов работы одного разработчика. Если вы хотите, чтобы мы провели аналогичный аудит
и оптимизацию вашего проекта, свяжитесь с нами.
Чек-лист для быстрой проверки
Перед каждым релизом проходите по этому чек-листу:
Заключение
Оптимизация Next.js — это системный процесс, а не магия. Каждый шаг даёт измеримый результат, и при правильном подходе
можно добиться 90+ баллов PageSpeed за один рабочий день. Ключевые направления: изображения, шрифты, бандл JavaScript и
сторонние скрипты.
Не забывайте, что PageSpeed — это не разовая работа. С каждым новым функционалом нужно следить за тем, чтобы показатели
не деградировали. Включите Lighthouse CI в ваш пайплайн деплоя и отслеживайте метрики в Google Search Console. О том,
как организовать SEO-мониторинг для вашего проекта, мы рассказываем в отдельной статье.
Если вам нужна помощь с оптимизацией Next.js-проекта или полноценная разработка на Next.js с учётом
производительности с первого дня, наша команда готова помочь.