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

8 lines
21 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 213,
"slug": "editorial-2022-02-practice-performance-budget",
"title": "Бюджет производительности: как не спрятать регрессию за общим PASS",
"excerpt": "Как описать один пользовательский путь, разделить его бюджет на именованные части и остановить изменение, которое превышает допуск, даже если общий результат ещё выглядит зелёным.",
"contentHtml": "<p>Проблема начинается с маленького изменения: на странице оформления следующий шаг появляется позже, чем ожидает пользователь. В отчёте при этом стоит зелёный PASS: общий размер страницы и суммарное время не вышли за предел. Команда добавляет виджет, стиль и обработчик. Каждый коммит проходит проверку, но через несколько релизов один участок пути забирает весь запас.</p>\n<p>Цена ошибки — не абстрактная «медленная страница». Пользователь дольше ждёт перехода, повторяет действие или закрывает вкладку. Инженер ищет причину среди уже принятых изменений. Если budget проверяет только сумму, он не отвечает на главный вопрос: какой компонент потратил запас и что нужно остановить?</p>\n<p>Вывод статьи простой: бюджет производительности должен описывать конкретный пользовательский путь, условия измерения и несколько именованных ограничений. Общая сумма полезна как дополнительный сигнал. Она не должна отменять провал отдельной части.</p>\n<h2>Что именно ограничивает бюджет</h2>\n<p>Performance budget — это набор заранее названных лимитов для метрик, связанных с производительностью. В него можно включить размер страницы или скриптов, число HTTP-запросов и время загрузки в заданном сетевом сценарии. Это не универсальная норма: лимит имеет смысл только вместе с путём, данными и условиями измерения.</p>\n<p>В учебном примере входом служат маршрут <code>/training/checkout/review</code> и сценарий <code>anonymous-cart-with-one-item</code>. Это условный маршрут, а не результат запуска настоящего сайта. Другой товар, авторизация, локаль или feature flag могут изменить набор ресурсов, поэтому их нельзя молча смешивать с тем же budget.</p>\n<p>Рядом с числами запишите версию сборки, сеть, состояние cache, устройство или профиль CPU, способ запуска и источник метрики. Если условие не измерялось, его нельзя восстанавливать по одному итоговому числу.</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>Route и scenario</td><td><code>/training/checkout/review</code>, одна корзина</td><td>Сравнивает один и тот же вход</td><td>Качество всех страниц</td></tr><tr><td>Conditions</td><td>Сборка, сеть, cache и устройство записаны</td><td>Показывает сопоставимость замеров</td><td>Поведение другой среды</td></tr><tr><td>Components</td><td><code>document</code>, <code>style</code>, <code>script</code>, <code>render</code></td><td>Находит участок, который забрал запас</td><td>Автоматически не объясняет причину</td></tr><tr><td>Limit и tolerance</td><td>Допуск у каждого имени</td><td>Даёт правило остановки</td><td>Универсальную норму продукта</td></tr><tr><td>Total</td><td>Вторичный предел суммы</td><td>Замечает общий рост</td><td>Разрешение на провал компонента</td></tr></tbody></table>\n<h2>Почему одна сумма даёт ложный PASS</h2>\n<p>Возьмём четыре части с лимитами 26, 17, 30 и 22 условные единицы. Их сумма равна 95. В baseline значения — 24, 15, 26 и 20. Candidate меняет только <code>script</code>: 26 становится 34. Итог candidate равен 93, поэтому total проходит. Но для <code>script</code> allowed равен 32: limit 30 плюс tolerance 2. Именованный компонент должен получить FAIL.</p>\n<p>Это перенос затрат. Остальные части не стали дешевле из-за тяжёлого скрипта. Если проверять только сумму, рост в одном месте можно случайно компенсировать уменьшением в другом. Для пользователя это не всегда равноценная замена: лишний JavaScript может задержать обработчик, а уменьшение картинки не уберёт работу на основном потоке.</p>\n<figure><img src=\"/assets/editorial/2022/performance-budget-allocation-2022.svg\" alt=\"Схема бюджета пути: document, style, script и render имеют отдельные лимиты, а общий total проходит при провале script\" loading=\"lazy\" /><figcaption>Учебная схема разделяет именованные проверки и общий предел. Она не показывает реальный trace, браузерный запуск или полевую метрику.</figcaption></figure>\n<h2>Воспроизводимый контракт сравнения</h2>\n<p>До сравнения candidate проверьте не только значения, но и сам вход. Код работает с frozen plain objects в памяти. Поля <code>documentTicks</code>, <code>styleTicks</code>, <code>scriptTicks</code> и <code>renderTicks</code> — условные единицы, а не названия браузерных метрик.</p>\n<pre><code>const contract = Object.freeze({\n route: '/training/checkout/review',\n scenario: 'anonymous-cart-with-one-item',\n conditions: Object.freeze({\n network: 'slow-3g',\n cache: 'cold',\n device: 'mobile',\n }),\n components: Object.freeze({\n documentTicks: Object.freeze({ limit: 26, tolerance: 1 }),\n styleTicks: Object.freeze({ limit: 17, tolerance: 1 }),\n scriptTicks: Object.freeze({ limit: 30, tolerance: 2 }),\n renderTicks: Object.freeze({ limit: 22, tolerance: 1 }),\n }),\n totalTicks: Object.freeze({ limit: 95, tolerance: 3 }),\n});\n\nconst baseline = {\n route: contract.route,\n scenario: contract.scenario,\n conditions: { ...contract.conditions },\n values: {\n documentTicks: 24,\n styleTicks: 15,\n scriptTicks: 26,\n renderTicks: 20,\n },\n};\n\nconst candidate = {\n ...baseline,\n values: { ...baseline.values, scriptTicks: 34 },\n};\n\nfunction hasExactKeys(value, expectedKeys) {\n if (!value || typeof value !== 'object') return false;\n const actualKeys = Object.keys(value);\n return actualKeys.length === expectedKeys.length\n && expectedKeys.every((key) =&gt; actualKeys.includes(key));\n}\n\nfunction sameRecord(left, right) {\n const keys = Object.keys(right);\n return hasExactKeys(left, keys)\n &amp;&amp; keys.every((key) =&gt; left[key] === right[key]);\n}\n\nfunction checkBudget(snapshot, expected) {\n const names = Object.keys(expected.components);\n if (snapshot.route !== expected.route\n || snapshot.scenario !== expected.scenario\n || !sameRecord(snapshot.conditions, expected.conditions)) {\n return { status: 'INVALID', reason: 'measurement-contract-mismatch' };\n }\n\n if (!snapshot.values || !hasExactKeys(snapshot.values, names)\n || !names.every((name) =&gt;\n Number.isFinite(snapshot.values[name]) &amp;&amp; snapshot.values[name] &gt;= 0\n )) {\n return { status: 'INVALID', reason: 'invalid-component-values' };\n }\n\n const components = names.map((name) =&gt; {\n const rule = expected.components[name];\n const allowed = rule.limit + rule.tolerance;\n const value = snapshot.values[name];\n return { name, value, allowed, pass: value &lt;= allowed };\n });\n const total = names.reduce((sum, name) =&gt; sum + snapshot.values[name], 0);\n const totalAllowed = expected.totalTicks.limit\n + expected.totalTicks.tolerance;\n const totalPass = total &lt;= totalAllowed;\n\n return {\n status: components.every((item) =&gt; item.pass) &amp;&amp; totalPass\n ? 'PASS'\n : 'FAIL',\n components,\n total,\n totalAllowed,\n totalPass,\n };\n}\n\nconsole.log(checkBudget(candidate, contract));\n// status: 'FAIL', scriptTicks: 34 &gt; 32, totalPass: true</code></pre>\n<p>Функция вернёт FAIL по <code>scriptTicks</code>: 34 больше allowed 32. Сумма равна 93 и меньше totalAllowed 98, но <code>totalPass: true</code> не меняет общего verdict. Если route, scenario, conditions или состав полей расходятся, результатом будет <code>INVALID</code>, а не ложная регрессия.</p>\n<p>Проверка не измеряет браузер. Она проверяет инвариант контракта: известные неотрицательные числа сравниваются по отдельности и затем суммируются. В рабочем проекте нужно отдельно получить эти числа, сохранить источник и связать каждое поле с владельцем измерения.</p>\n<h2>Симптомы и ограниченное действие</h2>\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>Total проходит, component падает</td><td>Рост скрыт внутри суммы</td><td>Сравнить каждую часть с её allowed</td><td>Остановить изменение и найти владельца части</td></tr><tr><td>Два запуска дают разные выводы</td><td>Не совпали сеть, cache, CPU или данные</td><td>Сопоставить conditions и route</td><td>Повторить в фиксированных условиях или разделить сценарии</td></tr><tr><td>Число выросло, причина неизвестна</td><td>Сохранён результат без состава</td><td>Проверить размер chunk, запросы и источник</td><td>Добавить детализацию, а не повышать лимит вслепую</td></tr><tr><td>Провал виден только у одной локали</td><td>Сценарии смешаны в одном job</td><td>Запустить тот же route отдельно</td><td>Включить локаль в scenario или выделить budget</td></tr><tr><td>Старый snapshot проходит после смены сборщика</td><td>Baseline получен в другой среде</td><td>Сравнить версию и формат данных</td><td>Создать baseline с причиной замены</td></tr><tr><td>После каждого FAIL повышают лимит</td><td>Лимит используют для удаления сигнала</td><td>Проверить ресурс и обоснование</td><td>Устранить рост или зафиксировать исключение</td></tr></tbody></table>\n<h2>Как выбрать источник измерения</h2>\n<p>Размер ресурсов и число запросов удобно проверять на этапе сборки. Они рано показывают состав артефакта и помогают остановить тяжёлую зависимость. Но одинаковый вес не гарантирует одинаковое восприятие: критический ресурс может прийти позже, а порядок загрузки меняет полезный результат.</p>\n<p>Для времени навигации браузер предоставляет Navigation Timing. Через <code>PerformanceNavigationTiming</code> можно получить запись текущего документа и разобрать интервалы ответа сервера, DOMContentLoaded и load. Эти значения описывают навигацию; они не превращают условные <code>ticks</code> из примера в реальные миллисекунды.</p>\n<p>Для отдельных ресурсов используется <code>PerformanceResourceTiming</code>. Он даёт временную шкалу загрузки и сведения о размере ресурса. Для cross-origin ресурсов подробные поля могут быть недоступны без <code>Timing-Allow-Origin</code>. Отсутствие данных нельзя принять за нулевую задержку.</p>\n<p>Не называйте сумму размеров «временем до интерактивности» и не называйте один синтетический snapshot полевой метрикой. В budget укажите класс источника, окно наблюдения, сегмент устройств и способ агрегации.</p>\n<h2>Порядок внедрения</h2>\n<ol><li>Выберите один путь, связанный с действием пользователя, и запишите route, scenario и ожидаемый результат.</li><li>Назначьте источник каждого числа: размер артефакта, browser timing, synthetic run или field data. Не смешивайте эти классы.</li><li>Зафиксируйте версию сборки, сеть, cache, устройство, данные и feature flags.</li><li>Разделите budget на части, которыми можно управлять отдельно: document, style, script, render или другой состав для этого пути.</li><li>Снимите baseline и сохраните его рядом с conditions. Не называйте один запуск истиной без диапазона и повторов.</li><li>Сначала проверяйте каждую часть, затем total. В отчёте печатайте оба verdict.</li><li>Прогоните положительный и отрицательный сценарии: один компонент должен превысить свой allowed при зелёном total.</li><li>Для каждого FAIL назначьте действие: уменьшить ресурс, убрать запрос, отложить код, изменить scenario или пересмотреть контракт с причиной.</li></ol>\n<h2>Ограничения</h2>\n<p>Budget не исправляет медленную страницу. Он превращает выбранное ожидание в сигнал. Плохой route, неверный baseline или неописанные conditions дадут точный ответ на неверный вопрос.</p>\n<p>Лимиты зависят от продукта. Экран с фотографиями, форма оплаты и текстовая статья имеют разный состав ресурсов. Одного числа для всех страниц недостаточно. Разделяйте budgets по типу пути, но не создавайте отдельный лимит для каждого случайного варианта: сигнал распадётся и станет необслуживаемым.</p>\n<p>Лабораторные данные помогают сравнивать изменения в одинаковой среде. Полевые данные показывают разброс устройств, сетей и поведения посетителей. Учебный код не содержит ни тех, ни других данных. Он проверяет только логику контракта.</p>\n<p>Иногда рост оправдан функцией. Тогда измените budget вместе с причиной, владельцем и способом повторной проверки. Не повышайте лимит только для зелёного статуса.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Контракт готов, если другой инженер может ответить на четыре вопроса: какой путь проверяется, в каких условиях, какая часть превысила допуск и какое действие следует выполнить. Автоматическая проверка должна завершаться FAIL, когда один компонент превышает свой allowed, даже если total остаётся внутри общего предела.</p>\n<p>Минимальный набор таков: baseline и candidate имеют одинаковые route, scenario и conditions; состав полей совпадает; каждое значение неотрицательно и конечно; результат печатает component verdict и total verdict; положительный пример проходит; отрицательный пример падает на <code>scriptTicks</code>. Это проверяет механизм, но не заявляет эффект на реальном трафике.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://web.dev/articles/performance-budgets-101\" target=\"_blank\" rel=\"noopener noreferrer\">web.dev: Performance budgets 101</a> — определяет budget как набор лимитов и приводит размеры страницы, время загрузки, число запросов и ограничения для ресурсов как варианты метрик. Страница не подтверждает условные числа этой статьи.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: PerformanceResourceTiming</a> — описывает временную шкалу загрузки ресурсов, сведения о размере и ограничение cross-origin timing без <code>Timing-Allow-Origin</code>.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Navigation_timing\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: Navigation timing</a> — описывает navigation timing entries текущего документа и границы API. Источники не превращают учебные <code>ticks</code> в browser measurement.</li></ul>"
}