Как ускорил Next.js сайт до LCP 0.8s
Подробный кейс: какие метрики были до, что мешало LCP и какие точные изменения в конфигурации изображений и шрифтов дали LCP 0.8s. Практические фрагменты кода и инструкции для повторения.
Статья была полезной?
Подробный кейс: какие метрики были до, что мешало LCP и какие точные изменения в конфигурации изображений и шрифтов дали LCP 0.8s. Практические фрагменты кода и инструкции для повторения.
Статья была полезной?
В середине 2025 года я провёл комплексную оптимизацию публичного сайта на Next.js и добился LCP 0.8s на страницах с большим героем. В лидере перечислены конкретные изменения по изображениям, шрифтам и сборке, которые дали основной выигрыш времени.
Исходная страница была создана на Next.js (App Router, версия 13.x/14.x, проект развивался до 2025–2026 годов) с серверным рендерингом (SSR) для маркетинговых страниц. В январе 2025 мы зафиксировали проблемную метрику LCP на ключевой товарной странице — около 2.9–3.3 секунды по Lighthouse и 2.6–3.1 секунды по WebPageTest (реалистичный 4G). FCP и TTFB были в пределах приемлемого: TTFB ~ 200–250 мс при использовании Vercel, FCP ~ 1.0–1.4 с, но именно LCP отставал.
Для измерения и верификации использовались следующие инструменты и дата-источники:
Проблема: LCP рендерился за счёт большого hero-изображения и кастомного веб-шрифта, которые загружались и рендерились поздно. При этом код страницы был относительно лёгким: JavaScript бандлы после минимизации составляли ~120–180 KB (gzipped), но браузер ждал загрузки ресурса hero и ключевых шрифтов до отображения видимого слоя страницы.
Детальный аудит выявил несколько конкретных причин задержки LCP:
@font-face без preload, с font-display: auto, что приводило к блокировке LCP до получения глифов или FOUT/FOIT в некоторых браузерах.В сумме все эти факторы давали «узкое место» — браузер рендерил LCP только после загрузки героя и шрифта, задерживая окончательный визуальный ответ страницы.
Ключевая гипотеза: если сделать hero-изображение и ключевой шрифт приоритетными — LCP упадёт существенно. Мы внесли изменения в три слоя: сборка/конфигурация Next.js, серверную отдачу ресурсов и HTML-шаблон.
Принятые решения и изменения:
next/image с атрибутом priority и корректным sizes. Это позволило Next.js генерировать оптимальные srcset и отдавать заранее подготовленную версию.<link rel="preload" as="image" href="/path/to/hero.avif" importance="high" imagesrcset="..." imagesizes="..." crossorigin="" для критичного условия, чтобы браузер начал загрузку до JS.module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
port: '',
pathname: '/**',
},
],
},
experimental: {
modern: true,
},
};Пример использования в React-компоненте (App Router / server component):
<import Image from 'next/image' />
<Image
src="/static/hero-1200.avif"
alt="Главный баннер"
width={1200}
height={650}
priority
sizes="(max-width: 768px) 100vw, 1200px"
placeholder="blur"
blurDataURL="data:image/..."
/>Важно: мы использовали статический импорт для ключевого героя (файлы в /public или импортируемые импорты), чтобы Next.js мог встроить оптимизированный blurDataURL и оперативно обслуживать основные размеры.
Шрифты давали значительную фрагментацию рендера. Что сделано:
app/head или в собственном <head> (_document.js/_app.js для pages-router):<link rel="preload" href="/fonts/inter-subset.woff2" as="font" type="font/woff2" crossorigin>Альтернативно, если используется встроенный механизм Next.js (next/font), то применили server-side font loading, чтобы избежать FOIT и давать glyphs раньше. Пример с next/font:
import { Inter } from 'next/font/google'
const inter = Inter({
subsets: ['latin', 'cyrillic'],
display: 'swap',
preload: true,
})
export default function RootLayout({ children }) {
return (<html lang="ru" className={inter.className}><body>{children}</body></html>)
}Если используете кастомные локальные файлы через @next/font/local, устанавливайте preload: true и указывайте font-display: swap в @font-face.
Мы добавили в голову страницы явные подсказки:
<link rel="preconnect" href="https://cdn.example.com" crossorigin> для установления TCP/TLS заранее.Параметры «до» замеров, собранные в феврале 2025 (средние значения по 10 тестам WebPageTest и Lighthouse):
RUM по пользователям показывал медиану LCP ~2.8s для посетителей из Европы и ~3.4s для мобильных 3G/4G пользователей из некоторых регионов без CDN preconnect. Проблемы наиболее ярко проявлялись на мобильных устройствах с высокими DPR, где загружался большой ресурс изображения.
После внедрения изменений (апрель — май 2025) показатели улучшились существенно. Окончательный набор мер включал: генерацию AVIF, приоритетную загрузку героя, прелоад шрифта, preconnect к CDN, критический inline CSS и удаление ненужных third-party скриптов в critical path. Результаты:
RUM через 30 дней показал медиану LCP 0.95s для всех пользователей и 0.82s для пользователей в приоритетных регионах с современными браузерами. Это позволило поднять Overall Performance score в Lighthouse с ~55 до ~95 точек на тестовой странице.
Главный эффект дал не один хитрый трюк, а набор простых действий: оптимальные форматы изображений, прелоад ключевых ресурсов и уменьшение критического пути рендеринга.
Привожу упрощённый чек-лист и конкретные шаги для повторения оптимизации на вашем Next.js-проекте. Эти шаги подходят для App Router и Pages Router, небольшие правки зависят от структуры.
lighthouse https://example.com --output=json --output-path=./lh-report.json.next/image с атрибутом priority.remotePatterns в next.config.js и убедитесь, что Next.js может оптимизировать изображение.<link rel="preload" as="image" href="..." imagesrcset="..." imagesizes="..." crossorigin> для критических случаев.<link rel="preload" as="font" href="/fonts/...woff2" type="font/woff2" crossorigin>.display: swap и/или next/font с preload: true.preconnect для CDN, установите длительный cache-control для статических AVIF/WOFF2.Полезные примерные команды и Snippets:
# WebPageTest API пример (curl)
curl -X POST "https://www.webpagetest.org/runtest.php" \
-F "url=https://example.com" \
-F "k=YOUR_API_KEY" \
-F "location=ec2-lon_mobile" \
-F "runs=9"
# Lighthouse CLI
lighthouse https://example.com --mobile --output html --output-path=./lh-report.htmlРезюме по приоритетам для быстрого выигрыша:
next/image с priority.Этот кейс также дополняет материалы по производительности на нашем сайте: смотрите смежные рубрики Performance и Frontend для примеров конфигураций и шаблонов оптимизации. Практика показала, что системный подход дает куда больший эффект, чем попытки «улучшить» только JS-бандлы без работы с ресурсами рендера.
Если необходимо, могу прислать конкретный diff для вашего Next.js проекта (next.config.js, head и компонент Image), а также пример CI сценария для автоматической проверки LCP в pull request-ах (Lighthouse CI / PSI).
Комментарии (0)
Войдите или зарегистрируйтесь, чтобы оставить комментарий
Загрузка комментариев…