Files
progcode/editorial/agent-rewrites/002.json
T

2 lines
17 KiB
JSON

{"index":2,"slug":"editorial-2027-12-mechanism-author-manifesto","title":"Web performance budget: как читать LCP, INP и CLS вместе","excerpt":"Разбираем, почему один показатель не объясняет скорость интерфейса, как разделить LCP, INP и CLS и превратить наблюдение в проверяемое действие.","contentHtml":"<p>Проблема начинается с отчёта «страница медленная». Такой симптом не говорит, что именно увидел пользователь: крупнейший блок появился поздно, клик обработался с задержкой или контент прыгнул под пальцем. Цена ошибки — чинить не тот участок: уменьшить JavaScript, пока главный баннер ждёт шрифт, или ускорить загрузку, оставив интерфейс заблокированным длинной задачей.</p>\n<p>Причина — свести LCP, INP и CLS к одному среднему score. Эти метрики отвечают на разные вопросы и требуют разных разрезов данных. LCP описывает появление крупнейшего видимого элемента, INP — задержку взаимодействия до следующей отрисовки, CLS — неожиданный сдвиг уже видимого layout. Сначала определите тип симптома и границу измерения, потом выбирайте изменение в коде.</p>\n<h2>Тезис: performance budget состоит из нескольких проверяемых границ</h2>\n<p>Budget — это не красивое число в дашборде. Это набор порогов, сегментов, периода и действий владельца. Для каждого показателя нужно знать, какую часть опыта он описывает, где искать причину и что считать регрессией. Рекомендованные границы Core Web Vitals — LCP до 2500 мс, INP до 200 мс и CLS до 0,1. Они помогают классифицировать опыт, но не доказывают причину.</p>\n<p>Для полевых данных используйте percentile, а не только среднее. Например, p75 показывает значение, ниже которого находится 75 процентов наблюдений в выбранном сегменте. Смешивать в одной строке мобильные и десктопные устройства, разные версии и разные периоды нельзя: итог потеряет смысл. В budget явно запишите URL, устройство, соединение, release и окно наблюдения.</p>\n<h2>Механизм: три метрики — три объекта диагностики</h2>\n<p>LCP фиксирует момент, когда крупнейший контентный элемент стал видимым в пределах загрузки. На результат влияют TTFB, критический CSS, изображение, шрифт, сеть и работа браузера. Поэтому плохой LCP — сигнал разобрать цепочку, а не команда сразу добавить preload или сжать картинку.</p>\n<p>INP оценивает задержку после взаимодействий пользователя. Ищите конкретное interaction, обработчик и long task на main thread. Причиной может быть тяжёлый обработчик, сторонний скрипт, лишняя синхронная работа или слабое устройство. Быстрый LCP не означает, что интерфейс быстро отвечает после ввода.</p>\n<p><strong>CLS (Cumulative Layout Shift)</strong> отвечает на вопрос: насколько неожиданно сдвигался уже видимый контент? В актуальном определении CLS выбирает самое большое session window — окно, в котором сдвиги идут с интервалами менее секунды и общей длительностью не более пяти секунд, — и суммирует баллы внутри него. Поэтому «сумма всех сдвигов за страницу» — устаревшее упрощение. Типовые источники — изображение без зарезервированных размеров, поздняя реклама, вставленный сверху контент или изменение шрифта. Для разбора нужны layout-shift entries, source-элементы и момент сдвига.</p>\n<table><caption>Как переводить симптом в проверку</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Вероятная причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>LCP выше 2500 мс</td><td>TTFB, ресурс, CSS или шрифт</td><td>element timing, TTFB, waterfall</td><td>Ускорить критический путь и повторить замер</td></tr><tr><td>INP выше 200 мс</td><td>long task или тяжёлый handler</td><td>interaction trace и main thread</td><td>Разбить работу или перенести её после ответа</td></tr><tr><td>CLS выше 0,1</td><td>Нет места под изображение или вставку</td><td>Session window, source-элемент и layout trace</td><td>Зарезервировать размер и стабилизировать layout</td></tr><tr><td>Среднее хорошее, p75 плохой</td><td>Длинный хвост или смешанные сегменты</td><td>p75 по URL, устройству и release</td><td>Разделить сегменты и назначить владельца</td></tr><tr><td>Lab и field расходятся</td><td>Разная среда и состав трафика</td><td>Сопоставить условия запуска</td><td>Не подменять один тип данных другим</td></tr></tbody></table>\n<h2>Рабочий пример: сначала зафиксировать контекст</h2>\n<p>Допустим, в отчёте checkout для мобильного сегмента p75 INP хуже порога, а LCP и CLS проходят. Это учебный payload, а не измерение реального сайта: он показывает, какие поля нельзя потерять при передаче результата между сбором, дашбордом и владельцем.</p>\n<pre><code>{\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}</code></pre>\n<p>Здесь проблема не в общем score: LCP 2180 мс и CLS 0,08 проходят рекомендованные границы, а INP 640 мс — нет. Следующий вопрос — не «как ускорить страницу», а «какое взаимодействие сформировало p75 и чем занят main thread перед следующим кадром».</p>\n<p>Для triage можно использовать маленький классификатор. Он не измеряет браузер и не считает p75: функция принимает уже агрегированные значения одного URL и сегмента. Все пороги в коде — проектная копия опубликованных рекомендаций; при изменении официальных границ их нужно обновлять вместе с тестами.</p>\n<pre><code>const 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 &lt; 0) return 'invalid';\n if (value &lt;= limits.good) return 'good';\n if (value &lt;= 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' }</code></pre>\n<p>Классификатор нужен для единого triage, но он не отвечает на вопрос о причине. Улучшение LCP после preload не доказывает, что preload был единственной причиной: могли измениться сервер, кеш или состав трафика. Не превращайте результат функции в автоматический rollback без проверяемой связи с release и без отдельного анализа trace.</p>\n<figure><img src=\"/assets/editorial/2027/author-manifesto-2027-quality-rubric-matrix.svg\" alt=\"Матрица web-performance связывает LCP, INP и CLS с разными объектами измерения, порогами и диагностическими разрезами\" loading=\"lazy\" /><figcaption>Один score скрывает разные механизмы. Для каждой метрики нужен собственный объект проверки и действие.</figcaption></figure>\n<h2>Flow: от строки в дашборде к изменению</h2>\n<pre aria-label='Поток диагностики web performance'><code>field data: p75 + сегмент + release\n -&gt; какая граница нарушена?\n -&gt; какой объект её объясняет: resource / interaction / shift?\n -&gt; какой один участок принадлежит владельцу?\n -&gt; изменение и повторный замер в том же сегменте\n -&gt; соседние метрики не ухудшились? да: оставить, нет: откатить</code></pre>\n<p>Поток разделяет две операции. Метрика говорит, где болит пользовательский опыт. Trace или запись элемента показывает, что проверять в системе. Владелец изменения отвечает за участок кода или доставки, но не может объявить причину доказанной только по совпавшему времени.</p>\n<h2>Порядок работы</h2>\n<ol><li>Определите URL, сегмент, percentile и окно наблюдения. Не сравнивайте p75 мобильного трафика со средним по всем устройствам.</li><li>Для плохого LCP найдите кандидат и разделите TTFB, загрузку ресурса и отрисовку. Для INP найдите interaction и разложите задержку на input, processing и presentation. Для CLS найдите session window и source-элементы.</li><li>Сформулируйте один budget на релиз и один диагностический сигнал. Не блокируйте сборку по метрике, которую CI не может воспроизвести.</li><li>Сопоставьте лабораторные и полевые данные отдельно. Lighthouse удобен для воспроизводимого CI, field data показывает реальное разнообразие сети и устройств.</li><li>Измените один тяжёлый участок: критический ресурс, обработчик, размеры изображения или резервирование места. Зафиксируйте владельца и путь отката.</li><li>Повторите измерение тем же сегментом и окном. Улучшение LCP не закрывает автоматически INP и CLS.</li></ol>\n<h2>Ограничения и критерий готовности</h2>\n<p>Локальный запуск классификатора не является field evidence. Performance Timeline даёт браузерные примитивы для доступа к entries и PerformanceObserver, но не проверяет вашу sampling-логику, backend-агрегацию или дашборд. Полевой INP может отсутствовать, если на странице не было подходящего взаимодействия. LCP зависит от эвристик и прекращает поиск кандидатов после определённых действий пользователя; bfcache и same-document navigation требуют отдельной обработки.</p>\n<p>Оптимизация одной метрики может ухудшить другую. Сжатие изображения уменьшает передаваемый объём, но может добавить CPU-декодирование или изменить момент отрисовки. Разделение JavaScript способно помочь INP, но добавить запросы и повлиять на LCP. Резервирование места снижает CLS, но не исправляет медленный ресурс. Рядом с Core Web Vitals проверяйте размер ресурсов, long tasks, ошибки и пользовательский сценарий.</p>\n<p>Готовность наступает, когда для выбранного URL есть p75 LCP, INP и CLS по явно названным сегментам; для каждой плохой строки указан кандидат, interaction или source-элемент; изменение повторено тем же способом; соседние метрики не ухудшились. Если одного поля нет, вывод нужно назвать неполным, а не превращать его в общий score.</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://www.w3.org/TR/2026/WD-largest-contentful-paint-20260826/' target='_blank' rel='noopener noreferrer'>W3C Largest Contentful Paint, Working Draft от 26 августа 2026 года</a> — API, кандидаты LCP и ограничения эвристического измерения. Это рабочий draft: он может измениться, поэтому не используйте его как неизменный контракт браузера.</li><li><a href='https://web.dev/articles/inp' target='_blank' rel='noopener noreferrer'>web.dev: Interaction to Next Paint</a> — состав interaction latency, границы 200/500 мс и p75 для полевых page loads; страница обновлена 2 сентября 2025 года.</li><li><a href='https://web.dev/articles/cls' target='_blank' rel='noopener noreferrer'>web.dev: Cumulative Layout Shift</a> — session window, source-элементы и границы 0,1/0,25; официальная guidance может уточняться вместе с инструментами.</li><li><a href='https://web.dev/articles/defining-core-web-vitals-thresholds' target='_blank' rel='noopener noreferrer'>web.dev: How the Core Web Vitals metrics thresholds were defined</a> — общая таблица порогов и правило p75; пороги — практическая рекомендация, а не причинная модель конкретной регрессии.</li><li><a href='https://www.w3.org/TR/performance-timeline/' target='_blank' rel='noopener noreferrer'>W3C Performance Timeline</a> — PerformanceEntry и PerformanceObserver для чтения браузерных измерений. Это Candidate Recommendation Draft, поэтому API и поддерживаемые entry types нужно проверять в целевых браузерах.</li></ul>"}