Seo

Core Web Vitals: как измерить и улучшить

LCP, INP, CLS — пороги, инструменты измерения (PageSpeed Insights, CrUX, Lighthouse, web-vitals JS) и конкретные способы улучшить каждую метрику.

Автор: Андрій Коваленко 10 мин чтения
Содержание

Core Web Vitals — три метрики Google: LCP, INP и CLS. Они измеряют, насколько быстро и стабильно загружается страница для реального пользователя. С 2021 года это официальный ranking-фактор. В этой статье — что означает каждая метрика, где их измерить и конкретные действия для улучшения.

Полный технический контекст CWV в системе технического SEO — в руководстве Техническое SEO: с чего начать.

Три метрики: пороги и что они измеряют

МетрикаЧто измеряетЗелёная зонаЖёлтаяКрасная
LCP — Largest Contentful PaintВремя до отрисовки самого большого элемента<2.5 с2.5–4 с>4 с
INP — Interaction to Next PaintЗадержка отклика на любое взаимодействие<200 мс200–500 мс>500 мс
CLS — Cumulative Layout ShiftСумма сдвигов элементов во время загрузки<0.10.1–0.25>0.25

Google принимает решение на уровне URL: каждая страница оценивается отдельно. Для ranking-сигнала Google смотрит на 75-й перцентиль реальных пользователей за последние 28 дней (CrUX-данные).

INP заменил FID 12 марта 2024

До марта 2024 третьей метрикой CWV был FID (First Input Delay). FID измерял только первое взаимодействие. INP строже: фиксирует все клики, тапы и нажатия клавиш в течение сессии и берёт 98-й перцентиль. Если ваш сайт раньше имел хороший FID, но JS-код тяжёлый — INP может показать красную зону там, где FID был зелёным.

Где измерить Core Web Vitals

PageSpeed Insights — стартовая точка

Адрес: pagespeed.web.dev. Введите любой URL — получите два блока:

  1. Field data (реальные данные) — из CrUX за последние 28 дней. Это то, что Google использует для ranking. Если страница получает менее ~1000 визитов/месяц — CrUX-данных нет, появится сообщение «Insufficient data».
  2. Lab data (лабораторные данные) — Lighthouse в симулированных условиях (эмулированный Moto G Power, медленный 4G). Используйте для диагностики и локальной отладки.

Разница между field и lab может быть значительной: реальные пользователи имеют разные устройства, сети, кеш браузера. Цель — зелёная зона в field data, но если её нет — сначала исправьте lab data.

Google Search Console — масштаб по всему сайту

GSC → Core Web Vitals (боковая панель) показывает URL-группы со статусами Poor, Needs Improvement и Good для мобайла и десктопа отдельно. Здесь видно, какие шаблоны страниц имеют наибольшие проблемы: например, все страницы категорий в красной зоне, а главная — в зелёной.

GSC также предлагает кнопку «Validate Fix» после исправления — Google проверяет страницы из группы и обновляет статус в течение нескольких недель.

Chrome DevTools и Lighthouse

В Chrome: F12 → вкладка Lighthouse → запуск Performance аудита. Полезно для:

  • Найти конкретный LCP-элемент.
  • Увидеть «Opportunities» и «Diagnostics» — объяснение, почему метрика плохая.
  • Сравнить до/после внесения правок локально.

Во вкладке Performance после записи загрузки: в треке Timings есть метки LCP, FCP с ссылкой на DOM-узел.

web-vitals.js — измерение в production

Библиотека от Google для сбора реальных CWV-данных с вашего сайта:

<script type="module">
  import {onLCP, onINP, onCLS} from 'https://unpkg.com/web-vitals@4/dist/web-vitals.attribution.js';

  function sendToAnalytics(metric) {
    // Отправляем в GA4 как custom event
    gtag('event', metric.name, {
      value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
      metric_id: metric.id,
      metric_value: metric.value,
      metric_delta: metric.delta,
      metric_rating: metric.rating,  // 'good' / 'needs-improvement' / 'poor'
    });
  }

  onLCP(sendToAnalytics);
  onINP(sendToAnalytics);
  onCLS(sendToAnalytics);
</script>

Пакет web-vitals также устанавливается через npm: npm install web-vitals. Attribution-версия добавляет детали: какой элемент вызвал LCP, какое взаимодействие дало худший INP, какие элементы сдвинулись при CLS.

CrUX API и дашборд

Chrome UX Report (CrUX) — публичный датасет Google. Доступ:

  • CrUX API (бесплатно, 150 запросов в минуту): https://chromeuxreport.googleapis.com/v1/records:queryRecord?key=API_KEY — данные по конкретному URL или домену.
  • CrUX дашборд: lookerstudio.google.com → ищите «CrUX Dashboard» — готовый Looker Studio отчёт, достаточно ввести домен.
  • BigQuery: полный датасет, обновляется ежемесячно.

CrUX агрегирует данные за 28 дней скользящим окном и обновляется ежедневно для CrUX API (ежемесячно для BigQuery).

Lab vs Field: когда что использовать

СитуацияКакой источник
Проверить, будет ли сайт ранжироватьсяField data (CrUX, GSC)
Найти причину медленного LCPLab data (Lighthouse, PSI diagnostics)
Сравнить до/после правки локальноLab data (DevTools, Lighthouse CI)
Мониторинг реальных пользователейweb-vitals.js → GA4 или другой коллектор
Масштаб проблем по сайтуGSC Core Web Vitals report

LCP: как улучшить

LCP — время до отрисовки самого большого элемента above the fold. В 80%+ случаев это hero-изображение или крупный заголовок h1.

Шаг 1: найти LCP-элемент

PageSpeed Insights → раздел «Largest Contentful Paint element» покажет конкретный DOM-узел. Если это <img> — смотрите на оптимизацию изображений. Если <h1> — проблема в шрифтах или server response time.

Шаг 2: оптимизировать изображения

<!-- Плохо: нет размеров, нет WebP, нет preload -->
<img src="hero.jpg" alt="Hero">

<!-- Хорошо: WebP, srcset, правильные размеры, preload для LCP-изображения -->
<link rel="preload" as="image" href="hero-800w.webp"
      imagesrcset="hero-400w.webp 400w, hero-800w.webp 800w"
      imagesizes="(max-width: 640px) 400px, 800px">

<img src="hero-800w.webp"
     srcset="hero-400w.webp 400w, hero-800w.webp 800w"
     sizes="(max-width: 640px) 400px, 800px"
     width="800" height="450"
     alt="Hero"
     fetchpriority="high">

Ключевые моменты:

  • WebP или AVIF вместо JPEG/PNG. WebP: -25-35% от JPEG при той же качестве. AVIF: ещё -20% от WebP, но поддержка браузеров несколько меньше.
  • fetchpriority="high" для LCP-изображения — говорит браузеру загрузить первым.
  • <link rel="preload"> для LCP-изображения в <head> — браузер начинает загрузку до того, как парсит img-тег.
  • loading="lazy" для изображений ниже первого экрана — не ставьте на LCP-элемент.

Шаг 3: уменьшить TTFB

Time to First Byte — время до первого байта ответа сервера. Цель: <800мс, идеал <200мс.

Способы:

  • CDN (Cloudflare, Bunny.net, Fastly) — отдаёт HTML с edge-ноды рядом с пользователем.
  • Кэширование на сервере — полноценный page cache для WordPress (LiteSpeed Cache, W3 Total Cache), статические генераторы (Hugo, Astro) дают TTFB <50мс по умолчанию.
  • Оптимизация базы данных — долгие SQL-запросы напрямую увеличивают TTFB.
  • Хостинг — shared hosting на переполненных серверах даёт TTFB 1-3с. VPS или managed WordPress хостинг — существенный скачок.

Шаг 4: Server-side rendering вместо client-side

Single-page applications (React, Vue, Angular) без SSR рендерят контент в браузере — LCP не может начаться, пока не загрузится и не выполнится весь JS-бандл. Решения:

  • Next.js (React) с SSR или Static Site Generation.
  • Nuxt.js (Vue) с SSR.
  • Astro — отправляет ноль JS по умолчанию.
  • Hugo — статический генератор, ультрабыстрый.

Шаг 5: Resource hints

<!-- DNS prefetch для внешних ресурсов (шрифты, CDN) -->
<link rel="dns-prefetch" href="https://fonts.googleapis.com">

<!-- Preconnect — устанавливает TCP/TLS до сервера заранее -->
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

<!-- Preload — загружает ресурс с наивысшим приоритетом -->
<link rel="preload" href="/fonts/inter-v13-latin-regular.woff2" as="font" type="font/woff2" crossorigin>

INP: как улучшить

INP измеряет, насколько быстро страница реагирует на взаимодействие пользователя. Большинство проблем INP — это JavaScript, блокирующий main thread.

Найти проблемное взаимодействие

В web-vitals.js attribution-версия показывает, какое взаимодействие дало худший INP:

import {onINP} from 'web-vitals/attribution';

onINP(({value, attribution}) => {
  console.log('INP:', value, 'ms');
  console.log('Элемент:', attribution.interactionTarget);
  console.log('Тип:', attribution.interactionType);
  console.log('Время обработки:', attribution.processingDuration);
});

В Chrome DevTools → Performance → после клика на странице: найдите в треке Main длинные tasks (красные). Кликните на task — увидите, какая функция занимает время.

Уменьшить long tasks

Long task — любая задача на main thread дольше 50мс. INP становится плохим, если после клика есть long task, задерживающий ответ.

// Плохо: тяжёлый обработчик блокирует main thread
button.addEventListener('click', () => {
  const result = heavyComputation(); // занимает 300мс
  updateUI(result);
});

// Хорошо: разбиваем на chunks
button.addEventListener('click', async () => {
  updateUI('Загружаю...');
  
  // Передаём управление браузеру между итерациями
  await scheduler.yield(); // Chrome 115+, или setTimeout(0) как fallback
  
  const result = await computeInChunks();
  updateUI(result);
});

Defer и async для скриптов

<!-- Блокирует парсинг HTML — избегайте для некритичных скриптов -->
<script src="analytics.js"></script>

<!-- async: загружает параллельно, выполняет как только загрузился -->
<script src="analytics.js" async></script>

<!-- defer: загружает параллельно, выполняет после парсинга HTML -->
<script src="non-critical.js" defer></script>

Google Tag Manager и большинство аналитических скриптов — используйте async. Скрипты, зависящие от DOM — defer.

Web Workers для тяжёлых вычислений

Если нужна тяжёлая обработка данных — выносите в Web Worker, чтобы не блокировать main thread:

// main.js
const worker = new Worker('heavy-worker.js');

button.addEventListener('click', () => {
  worker.postMessage({data: largeDataset});
});

worker.onmessage = (e) => {
  updateUI(e.data.result);
};

// heavy-worker.js
self.onmessage = (e) => {
  const result = processData(e.data.data); // не блокирует UI
  self.postMessage({result});
};

React: специфическая оптимизация

Если используете React — INP часто страдает из-за лишних re-renders:

// useMemo — кэшировать тяжёлые вычисления
const sortedItems = useMemo(
  () => items.sort((a, b) => b.date - a.date),
  [items]
);

// React.memo — не re-render если props не изменились
const ItemCard = React.memo(({item}) => <div>{item.title}</div>);

// useTransition — пометить обновление как несрочное
const [isPending, startTransition] = useTransition();

function handleFilter(value) {
  startTransition(() => {
    setFilteredItems(items.filter(i => i.name.includes(value)));
  });
}

CLS: как улучшить

CLS измеряет, насколько страница «прыгает» во время загрузки. Каждый раз, когда элемент сдвигается без взаимодействия пользователя — это вносится в CLS.

Всегда задавать размеры для медиа

Самая распространённая причина CLS — изображения и видео без явных размеров. Браузер не знает размера, резервирует 0px, и после загрузки изображения весь контент ниже сдвигается.

<!-- Плохо: браузер не знает размеров -->
<img src="product.jpg" alt="Продукт">

<!-- Хорошо: браузер резервирует место заранее -->
<img src="product.jpg" width="600" height="400" alt="Продукт">

<!-- Для responsive изображений — aspect-ratio через CSS -->
<style>
  .product-img {
    aspect-ratio: 600 / 400;
    width: 100%;
    height: auto;
  }
</style>
<img class="product-img" src="product.jpg" alt="Продукт">

То же самое для <video>:

<video width="1280" height="720" controls>
  <source src="demo.mp4" type="video/mp4">
</video>

Резервировать место для рекламы и embeds

/* Баннер 728×90 — всегда резервируем место */
.ad-container {
  min-height: 90px;
  width: 728px;
}

/* Responsive вариант */
.ad-slot {
  min-height: 250px; /* типичный размер medium rectangle */
}

Для динамического контента (чат, cookie banner, sticky notification):

  • Cookie banner — показывайте снизу страницы, а не сверху. Banner сверху толкает весь контент вниз = CLS.
  • Lazy-loaded контент — не вставляйте между существующими элементами. Добавляйте в конец или в заранее зарезервированное пространство.

Шрифты: font-display

Когда кастомный шрифт загружается позже системного — текст перерисовывается, что может вызвать CLS:

@font-face {
  font-family: 'Inter';
  font-display: optional; /* идеал для CLS: если не загрузился за ~100мс — не подменять */
  src: url('/fonts/inter.woff2') format('woff2');
}

/* Или: swap — всегда показывать, подменять после загрузки */
/* Это может дать небольшой CLS, но лучше чем невидимый текст (FOIT) */
font-display: swap;

Preload важнейших шрифтов в <head> уменьшает задержку:

<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>

Типичные ошибки и как их избежать

  1. Измеряете только главную страницу. В GSC часто landing pages, статьи и страницы категорий имеют худшие CWV чем главная. Смотрите GSC Core Web Vitals report — он группирует страницы по шаблону.
  2. Ставите fetchpriority="high" на все изображения. Если все «важные» — ни одно не важнее. Один fetchpriority="high" на LCP-изображение.
  3. Lazy load на LCP-изображение. loading="lazy" на hero-изображении — самый быстрый способ испортить LCP. Lazy load только для изображений ниже первого экрана.
  4. Большой JS-бандл без code splitting. Если весь frontend-код в одном файле на 500кб — браузер загружает и парсит всё перед рендерингом. Webpack/Vite code splitting + dynamic imports.
  5. Рекламный баннер без зарезервированного места. AdSense и programmatic реклама часто меняют размер после загрузки — CLS прыгает. Фиксируйте min-height контейнера.
  6. Игнорируете mobile данные. У большинства сайтов хуже мобильные CWV. Эмуляция в Lighthouse не передаёт реальный опыт — используйте field data.
  7. Не ждёте обновления CrUX. CrUX обновляется постепенно (28-дневное окно). Исправили CLS — GSC покажет улучшение через 4-6 недель, не завтра.
  8. GTM без оптимизации тегов. Google Tag Manager не блокирует LCP (асинхронный), но тяжёлые теги (рекламные SDK, heatmap, live chat) создают long tasks → плохой INP. Аудит активных тегов GTM — стандартная CWV-оптимизация.

Практическая очерёдность: что исправлять первым

Если все метрики красные — рекомендую такую очерёдность:

ПриоритетДействиеЧто фиксируетСложность
1Добавить width/height к изображениямCLSНизкая — 1-2 ч
2WebP/AVIF + fetchpriority="high" на LCPLCPНизкая
3<link rel="preload"> для LCP-image и шрифтовLCPНизкая
4CDN или page cacheLCP (TTFB)Средняя
5Cookie banner снизу, а не сверхуCLSНизкая
6defer/async для некритичных скриптовINP (TBT)Низкая
7Аудит и очистка GTM-теговINPСредняя
8Code splitting JS-бандлаINPВысокая
9SSR/SSG вместо клиентского рендерингаLCP + INPВысокая

Мониторинг после исправлений

Разового исправления недостаточно. CWV может ухудшиться после каждого релиза. Выстройте процесс:

  1. Lighthouse CI в CI/CD pipeline — автоматически проверяет CWV на каждом PR перед деплоем.
  2. web-vitals.js → GA4 — собирайте реальные данные постоянно, настройте алерты на ухудшение.
  3. GSC Core Web Vitals report — проверяйте раз в неделю после крупных релизов.
  4. PageSpeed Insights API — автоматические еженедельные снимки для ключевых страниц.

Пример еженедельного мониторинга через PSI API (bash):

URL="https://example.com"
API_KEY="YOUR_KEY"

curl -s "https://www.googleapis.com/pagespeedonline/v5/runPagespeed?url=${URL}&strategy=mobile&key=${API_KEY}" \
  | jq '.loadingExperience.metrics | {
      LCP: .LARGEST_CONTENTFUL_PAINT_MS.percentile,
      INP: .INTERACTION_TO_NEXT_PAINT.percentile,
      CLS: .CUMULATIVE_LAYOUT_SHIFT_SCORE.percentile
    }'

Связанные ресурсы

Руководства:

Чек-листы:

Инструменты:

  • Meta Tag Checker — проверить title, description, canonical одним запросом
  • SERP Preview — предпросмотр сниппета
  • Все инструменты сайта → /ru/tools/

Похожие статьи

Смотрите также

Эту статью пишет и обновляет Андрій Коваленко — без AI-воды и партнёрских ссылок. Заметил устаревший факт или неточность — напиши, перепишу в ту же неделю.

Кто ведёт сайт и почему без AI