
Салют, Хабр!
Меня зовут Паша, я вхожу в группу обеспечения производительности интерфейсов; неформально это команда «Скорость» SberDevices. Мы с коллегами отвечаем за то, чтобы сайты sberdevices.ru, giga.chat, developers.sber.ru и другие быстро работали и красиво выглядели. Если пользователю неудобно использовать сайт, он быстро с него уйдёт (и не станет ничего читать и покупать).
Как только потребовалась оценка качества веб-страниц, появилось и множество подходов к ним. Но в итоге индустриальным стандартом стали методы, предложенные Google и реализованные на уровне браузерных API. Чтобы понять, как сайты стоит оптимизировать и что влияет на их быстродействие, Google оперирует тремя метриками — Core Web Vitals. Две из них действуют с мая 2020 года, а третьей не исполнилось и года. Сегодня расскажу, как разработчик может оценить их и улучшить без консультации команды скорости (если она есть). А ещё благодаря оптимизации метрик Core Web Vitals сайт ранжируется в поисковиках выше. Поехали!
Cumulative Layout Shift (CLS): элементы сайта резко появляются, когда их не ждали
Пользователь заходит на сайт, чтобы прочесть статью или купить товар — и в этот момент страница неожиданно «дёргается»: из-за внезапной загрузки рекламы или баннера контент смещается. Вместо нужной кнопки он случайно кликает на рекламу или не туда, куда хотел, и вот уже ему сильно и неудержимо хочется уйти с сайта.
Чтобы этого не происходило, Google ввёл метрику CLS (Cumulative Layout Shift) — она оценивает визуальную стабильность страницы и помогает избежать «прыгающих» элементов.
CLS сильно снижают мелкие технические недочёты. Например, асинхронная загрузка — изображения, реклама или виджеты, которые подгружаются с опозданием, вытесняя уже отрисованный контент. Динамически добавляемые элементы: всплывающие формы или подгружаемый скриптами контент могут встраиваться в DOM, сдвигая соседние блоки. Ещё медиа без размеров (видео и визуалы без прописанных width/height резко расширяют область при загрузке). И шрифты-невидимки: если кастомный шрифт грузится долго, а временный резерв вроде Arial занимает другое пространство, после подзагрузки текст «прыгает».
Подсчёт CLS происходит на протяжении всей сессии, однако для метрики выбирается только максимальное значение CLS, которое было записано в течении окна сеанса.
Окно сеанса CLS — это пятисекундный период, который начинается при первом смещении. Он продлевается на 1 секунду после каждого последующего сдвига, если интервал между ними менее 1 секунды:
Пользователь заходит на страницу – браузер начинает запись нового окна сеанса.
В первые 3 секунды происходят «дёргания»: сначала появляются рекламные баннеры, через полсекунды — изображения без размеров. Каждое смещение плюсуется к CLS. В конечном итоге значение достигает пика — 0,3.
Пауза 5 секунд, никаких смещений нет. Браузер закрывает текущее окно сеанса, фиксируя значения CLS равным 0,3. Новые изменения его не увеличат.
Пользователь начинает прокручивать страницу, и браузер начинает запись нового окна сеанса. Если вновь произойдут какие-то изменения, то значение CLS будет записано уже в этом окне.
Для финальной отправки (например, в google lighthouse, SEO) берётся наибольший CLS, который был записан в окнах сеанса.

Как посчитать CLS и какой результат считается хорошим?
CLS — безразмерная величина, то есть у неё нет нет единиц измерения. Есть лишь формула:
Impact fraction отвечает за то, сколько элементов было сдвинуто другим элементом. Эта метрика — отношение сдвинутых элементов (измеряется в пикселях или в vh-vw, в процентах) на всю высоту или ширину экрана в пикселях — или, если говорить терминологически, viewport. Тестовая страница для примера:

Та же страница до появления баннера

Здесь баннер появился вверху страницы, однако ниже header и описания сайта, поэтому CLS не так велик. Если бы баннер появлялся в самом верху страницы, выше header и описания, impact fraction был бы равен единице. Представим, что высота нашего экрана (viewheight) равна 768px, а высота header и описания равна 360px. Тогда:
Переменная distance fraction, как понятно из названия, отвечает за то, на сколько пикселей был сдвинут (либо добавлен) элемент. Вернёмся к картинке сайта с баннером. Возьмём за высоту баннера величину, равную 50px. Тогда distance fraction будет высчитываться так:
Итого, подставляя в исходную формулу значения,
0,03 — это хороший показатель. Хорошие значения CLS не превышают 0,1. На деле на большинстве сайтах не заметить CLS выше 0,1 трудно — это будет сразу бросаться в глаза (как наш пример с баннером). Всё, что выше 0,1, нужно поправлять.

Как улучшить показатели CLS?
Иногда изменения сайта вызвал сам пользователь — например, открыл модальное окно, раскрыл список, нажал на кнопку. Эти сдвиги на сайте, которые происходят в ответ на действия пользователя допустимы, если они происходят сразу после взаимодействия. Тогда взаимосвязь будет ясна пользователю. Для сдвигов на сайте, которые происходят в течение 500 миллисекунд после действий пользователя, будет установлен флаг hadRecentInput, поэтому они не входят в вычисления.
Можно использовать анимации и переходы (если сделать их правильно) — это отличный способ обновить контент на странице, не удивляя пользователя. Более того, такой способ не только улучшает CLS, но и делает сайт понятнее. Контент, который постепенно и естественно перемещается из одной позиции в другую, часто может помочь пользователю лучше понять, что происходит, и направлять его между изменениями состояний. Но не стоит перебарщивать с анимацией: иногда это просто плохо выглядит, а иногда пользователям с проблемным вестибулярным аппаратом может физически поплохеть (⇀‸↼‶)
Обязательно нужно соблюдать настройки браузера prefers-reduced-motion — иначе некоторые посетители сайта могут испытывать неприятные последствия или проблемы с вниманием из-за анимации. Prefers-reduced-motion — это media css параметр, который отвечает за то, включена ли у пользователя настройка «уменьшить движения» или «отключение анимации». Так, например, пользователи с вестибулярным нарушением будут испытывать трудности, если будут видеть лишние движение на экране, в том числе любую анимацию на сайте. Вот пример такой настройки в системе MacOS: нижняя строка — уменьшить движение.

Вот как следует работать с медиа-параметром prefers-reduced-motion:
Код
.animation {
animation: pulse 1s linear infinite both;
background-color: purple;
}
@media (prefers-reduced-motion) {
.animation {
animation: dissolve 4s linear infinite both;
background-color: green;
text-decoration: overline;
}
}
.animation {
color: #fff;
font: 1.2em sans-serif;
width: 10em;
padding: 1em;
border-radius: 1em;
text-align: center;
}
Interaction to Next Paint (INP): обманчивое ощущение, что сайт завис
Трудно представить сайт, на котором нельзя что-либо сделать, нажать или перейти куда-то (кроме сайтов-визиток). 99% взаимодействия с сервисом в вебе или в приложении основаны на интерактивности. Пользователи на сайте что-то кликают, печатают, смотрят видео, переключают песни и регулируют громкость, покупают… Поэтому очень важно обращать на это внимание. Если при плохом CLS пользователь видит резко появившийся баннер, то при плохом INP он вообще ничего не увидит сразу.
INP (Interaction to Next Paint) — та самая метрика, которая меньше года назад официально заменила First Input Delay в CWV. Сейчас метод onFid, который используется для сбора одноименной метрики, помечен как deprecated. FID, как и следует из названия, считал только первое взаимодействие, а INP считает все взаимодействия на протяжении жизни всего сеанса.

Чтобы понять, что считается в INP, можно зайти на нашу тестовую страницу и начать что-либо делать. Допустим, мы решили узнать, в честь какого великого человека названа черепашка-ниндзя. Нажмём на кнопку «показать художника».

При нажатии на кнопку «показать художника» или «показать черепашку» INP уже начнёт считаться. Однако не каждое взаимодействие с сайтом засчитывается в INP. Например, можно не беспокоиться за прокручивание страницы.
В отличие от CLS (да, сравнений будет много), где мы понимаем, что именно будет влиять на метрику, в INP есть одна загвоздка: iframe. Тут небольшое отступление.
Немного контекста: RUM, CrUX и в чем их различия
Есть RUM — Real User Monitoring — например, телеметрия команды «Скорость». А есть CrUX — сбор данных от Google (Chrome User Experience). Это открытая база данных от Google, которая собирает реальные метрики производительности веб-страниц на основе анонимизированных данных пользователей Chrome.
Дилемма вот в чём: из-за ограничений безопасности браузеров RUM-инструменты не могут измерить метрики внутри iframe сторонних доменов, тогда как CrUX включает их в отчёты, так как собирает данные на уровне Chrome. Это создаёт расхождения в данных. Например, INP видеоплеера в iframe может учитываться в CrUX, но отсутствовать в RUM. Тем не менее, основные взаимодействия пользователя с браузером подсчитать всё-таки можно.
Посмотреть на INP
Чтобы лично оценить, чем плох высокий показатель INP, можно снова перейти на наш тестовый сайт и нажать на кнопку в левом нижнем углу «Change INP delay». После этого появится задержка при нажатии на кнопку «показать художника/черепашку».
Как подсчитывается INP и какой результат хороший?
Подсчёт INP начинается с простого нажатия на экран или кнопку. Если основной поток не заблокирован, начинают работать обработчики, затем идёт этап рендеринга и вуаля — следующий frame отрисован, INP будет подсчитан! Ну а если поток заблокирован, в это время рендеринга не происходит, мы ждём, когда освободится основной поток — и время блокировки засчитывается в INP.

В отличии от CLS, у INP есть единица измерения — миллисекунды. Человеческий глаз не способен видеть изменения менее 100 миллисекунд, так что любое изменение, произошедшее быстрее, чем за 100 миллисекунд, для человеческого глаза является моментальным. Всё, что менее 200 миллисекунд, хороший результат; всё, что ниже 500, требует доработки. Всё, что выше 500, уже очень плохо.

Для INP выбирается наибольшее значение на каждые 50 взаимодействий. При этом пользователь на одной странице очень редко превышает данное количество взаимодействий, так что по сути берётся просто наибольшее значение INP за весь жизненный цикл сайта.
Как улучшить показатели INP?
Как и для любой метрики, нет универсального решения для всех сервисов. Но базовые советы, конечно же, есть:
— Можно добавить loader сразу после нажатия — например, анимацию загрузки на кнопке. Так вы сообщите пользователю, что его взаимодействие в обработке, а сайт не завис. INP в этом случае будет хорошим, и вы не потеряете драгоценные миллисекунды.
— Стоит часто возвращать управление основному потоку. При обработке событий рекомендуется переносить вызовы в колбэки. Если ваш сервис выполняет сложные последовательные вычисления, разбивайте их на отдельные макрозадачи. Это предотвращает блокировку основного потока длительными задачами и ускоряет выполнение других взаимодействий. Для этого можно использовать setTimeout, чтобы разгрузить основной поток, или requestIdleCallback, который отложит вызов некритической функции до момента, когда основной поток будет свободен.
Например, так
export const optimizedHandleLogEvent = (heavyFunction: () => void, timeout: number = 5000): void => {
if (typeof window !== 'undefined') {
if ('requestIdleCallback' in window) {
requestIdleCallback(logFunction, { timeout });
} else {
setTimeout(logFunction, 0);
}
}
};
Также стоит избегать вызовов, которые введут к пересчету layout, так как процесс рендеринга будет достаточно долгим. Подробнее тут.
Largest Contentful Paint (LCP). Слишком медленная загрузка основного контента
Представить сайт без интерактивных элементов так же трудно, как сайт без картинок. Они есть на каждом сайте, и им нужно время, чтобы загрузиться. LCP измеряет скорость загрузки самой большой картинки на сайте (или не картинки, но об этом далее).
У LCP есть двоюродный брат — FCP: First Contentful Paint. Это метрика, измеряющая время от начала загрузки страницы до момента, когда браузер отображает первый видимый пользователю контент — например, текст или изображения. А двоюродный, потому что FCP не входит в состав Core Web Vitals.
В подсчет LCP попадают не только картинки, но и:
Изображения с тегом <img> (что очевидно);
тег <video> ;
картинки, которые описаны в css, то есть фоновые изображения;
любой текст, например <p>.
При этом есть подпункты, которые тоже должны соблюдаться. Иначе можно было бы добавить на картинку супер-лёгкую картинку, и пусть Google считает её за LCP. Итак:
Прозрачность картинки должна быть больше 0. Скрыть картинку под opacity: 0 не получится.
Размер картинки должен быть менее 100%.
Энтропия изображения должна быть больше 0,05.
На последнем пункте хочется остановиться поподробнее. Понятие «энтропия изображения» появилось в апреле 2023 года, когда возникла проблема «пустышек» — изображений, которые весили очень мало, а места занимали много.
Энтропия характеризует информационную насыщенность изображения. Энтропия изображения считается как вес картинки в байтах / размер картинки в пикселях. Например, возьмём портрет скульптора Донателло.

Для статьи фактический размер визуала уменьшен. В оригинале изображение весило 46,7 Кб и было размером 355 * 300 пикселей. Считаем:
В нашем случае 0,15 больше, чем 0,05. Следовательно, это изображение будет включено в подсчет LCP. А вот изображения с энтропией менее 0,05 — не будут. И это стоит учитывать.
Как записывается LCP и какой результат является хорошим?
Запись LCP начинается в момент загрузки страницы: браузер парсит HTML и строит DOM-дерево, одновременно запуская автоматическое сканирование элементов, которые могут претендовать на роль самого крупного контента.
Для каждого кандидата браузер вычисляет площадь отображения, исключая отступы и границы, но учитывая масштабирование. После этого в процессе загрузки ресурсов он динамически обновляет лидера по размеру, фиксируя время начала загрузки и полного отображения каждого элемента.
Всё, что загружается быстрее 2,5 секунд — хорошие показатели LCP. От 2,5 до 4 секунд — «жёлтая зона». Всё, что больше четырёх секунд, плохо.

Важный факт: измерение LCP заканчивается сразу же, если пользователь начинает взаимодействовать с сайтом — например, кликать на что-то или печатать (зато будет измеряться INP). Если пользователи вашего сервиса любят нервничать, информации будет не очень много.
Как улучшить показатель LCP?
Самое очевидное — можно попробовать уменьшить вес изображений, так как он влияет на скорость их загрузки, обработки и рендера на странице. Однако это может слабо помочь, поэтому можно оптимизировать и уменьшить:
— Time To First Byte (TTFB) — влияет в том числе на LCP;
— задержку загрузки ресурса;
— длительность загрузки ресурса;
— задержку перед рендерингом элемента;
На все эти пункты (кроме TTFB) влияет загруженность основного потока, подгрузка скриптов и прочее. Соответственно, чем меньше значения этих параметров, тем быстрее будут подгружаться большие картинки.

Что, например, можно сделать:
— Лучше загружать скрипты не сразу, добавить их в конец <body> или использовать async/defer.
— Самые важные стили лучше встроить в <head>, остальное загружать асинхронно вместе со скриптами.
— Загружайте сразу все важные шрифты и картинки: используйте preload для изображений, font-display: swap для избежания FOIT.
— Стоит оптимизировать TTFB и не забывать про пререндеринг для SPA, SSR кеширование и CDN.
Заключение
В первую очередь Core Web Vitals нужны для того, чтобы пользователям было удобно пользоваться сайтами. Ведь в итоге не разработчики, а они чувствуют на себе все проблемы плохих показателей. Core Web Vital (и не только) стоит отслеживать регулярно — тогда получится вовремя забить тревогу, если что-то не так, и улучшить взаимодействие с сайтом для пользователей, чтобы не потерять их.
Полезные ссылки
Посмотреть, что такое плохой CLS
Изучить аналитику различных страниц
Репозиторий с нашей телеметрией
Наш тестовый сайт с черепашками-ниндзя, за которыми стоят известные художники эпохи Ренессанса
alpaca
А для вашего проекта, как это в итоге повлияло? Есть метрики, что продажи выросли или какие-нибудь конверсии? Или Вы в SEO выше стали.