core-web-vitals-practice.md — vim

Core Web Vitals: перевожу «сайт тормозит» на язык цифр

LCP, INP и CLS простыми словами — что означают, какие пороги и чем я их реально чинил. Плюс про то, как INP заменил FID и почему это важно.


«Сайт тормозит» — это не задача, с ней ничего нельзя сделать. «LCP 4.2 секунды при пороге 2.5» — задача, её можно решить и потом доказать, что решил.

Поэтому любой разговор про производительность я начинаю с трёх метрик.

МетрикаПро чтоХорошоПлохо
LCPкогда появился главный элемент экрана≤ 2,5 с> 4 с
INPкак быстро страница отвечает на действия≤ 200 мс> 500 мс
CLSнасколько скачет вёрстка≤ 0,1> 0,25

Считаются они по 75-му перцентилю реальных пользователей. То есть «хорошо» — это когда хорошо трём четвертям людей, а не когда красиво открывается на твоём макбуке.

Отдельно про INP: с марта 2024 он заменил FID. Разница принципиальная. FID мерил только задержку до начала обработки первого клика - метрику было легко сдать на отлично, имея при этом дико тормозящий интерфейс. INP смотрит на полный цикл: клик → обработчик → следующая отрисовка, и берёт худшие взаимодействия за сессию. Гораздо ближе к тому, что человек реально чувствует.

LCP

Обычно это либо большая картинка, либо заголовок, который ждёт шрифт или данные.

Первое и главное - не прячь главный элемент за JavaScript. Если геро-блок рисуется только после загрузки бандла и ответа API, LCP будет плохим всегда, сколько ни оптимизируй картинки. Отсюда серверный рендер для контентных страниц.

Дальше по мелочи:

<img src="/hero.webp" width="1200" height="630" fetchpriority="high" alt="…" />

И никакого loading="lazy" на том, что видно сразу - ленивая загрузка первого экрана только вредит, хотя выглядит как оптимизация.

Шрифты - font-display: swap и предзагрузка, иначе текст невидим, пока грузится шрифт, и LCP растёт ровно на это время:

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

Ну и WebP/AVIF вместо PNG на фотографиях. Минус 30-60% веса, разницы на глаз нет.

INP

Это всё про главный поток. Он один, и пока он занят, интерфейс не отвечает.

Любая задача длиннее 50 мс блокирует ответ на клик. Тяжёлую обработку надо резать и отдавать управление браузеру:

async function processInChunks<T>(items: T[], fn: (x: T) => void, size = 100) {
  for (let i = 0; i < items.length; i += size) {
    items.slice(i, i + size).forEach(fn);
    await new Promise(r => setTimeout(r, 0)); // браузер успевает отрисовать
  }
}

Ещё важнее не считать тяжёлое прямо в обработчике клика. Сначала покажи реакцию - спиннер, изменение состояния кнопки - потом считай. Человек готов ждать результат, но не готов ждать реакцию на нажатие, это разные вещи.

Совсем тяжёлое (парсинг больших CSV, сортировка десятков тысяч строк) - в Web Worker.

И самое надёжное: меньше JS. Код, который ты не отправил, выполняется мгновенно. Динамические импорты для редких экранов, отказ от библиотеки ради трёх функций, и, пожалуйста, не пять аналитик на странице.

CLS

Самая бесящая пользователя метрика: он целится в кнопку, контент прыгает, он попадает в баннер.

Чинится почти всегда четырьмя вещами:

  • у картинок и видео проставлены width/height или aspect-ratio;
  • под динамические блоки зарезервировано место скелетоном той же высоты;
  • ничего не вставляется над контентом (привет, плашка про куки, которая появляется через секунду);
  • шрифты подобраны с похожими метриками (size-adjust), чтобы подмена не меняла высоту текста.

Чем мерить

Lighthouse в DevTools - быстро и воспроизводимо, но это лаборатория. Обязательно включай троттлинг процессора 4-6х: средний пользовательский телефон в разы медленнее твоего ноутбука, и без троттлинга ты меряешь не то.

Реальность показывают только полевые данные:

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

const send = (m: { name: string; value: number }) =>
  navigator.sendBeacon('/api/metrics', JSON.stringify(m));

onLCP(send); onINP(send); onCLS(send);

Только они отвечают на вопрос «стало ли лучше людям», и именно они попадают в CrUX, на который смотрит Google.

Кстати про Google и SEO, чтобы без мифов: Core Web Vitals действительно входят в сигналы ранжирования, но это один фактор среди многих и релевантность контента он не перебивает. Так что аргументировать оптимизацию лучше не позициями, а отказами и конверсией - это видно прямо в вашей же аналитике и звучит убедительнее.

Три метрики покрывают три вопроса пользователя: когда я это увижу, когда оно откликнется и перестанет ли оно прыгать. Остальное - детали.

Ссылки