{"index":2,"slug":"editorial-2027-12-mechanism-author-manifesto","title":"Web performance budget: как читать LCP, INP и CLS вместе","excerpt":"Разбираем, почему один показатель не объясняет скорость интерфейса, как разделить LCP, INP и CLS и превратить наблюдение в проверяемое действие.","contentHtml":"
Проблема начинается с отчёта «страница медленная». Такой симптом не говорит, что именно увидел пользователь: крупнейший блок появился поздно, клик обработался с задержкой или контент прыгнул под пальцем. Цена ошибки — чинить не тот участок: уменьшить JavaScript, пока главный баннер ждёт шрифт, или ускорить загрузку, оставив интерфейс заблокированным длинной задачей.
\nПричина — свести LCP, INP и CLS к одному среднему score. Эти метрики отвечают на разные вопросы и требуют разных разрезов данных. LCP описывает появление крупнейшего видимого элемента, INP — задержку взаимодействия до следующей отрисовки, CLS — неожиданный сдвиг уже видимого layout. Сначала определите тип симптома и границу измерения, потом выбирайте изменение в коде.
\nBudget — это не красивое число в дашборде. Это набор порогов, сегментов, периода и действий владельца. Для каждого показателя нужно знать, какую часть опыта он описывает, где искать причину и что считать регрессией. Рекомендованные границы Core Web Vitals — LCP до 2500 мс, INP до 200 мс и CLS до 0,1. Они помогают классифицировать опыт, но не доказывают причину.
\nДля полевых данных используйте percentile, а не только среднее. Например, p75 показывает значение, ниже которого находится 75 процентов наблюдений в выбранном сегменте. Смешивать в одной строке мобильные и десктопные устройства, разные версии и разные периоды нельзя: итог потеряет смысл. В budget явно запишите URL, устройство, соединение, release и окно наблюдения.
\nLCP фиксирует момент, когда крупнейший контентный элемент стал видимым в пределах загрузки. На результат влияют TTFB, критический CSS, изображение, шрифт, сеть и работа браузера. Поэтому плохой LCP — сигнал разобрать цепочку, а не команда сразу добавить preload или сжать картинку.
\nINP оценивает задержку после взаимодействий пользователя. Ищите конкретное interaction, обработчик и long task на main thread. Причиной может быть тяжёлый обработчик, сторонний скрипт, лишняя синхронная работа или слабое устройство. Быстрый LCP не означает, что интерфейс быстро отвечает после ввода.
\nCLS (Cumulative Layout Shift) отвечает на вопрос: насколько неожиданно сдвигался уже видимый контент? В актуальном определении CLS выбирает самое большое session window — окно, в котором сдвиги идут с интервалами менее секунды и общей длительностью не более пяти секунд, — и суммирует баллы внутри него. Поэтому «сумма всех сдвигов за страницу» — устаревшее упрощение. Типовые источники — изображение без зарезервированных размеров, поздняя реклама, вставленный сверху контент или изменение шрифта. Для разбора нужны layout-shift entries, source-элементы и момент сдвига.
\n| Симптом | Вероятная причина | Проверка | Действие |
|---|---|---|---|
| LCP выше 2500 мс | TTFB, ресурс, CSS или шрифт | element timing, TTFB, waterfall | Ускорить критический путь и повторить замер |
| INP выше 200 мс | long task или тяжёлый handler | interaction trace и main thread | Разбить работу или перенести её после ответа |
| CLS выше 0,1 | Нет места под изображение или вставку | Session window, source-элемент и layout trace | Зарезервировать размер и стабилизировать layout |
| Среднее хорошее, p75 плохой | Длинный хвост или смешанные сегменты | p75 по URL, устройству и release | Разделить сегменты и назначить владельца |
| Lab и field расходятся | Разная среда и состав трафика | Сопоставить условия запуска | Не подменять один тип данных другим |
Допустим, в отчёте checkout для мобильного сегмента p75 INP хуже порога, а LCP и CLS проходят. Это учебный payload, а не измерение реального сайта: он показывает, какие поля нельзя потерять при передаче результата между сбором, дашбордом и владельцем.
\n{\n 'url': '/checkout',\n 'segment': { 'device': 'mobile', 'connection': '4g' },\n 'release': '2027.12.1',\n 'percentile': 75,\n 'metrics': { 'lcpMs': 2180, 'inpMs': 640, 'cls': 0.08 }\n}\nЗдесь проблема не в общем score: LCP 2180 мс и CLS 0,08 проходят рекомендованные границы, а INP 640 мс — нет. Следующий вопрос — не «как ускорить страницу», а «какое взаимодействие сформировало p75 и чем занят main thread перед следующим кадром».
\nДля triage можно использовать маленький классификатор. Он не измеряет браузер и не считает p75: функция принимает уже агрегированные значения одного URL и сегмента. Все пороги в коде — проектная копия опубликованных рекомендаций; при изменении официальных границ их нужно обновлять вместе с тестами.
\nconst BUDGET = {\n lcpMs: { good: 2500, poor: 4000 },\n inpMs: { good: 200, poor: 500 },\n cls: { good: 0.1, poor: 0.25 },\n};\n\nfunction classify(value, limits) {\n if (!Number.isFinite(value) || value < 0) return 'invalid';\n if (value <= limits.good) return 'good';\n if (value <= limits.poor) return 'needs-improvement';\n return 'poor';\n}\n\nfunction classifyWebVitals({ lcpMs, inpMs, cls }) {\n const status = {\n lcp: classify(lcpMs, BUDGET.lcpMs),\n inp: classify(inpMs, BUDGET.inpMs),\n cls: classify(cls, BUDGET.cls),\n };\n\n if (Object.values(status).includes('invalid')) {\n return { ok: false, reason: 'vital-input-invalid' };\n }\n\n const overall = Object.values(status).includes('poor') ? 'poor'\n : Object.values(status).includes('needs-improvement')\n ? 'needs-improvement'\n : 'good';\n\n return { ok: true, ...status, overall };\n}\n\nconsole.log(classifyWebVitals({ lcpMs: 2180, inpMs: 640, cls: 0.08 }));\n// { ok: true, lcp: 'good', inp: 'poor', cls: 'good', overall: 'poor' }\nКлассификатор нужен для единого triage, но он не отвечает на вопрос о причине. Улучшение LCP после preload не доказывает, что preload был единственной причиной: могли измениться сервер, кеш или состав трафика. Не превращайте результат функции в автоматический rollback без проверяемой связи с release и без отдельного анализа trace.
\nfield data: p75 + сегмент + release\n -> какая граница нарушена?\n -> какой объект её объясняет: resource / interaction / shift?\n -> какой один участок принадлежит владельцу?\n -> изменение и повторный замер в том же сегменте\n -> соседние метрики не ухудшились? да: оставить, нет: откатить\nПоток разделяет две операции. Метрика говорит, где болит пользовательский опыт. Trace или запись элемента показывает, что проверять в системе. Владелец изменения отвечает за участок кода или доставки, но не может объявить причину доказанной только по совпавшему времени.
\nЛокальный запуск классификатора не является field evidence. Performance Timeline даёт браузерные примитивы для доступа к entries и PerformanceObserver, но не проверяет вашу sampling-логику, backend-агрегацию или дашборд. Полевой INP может отсутствовать, если на странице не было подходящего взаимодействия. LCP зависит от эвристик и прекращает поиск кандидатов после определённых действий пользователя; bfcache и same-document navigation требуют отдельной обработки.
\nОптимизация одной метрики может ухудшить другую. Сжатие изображения уменьшает передаваемый объём, но может добавить CPU-декодирование или изменить момент отрисовки. Разделение JavaScript способно помочь INP, но добавить запросы и повлиять на LCP. Резервирование места снижает CLS, но не исправляет медленный ресурс. Рядом с Core Web Vitals проверяйте размер ресурсов, long tasks, ошибки и пользовательский сценарий.
\nГотовность наступает, когда для выбранного URL есть p75 LCP, INP и CLS по явно названным сегментам; для каждой плохой строки указан кандидат, interaction или source-элемент; изменение повторено тем же способом; соседние метрики не ухудшились. Если одного поля нет, вывод нужно назвать неполным, а не превращать его в общий score.
\n