8 lines
17 KiB
JSON
8 lines
17 KiB
JSON
{
|
||
"index": 212,
|
||
"slug": "editorial-2022-02-mechanism-performance-budget",
|
||
"title": "Бюджет производительности маршрута: почему общий PASS скрывает регрессию",
|
||
"excerpt": "Практическая модель бюджета маршрута: фиксируем условия, сравниваем именованные компоненты с допуском и останавливаем проверку, если входы больше не сопоставимы.",
|
||
"contentHtml": "<p>После небольшого изменения команда запускает проверку и получает PASS. Общая сумма работы выросла с 85 до 93 условных единиц, но осталась ниже общего лимита 98. Через несколько дней пользователи замечают, что первый экран стал реагировать позже. В отчёте нет явного нарушения: зелёный итог скрыл рост в одном компоненте.</p>\n<p>Цена такой ошибки — не только лишние миллисекунды. Команда выбирает неверное действие. Она меняет кеш, сеть или весь маршрут, хотя регрессия появилась в одном участке. Потом сравнения начинают проводиться в разных условиях, а бюджет теряет смысл.</p>\n<p><strong>Тезис:</strong> бюджет маршрута должен проверять именованные части до общей суммы. Общий итог нужен как дополнительный ограничитель. Он не отменяет нарушение отдельного компонента.</p>\n<h2>Что именно защищает бюджет</h2>\n<p>Бюджет — это не одно число в конфигурации. Он связывает маршрут, пользовательский сценарий, версию сборки, условия запуска и измеряемые части работы. Эти поля образуют measurement contract. Если контракт не записан, два числа «до» и «после» могут описывать разные события.</p>\n<p>В учебной модели ниже четыре поля: <code>documentTicks</code>, <code>styleTicks</code>, <code>scriptTicks</code> и <code>renderTicks</code>. Суффикс <code>Ticks</code> намеренный. Это условные значения локального примера. Они не являются LCP, TTFB, INP, DOMContentLoaded или результатом браузерного профиля.</p>\n<p>Настоящий браузер предоставляет другие записи. Например, Navigation Timing описывает навигацию документа, а Resource Timing — загрузку ресурсов. Сначала нужно получить такую запись из поддерживаемого API, затем явно сопоставить её поля с внутренней схемой. Нельзя переименовать произвольное число в LCP и получить от этого реальную метрику.</p>\n<h2>Механизм сравнения</h2>\n<p>Сравнение проходит четыре слоя.</p>\n<ol><li><strong>Контракт.</strong> Проверяем одинаковые маршрут, сценарий, версию workload и условия. В условия входят, например, тип навигации, cache policy, viewport, браузер и сеть.</li><li><strong>Snapshot.</strong> Сохраняем baseline с той же схемой полей. Baseline — это не «последний удачный отчёт», а точка отсчёта для конкретного контракта.</li><li><strong>Компоненты.</strong> Для каждого именованного поля проверяем собственный limit и tolerance. Результат должен показывать имя, baseline, candidate, allowed и delta.</li><li><strong>Решение.</strong> Overall получает FAIL, если нарушен хотя бы один компонент. Aggregate читается после component checks и остаётся вторичным guard.</li></ol>\n<p>Допуск — часть правила, а не скрытая скидка. При <code>limit = 28</code> и <code>tolerance = 2</code> порог равен 30. В отчёте нужно сохранить оба значения. Иначе нельзя отличить осознанный допуск от случайно изменённого лимита.</p>\n<pre><code>const contract = {\n route: '/checkout',\n scenario: 'open-and-submit',\n conditions: {\n browser: 'chromium',\n viewport: '390x844',\n cache: 'cold',\n network: 'fixed-4g'\n }\n};\n\nconst baseline = {\n documentTicks: 18,\n styleTicks: 15,\n scriptTicks: 26,\n renderTicks: 26\n};\n\nconst candidate = {\n documentTicks: 20,\n styleTicks: 16,\n scriptTicks: 34,\n renderTicks: 23\n};\n\nconst rules = {\n documentTicks: { limit: 20, tolerance: 2 },\n styleTicks: { limit: 18, tolerance: 2 },\n scriptTicks: { limit: 28, tolerance: 2 },\n renderTicks: { limit: 30, tolerance: 2 }\n};</code></pre>\n<p>В этом примере baseline равен 85, candidate — 93. Общий allowed равен 98, поэтому aggregate проходит. Но <code>scriptTicks</code> вырос с 26 до 34. Его allowed равен 30. Компонент нарушен, значит итоговая проверка должна вернуть FAIL. Разница между 34 и 28 равна 6. Разница между 34 и allowed равна 4. Обе величины полезны, если отчёт называет их однозначно.</p>\n<table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th>Симптом</th><th>Причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>Общий PASS, но один участок вырос</td><td>Aggregate скрывает component failure</td><td>Сравнить каждый field с его allowed</td><td>Остановить итог как FAIL и открыть только failed component</td></tr><tr><td>До и после нельзя честно сравнить</td><td>Различаются cache, route, browser или scenario</td><td>Сравнить все поля contract</td><td>Вернуть status <code>measurement-contract-invalid</code> и переснять candidate</td></tr><tr><td>Отчёт меняется от запуска к запуску</td><td>Условия не зафиксированы или шум выше tolerance</td><td>Повторить сценарий и записать условия</td><td>Изменить способ измерения или обосновать tolerance</td></tr><tr><td>Красный компонент приводит к глобальной оптимизации</td><td>Диагностика не ограничена именем поля</td><td>Проверить input и границу failed component</td><td>Сделать одну локальную проверку, не менять весь стек</td></tr><tr><td>Учебный отчёт называют browser metric</td><td>Внутреннее поле не сопоставлено с API</td><td>Найти источник и mapping для значения</td><td>Переименовать поле или добавить реальный сбор</td></tr></tbody></table>\n<h2>Почему нужен отрицательный путь</h2>\n<p>Представим, что baseline снят с cold cache, а candidate — с warm cache. Candidate может оказаться быстрее. Это не доказательство улучшения: изменился вход. То же происходит, если baseline относится к <code>/checkout</code>, а candidate — к <code>/cart</code>, или если один запуск включает авторизацию, а другой нет.</p>\n<p>Правильный результат в таком случае — не FAIL и не PASS. Сравнение нужно остановить с причиной <code>measurement-contract-invalid</code>. Component map остаётся пустым, aggregate не получает performance-смысл, а следующий шаг — восстановить один контракт и повторить измерение.</p>\n<p>Не стоит автоматически нормализовать несовпадение. Если система молча заменит <code>warm</code> на <code>cold</code> или возьмёт последний baseline, она создаст удобный, но ложный verdict. Лучше потерять один результат, чем принять несопоставимые числа за регрессию или улучшение.</p>\n<figure><img src=\"/assets/editorial/2022/performance-budget-trend-2022.svg\" alt=\"Учебное сравнение baseline и candidate: общий total растёт с 85 до 93 и остаётся ниже allowed 98, но scriptTicks растёт с 26 до 34 и превышает allowed 30\"><figcaption>Учебная иллюстрация: зелёный aggregate не отменяет красный named component. Значения показывают условные ticks, а не результаты production-профиля.</figcaption></figure>\n<h2>Как связать модель с браузерным измерением</h2>\n<p>Модель полезна только после явной границы между сбором и решением. Сбор получает официальную запись API. Адаптер выбирает поля, нормализует единицы и сохраняет условия. Сравниватель работает уже с проверенной внутренней схемой.</p>\n<pre><code>const navigation = performance.getEntriesByType('navigation')[0];\n\nconst observation = {\n source: 'PerformanceNavigationTiming',\n fields: {\n responseEnd: navigation.responseEnd,\n domInteractive: navigation.domInteractive,\n loadEventEnd: navigation.loadEventEnd\n },\n conditions: {\n route: location.pathname,\n navigationType: navigation.type\n }\n};\n\n// Учебный пример: здесь нет решения о PASS/FAIL.\n// Сначала нужен отдельный mapping в схему проекта.</code></pre>\n<p>Этот код показывает границу, а не готовый production-сборщик. Он не учитывает отправку данных, sampling, privacy, доступность API и различия браузеров. Он также не превращает три timestamp в четыре условных компонента автоматически. Mapping должен описывать формулу, единицы, поддержку браузеров и условия применимости.</p>\n<p>Если проекту нужен ресурсный бюджет, следует получить Resource Timing и отдельно решить, какие ресурсы входят в контракт. Документная навигация и загрузка каждого ресурса отвечают на разные вопросы. Смешивать их в один total без правила агрегации нельзя.</p>\n<h2>Порядок работы</h2>\n<ol><li>Назвать маршрут и пользовательский сценарий. Не использовать «страница в целом».</li><li>Записать условия: браузер, viewport, сеть, cache policy, версия сборки и тип навигации.</li><li>Определить поля и единицы. Для каждого поля указать источник, limit и tolerance.</li><li>Снять baseline и сохранить его вместе с contract. Не заменять его последним удачным запуском.</li><li>Снять candidate в тех же условиях. При изменении условий завершить проверку на validation error.</li><li>Сначала проверить наличие, набор и диапазон полей. Затем сравнить компоненты.</li><li>Посчитать aggregate только для контекста. Он не должен маскировать component failure.</li><li>Для первого нарушения назначить одну ограниченную проверку: конкретный input, ресурс или границу маршрута.</li><li>После изменения повторить измерение с тем же контрактом и сравнить новый candidate с тем же baseline.</li></ol>\n<h2>Ограничения модели</h2>\n<p>Учебные ticks не дают сведений о реальном количестве пользователей, SLA, полевых перцентилях или влиянии устройства. Даже настоящий browser trace не объясняет сам по себе причину регрессии. Он показывает наблюдение. Причину нужно искать в ресурсах, коде, серверном ответе, cache и сценарии.</p>\n<p>Один запуск не описывает шум. Число tolerance нельзя выбрать по привычке. Его обосновывают повторениями, средой, источником данных и ценой ложного срабатывания. Слишком большой допуск прячет регрессию. Слишком маленький превращает проверку в шумный сигнал.</p>\n<p>Бюджет также не заменяет продуктовую проверку. Быстрый маршрут может быть функционально неполным, а снижение одной метрики может ухудшить другой сценарий. Поэтому контракт должен ограничивать именно тот путь, который команда защищает, а рядом должны существовать проверки корректности.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Изменение готово, если маршрут и сценарий названы; условия сохранены; baseline и candidate имеют одну схему; каждое поле имеет limit и tolerance; отчёт показывает component checks отдельно от aggregate; mismatch условий останавливает сравнение; failed component ведёт к одной ограниченной проверке; повторный candidate снят в том же contract.</p>\n<p>Зелёный total сам по себе этому критерию не соответствует. Проверка считается полезной только тогда, когда по её результату можно понять, что именно нарушено, что нужно проверить дальше и почему сравнение вообще допустимо.</p>\n<h2>Проверяемые источники</h2>\n<ul><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> — описание <code>PerformanceNavigationTiming</code> и навигационных записей браузера.</li><li><a href=\"https://www.w3.org/TR/resource-timing/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Resource Timing</a> — спецификация записей о загрузке ресурсов.</li></ul>"
|
||
}
|