Почему это важно не только для SEO

С 2021 года Google включил Core Web Vitals в алгоритм ранжирования. Но главная причина следить за этими метриками — не позиции в поиске, а реальный пользовательский опыт. Страница, которая прыгает при загрузке или не реагирует на клик полсекунды, теряет пользователей быстрее, чем любой алгоритм успевает её понизить.

Три метрики:

  • LCP (Largest Contentful Paint) — когда отрисовывается самый большой элемент на экране
  • CLS (Cumulative Layout Shift) — суммарный сдвиг макета во время загрузки
  • INP (Interaction to Next Paint) — время от клика или тапа до визуального отклика

Пороги «хорошо» / «плохо»:

  • LCP: хорошо < 2.5 с, плохо > 4 с
  • CLS: хорошо < 0.1, плохо > 0.25
  • INP: хорошо < 200 мс, плохо > 500 мс

LCP — что тормозит главный элемент страницы

LCP-элемент — почти всегда героическое изображение в шапке или крупный заголовок. Посмотреть, что именно считается LCP на конкретной странице, можно в Chrome DevTools: вкладка Performance → Timings → LCP.

Типичные виновники медленного LCP:

  1. Картинка в шапке загружается поздно — браузер обнаруживает её только при разборе CSS или JS, а не сразу в HTML
  2. Сервер медленно отвечает (TTFB > 600 мс)
  3. Изображение не оптимизировано — весит 2 МБ в PNG вместо 80 КБ в WebP

Фикс №1 — fetchpriority

Добавь атрибут fetchpriority="high" на LCP-изображение:

<img src="hero.webp" fetchpriority="high" alt="...">

Это говорит браузеру: не жди, качай это первым. Прирост LCP — обычно 300–600 мс без каких-либо других изменений.

Фикс №2 — preload для фонового изображения

Если LCP-элемент — фон через CSS, браузер вообще не знает о нём до разбора стилей. Добавляй preload в <head>:

<link rel="preload" as="image" href="hero.webp">

Фикс №3 — конвертация в WebP или AVIF

PNG и JPEG в большинстве случаев можно безболезненно заменить на WebP. Потеря качества незаметна, размер падает в 3–5 раз. AVIF ещё эффективнее, но поддержка чуть хуже — используй <picture> с fallback:

<picture>
  <source srcset="hero.avif" type="image/avif">
  <source srcset="hero.webp" type="image/webp">
  <img src="hero.jpg" fetchpriority="high" alt="...">
</picture>

TTFB лечится на уровне сервера или CDN. Если хостинг отвечает за 800 мс — никакие оптимизации фронтенда не спасут. Нужен либо переезд на нормальный VPS, либо CDN поверх.

CLS — почему страница прыгает и как это остановить

CLS — одна из самых раздражающих вещей в интерфейсе. Тянешься нажать кнопку, страница сдвигается, и промахиваешься. Или читаешь текст — и он уезжает вниз, потому что над ним загрузился баннер.

Что вызывает CLS:

  • Изображения без явных атрибутов width и height
  • Рекламные блоки и баннеры, которые появляются после загрузки
  • Веб-шрифты с FOUT (Flash of Unstyled Text)
  • Динамически добавляемый контент над уже отрисованным

Фикс №1 — размеры изображений

Всегда указывай width и height в HTML. Браузер заранее резервирует нужное место:

<img src="photo.webp" width="800" height="600" alt="...">

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

Фикс №2 — место под рекламу

Если баннер загружается асинхронно, задай контейнеру минимальную высоту через CSS:

.ad-banner {
  min-height: 90px;
}

Фикс №3 — шрифты

Используй font-display: optional — браузер использует системный шрифт, если кастомный не загрузился за первые 100 мс. Никакого сдвига:

@font-face {
  font-family: 'MyFont';
  src: url('font.woff2') format('woff2');
  font-display: optional;
}

Либо предзагружай шрифты в <head>:

<link rel="preload" as="font" href="font.woff2" crossorigin>

Реальный кейс: сайт интернет-магазина имел CLS 0.38 — это уже в зоне «плохо». Причина — слайдер в шапке без фиксированной высоты. После добавления aspect-ratio: 16/9 на контейнер и width/height на все изображения слайдера CLS упал до 0.04. Время работы — 20 минут.

INP — самая новая и самая недооценённая метрика

INP заменил FID в марте 2024. Если FID мерял только первое взаимодействие, то INP отслеживает все клики, тапы и нажатия клавиш за сессию и берёт 98-й перцентиль. Порог хорошего значения — 200 мс: от нажатия до визуального отклика.

Что убивает INP:

  1. Длинные задачи (Long Tasks) в основном потоке — скрипты, которые работают дольше 50 мс
  2. Тяжёлые обработчики событий: кнопка «Добавить в корзину» синхронно пересчитывает всю корзину до того, как обновить UI
  3. Избыточные перерисовки DOM после клика

Как искать проблемы:

Chrome DevTools → Performance → запись сессии. Ищешь Long Tasks — красные блоки поверх графика. Задача > 50 мс — потенциальный виновник.

Быстрый способ через консоль:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 200) {
      console.log('Slow INP:', entry);
    }
  }
});
observer.observe({ type: 'event', buffered: true });

Как чинить:

Разбивай длинные задачи через scheduler.yield():

async function handleAddToCart(item) {
  updateCartUI(item); // быстро — делаем сразу

  await scheduler.yield(); // уступаем браузеру

  await recalculateTotal(); // тяжёлая операция — после
  await saveToServer(item);
}

Пользователь видит реакцию мгновенно. Тяжёлая работа идёт фоном.

Для React — переноси тяжёлые вычисления в startTransition:

import { startTransition } from 'react';

function handleFilter(value) {
  startTransition(() => {
    setFilteredItems(heavyFilter(items, value));
  });
}

Инструменты — чем мерять

Lighthouse — встроен в Chrome DevTools. Запускай в режиме инкогнито, чтобы расширения не искажали результат. Минус: лабораторные данные, не полевые.

PageSpeed Insights — показывает и лабораторные данные, и полевые (CrUX — реальные пользователи с реальных устройств). Смотри именно полевые. Если их нет — сайт слишком мало посещают для репрезентативной выборки.

Search Console → раздел «Основные интернет-показатели» — сводная картина по всему сайту с разбивкой по URL. Именно на эти данные Google ориентируется при ранжировании.

web-vitals — JavaScript-библиотека от Google для сбора метрик прямо в коде:

import { onLCP, onCLS, onINP } from 'web-vitals';

onLCP(console.log);
onCLS(console.log);
onINP(console.log);

Подключи это к своей аналитике — и будешь видеть реальные данные от реальных пользователей, а не только цифры из синтетических тестов.

Порядок работы на реальном проекте

Когда в REEXY приходит задача на производительность, работаем по одной схеме:

  1. Аудит — PageSpeed Insights плюс Search Console. Смотрим, что именно красное и на каких страницах.
  2. LCP первым — чаще всего даёт максимальный прирост за минимум времени.
  3. CLS следом — обычно лечится быстро, если понимаешь причины.
  4. INP в конце — требует профилирования и занимает больше времени.

Не пытайся оптимизировать всё сразу вслепую. Сначала измерь, потом чини конкретную проблему, потом снова измерь.

Что ещё влияет, но не входит в Core Web Vitals

TTFB формально не Core Web Vital, но напрямую влияет на LCP. Хороший порог — до 200 мс. Если сервер отвечает медленнее — фронтенд-оптимизации не помогут.

Render-blocking ресурсы — CSS и JS, которые блокируют отрисовку. Некритичный JS переноси в конец body или добавляй defer. Некритичный CSS — грузи асинхронно.

Лишние начертания шрифтов — если подключаешь Google Fonts, не тяни все 9 вариантов. Оставь только те, которые реально используются.

Быстрый чек-лист перед деплоем

  • LCP-изображение имеет fetchpriority="high" или preload
  • Все изображения имеют атрибуты width и height
  • Изображения в WebP или AVIF
  • Шрифты с font-display: optional или с preload
  • Рекламные блоки с зарезервированным местом
  • Нет JS-скриптов без defer или async в <head>
  • TTFB сервера < 200 мс
  • Long Tasks < 50 мс в критических обработчиках событий

Большинство этих пунктов закрываются за несколько часов работы. Результат — сайт быстрее реагирует, меньше раздражает пользователей и лучше ранжируется в поиске. Не потому что Google сказал «оптимизируй», а потому что это просто более качественный продукт.