гора.
запись · 4 мая 2026 г. · 9 мин чтения

Core Web Vitals — LCP, INP, CLS на простом языке

Открываешь PageSpeed Insights, вбиваешь свой домен, ждёшь 15 секунд. И получаешь четыре оранжевых кружка и один красный. Под ними — буквы LCP, INP, CLS, FCP, TTFB. Ниже список из тридцати "возможностей" на английском: eliminate render-blocking resources, reduce unused JavaScript, properly size images. Скроллишь. Ничего не понятно. Закрываешь.

Я через это прошёл. И с десятками клиентов через это прошёл. Поэтому давай разберёмся, что из этого реально важно, а что — шум.

Из всего отчёта PSI имеет значение только три буквы: LCP, INP, CLS. Это Core Web Vitals. Google публично подтвердил, что они входят в сигналы ранжирования с июня 2021. И с тех пор только усиливал их вес. Из общих 30+ факторов SEO Core Web Vitals — единственное, что Google официально измеряет через браузеры реальных пользователей.

Метрики обновлялись. В марте 2024 INP заменил собой FID (First Input Delay). FID считал только задержку первого клика. INP смотрит на все взаимодействия за визит и берёт худшие из них. Жёстче, реалистичнее, и убить его теперь сложнее.

Цель этой статьи — объяснить три метрики человеческим языком, дать конкретные таргеты и показать, что делать в коде. Без воды про "ускорьте ваш сайт".


LCP — Largest Contentful Paint

Это время от начала загрузки страницы до того момента, как пользователь видит самый большой элемент в видимой части экрана. Обычно это hero-картинка, видео или большой блок с h1. Браузер сам решает, что самое крупное.

Таргеты:

  • < 2.5s — хорошо
  • 2.5–4s — нужно работать
  • 4s — плохо

Эти 2.5 секунды — не от твоего клика. Они от момента, когда браузер начал получать первый байт. То есть DNS, TCP, TLS, ответ сервера, скачивание HTML, парсинг, скачивание картинки, рендер — всё это должно уложиться в 2500 миллисекунд.

Что обычно ломает LCP:

Тяжёлая hero-картинка. Дизайнер прислал JPEG 4000×3000 на 2.8 МБ. Ты её прикрутил без оптимизации. Браузер скачивает 2.8 МБ через 4G, LCP уходит за 5 секунд. Решается через next/image с priority, WebP/AVIF, корректными размерами. На Next 15 — <Image src={hero} priority placeholder="blur" /> и всё.

Render-blocking JS и CSS. В <head> стоят синхронные <script> или огромный CSS-файл. Браузер не может отрисовать DOM, пока их не скачает и не выполнит. Решение: defer/async на скрипты, выноси критичный CSS инлайном, остальное — отдельным файлом с media="print" трюком или через критический CSS.

Медленный TTFB. Time To First Byte. Сервер думает 800 мс перед тем, как отдать HTML. Причины: серверный рендер без кэша, тяжёлая БД, холодный старт serverless-функции, географически далёкий хостинг. Решение: CDN перед всем (Cloudflare, Vercel Edge), кэширование на уровне страницы, ISR для статичных частей.

Отсутствие preload для критичных ресурсов. Шрифт, который используется в h1, начинает грузиться только после парсинга CSS. Между этим — пустое место. Поставь <link rel="preload" as="font" href="..." crossorigin> для main-шрифта, и LCP падает на 300–500 мс.

Что не делать: lazy-load на hero-картинку. Это классическая ошибка. У тебя <img loading="lazy"> на самой LCP-картинке, и она ждёт, пока scroll-observer её активирует. LCP взлетает до небес. На hero — loading="eager" и fetchpriority="high".

Ещё одна тонкость: LCP-элемент может меняться по ходу загрузки страницы. Сначала это h1, потом отрисовался hero — стал hero. Потом догрузилась видео-обложка — стала она. Браузер фиксирует финальный LCP только когда страница уходит в background или происходит первое взаимодействие. Поэтому если у тебя анимация, которая выезжает на hero через секунду — LCP считается с этой секунды, а не с начального h1. Лучше anti-pattern — пусть критичный визуал виден сразу.


INP — Interaction to Next Paint

Это время от момента, когда ты тапнул по кнопке, до момента, когда браузер показал следующий кадр после реакции на этот тап. Не "когда событие обработалось", а "когда юзер увидел результат".

Таргеты:

  • < 200ms — хорошо
  • 200–500ms — нужно работать
  • 500ms — плохо

200 миллисекунд — это порог, после которого мозг человека воспринимает интерфейс как "тормозит". Меньше — реагирует мгновенно.

INP считает все взаимодействия за сессию (клики, тапы, нажатия клавиш) и берёт 98-й перцентиль из них. То есть достаточно одного тяжёлого клика, чтобы метрика провалилась.

Что обычно ломает INP:

Тяжёлый JavaScript на main thread. Клик по фильтру в каталоге запускает обработчик, который пересчитывает 2000 товаров и ререндерит весь список. React делает reconciliation на 600 мс. Палец уже отпустил кнопку — на экране ничего. Решения: useTransition для тяжёлых апдейтов (React 18+), виртуализация списков, разбивка на чанки через scheduler.postTask.

Long tasks. Любая задача на main thread длиннее 50 мс — long task. Если у тебя в обработчике клика трекер событий, который синхронно лезет в localStorage, парсит JSON и шлёт fetch — это всё блокирует поток. Решение: переноси на requestIdleCallback, выноси аналитику в web worker.

Layout thrashing. Обработчик клика читает offsetWidth, потом меняет стиль, потом снова читает offsetWidth. Браузер каждый раз пересчитывает layout. На сложной странице это даёт 100+ мс на ровном месте. Решение: батчить чтения и записи DOM, использовать requestAnimationFrame.

Жирные сторонние скрипты. Intercom, Hotjar, Google Tag Manager. Они вешают свои обработчики поверх твоих. Каждый клик идёт через цепочку перехватчиков. Решение: лениво загружай аналитику после load, используй Partytown для выноса скриптов в worker.

Code-splitting тоже помогает, но не напрямую — он снижает не INP, а количество JS, которое парсится. Косвенно это разгружает main thread.

Из практики: чаще всего INP проседает на двух экранах — список с фильтрами и форма с автокомплитом. На фильтрах помогает дебаунс ввода (300 мс достаточно) плюс useDeferredValue для отрисовки результатов. На автокомплите — отмена предыдущего запроса при новом нажатии (AbortController) и виртуализация выпадашки, если результатов больше тридцати.


CLS — Cumulative Layout Shift

Это сумма всех сдвигов вёрстки за время жизни страницы. Не время, не миллисекунды — безразмерное число от 0 и выше. Считается как impact × distance: какая площадь экрана сдвинулась, на сколько процентов экрана она уехала.

Таргеты:

  • < 0.1 — хорошо
  • 0.1–0.25 — нужно работать
  • 0.25 — плохо

0.1 на практике — это, например, картинка на 30% экрана, которая прыгнула на 30% высоты viewport. Один такой прыжок — и ты на грани.

CLS — это про "я тыкаю на кнопку, и в этот момент над ней появляется баннер, я тыкаю на баннер". Бесит всех. Google это знает.

Что обычно ломает CLS:

Картинки и iframes без явных размеров. Если у <img> нет атрибутов width и height (или их CSS-эквивалентов), браузер не знает, сколько места резервировать. Картинка догружается — и пушит вниз весь контент под собой. Решение: всегда указывай width и height (или aspect-ratio в CSS). В Next/Image это включено по умолчанию, если ты передаёшь размеры.

Динамические инжекты. Cookie-баннер, который появляется сверху через секунду после загрузки и сдвигает всю страницу вниз. AdSense, который вставляет блок в середину статьи. Любой A/B-тест, который меняет DOM на лету. Решение: резервируй место заранее. Cookie-баннер — фиксированный оверлей, а не block element. Реклама — слот фиксированной высоты.

Font swap без size-adjust. Грузится кастомный шрифт, до этого работает fallback. У них разная высота строки. Когда шрифт догрузился, текст перевёрстывается, всё прыгает. Решение: font-display: swap + size-adjust в @font-face, чтобы fallback занимал точно столько же места. Или font-display: optional, который не свопит на медленных соединениях. На Next можно через next/font — он сам считает size-adjust.

Асинхронный контент сверху страницы. Загружается список рекомендаций через fetch и встраивается над hero. Hero уезжает вниз. Никогда не вставляй контент над тем, что уже видно пользователю. Если нужно — добавляй вниз, а не вверх.


Field vs Lab data

Это место, где половина людей плывёт.

PageSpeed Insights показывает два блока. Сверху — "Discover what your real users are experiencing" (field data). Снизу — "Diagnose performance issues" (lab data). Это разные вещи.

Lab data — это один прогон Lighthouse в идеальных условиях. Эмулируется Moto G4 на медленном 4G, без расширений, без кэша, без других вкладок. Воспроизводимо, удобно для отладки, но не отражает реальность.

Field data — это CrUX. Chrome User Experience Report. Google собирает анонимные метрики с реальных пользователей Chrome (тех, у кого включена синхронизация и история). Агрегирует за последние 28 дней. Если на твой сайт зашло достаточно людей — Google показывает 75-й перцентиль их метрик.

Ранжирует Google по field. Lab — только подсказка. Можешь иметь 100/100 в Lighthouse и при этом красный CrUX, потому что реальные юзеры приходят с медленных Android-флагманов 2019 года через метро.

Где смотреть field:

  • PageSpeed Insights — самый простой способ, домашняя страница и отдельные URL.
  • Search Console → Core Web Vitals report — даёт разбивку по группам URL: "поиск", "карточки товаров", "статьи". Видно, где именно проседает.
  • PSI API — для автоматизации. Гонишь через скрипт раз в неделю по списку важных страниц, складываешь в таблицу, строишь динамику.
  • CrUX BigQuery dataset — для тех, кто умеет SQL. Можно вытянуть полные распределения, а не только перцентили.

Один важный момент. Если у сайта мало трафика — CrUX не покажет field data. Просто скажет "недостаточно данных". В этом случае Google для ранжирования использует data на уровне origin (всего домена) или fall back на lab. Поэтому даже на молодом сайте имеет смысл выжимать лучший Lighthouse — это твоя страховка, пока CrUX не накопится.



Что НЕ помогает

Список вещей, которые часто советуют, но которые на Core Web Vitals не влияют или влияют отрицательно.

Минификация CSS на и без того лёгком сайте. У тебя 18 КБ CSS, gzip жмёт это до 4 КБ. PSI пишет "minify CSS, save 1.2 KB". Эта 1.2 КБ ничего не даст. Тратишь время впустую.

Lazy load всего подряд. loading="lazy" на всех картинках — самая распространённая ошибка после ввода нативного lazy в Chrome. Включая hero. Включая логотип в шапке. В итоге картинки выше fold ждут intersection observer вместо того, чтобы качаться сразу. LCP растёт. Lazy ставится только на то, что ниже fold.

Агрессивный preload всего, что движется. Прочитал статью про "preload critical resources", повесил <link rel="preload"> на десять файлов. Браузер качает их с приоритетом выше HTML. Сеть забита. LCP-картинка ждёт своей очереди. Preload — это острый инструмент. Максимум два-три критичных ресурса.

Service Worker с кэшем "на всё". Кэширование умное, но если ты не понимаешь, что кешируешь — пользователь видит старую версию сайта. К Core Web Vitals это не имеет отношения вообще, а проблем добавляет.


Итог

Если у тебя ограниченное время — приоритизация такая.

LCP в первую очередь. Медленная страница — пользователи выбегают до того, как что-то увидели. Это бьёт и по ранжированию, и по конверсии напрямую. Hero-картинка через next/image priority, preload шрифтов, CDN перед всем. Это даёт 80% результата.

CLS вторым. Прыгающая вёрстка раздражает и убивает доверие. Особенно на мобиле. Размеры на media, font-display с size-adjust, не вставляй контент над уже видимым. Эти правки делаются быстро и не возвращаются.

INP третьим. Это тоньше всех и требует больше работы — профилирование, выяснение тяжёлых обработчиков, рефакторинг. Но без LCP и CLS в порядке за INP браться рано.

Метрики смотри в field — Search Console и PSI. Lab используй только как песочницу для проверки правок до деплоя. И помни: одного прогона недостаточно. CrUX обновляется раз в день, агрегирует за 28 дней. После правок жди как минимум две недели, прежде чем делать выводы.

Core Web Vitals — LCP, INP, CLS на простом языке · hiregora.com