{ "index": 212, "slug": "editorial-2022-02-mechanism-performance-budget", "title": "Бюджет производительности маршрута: почему общий PASS скрывает регрессию", "excerpt": "Практическая модель бюджета маршрута: фиксируем условия, сравниваем именованные компоненты с допуском и останавливаем проверку, если входы больше не сопоставимы.", "contentHtml": "

После небольшого изменения команда запускает проверку и получает PASS. Общая сумма работы выросла с 85 до 93 условных единиц, но осталась ниже общего лимита 98. Через несколько дней пользователи замечают, что первый экран стал реагировать позже. В отчёте нет явного нарушения: зелёный итог скрыл рост в одном компоненте.

\n

Цена такой ошибки — не только лишние миллисекунды. Команда выбирает неверное действие. Она меняет кеш, сеть или весь маршрут, хотя регрессия появилась в одном участке. Потом сравнения начинают проводиться в разных условиях, а бюджет теряет смысл.

\n

Тезис: бюджет маршрута должен проверять именованные части до общей суммы. Общий итог нужен как дополнительный ограничитель. Он не отменяет нарушение отдельного компонента.

\n

Что именно защищает бюджет

\n

Бюджет — это не одно число в конфигурации. Он связывает маршрут, пользовательский сценарий, версию сборки, условия запуска и измеряемые части работы. Эти поля образуют measurement contract. Если контракт не записан, два числа «до» и «после» могут описывать разные события.

\n

В учебной модели ниже четыре поля: documentTicks, styleTicks, scriptTicks и renderTicks. Суффикс Ticks намеренный. Это условные значения локального примера. Они не являются LCP, TTFB, INP, DOMContentLoaded или результатом браузерного профиля.

\n

Настоящий браузер предоставляет другие записи. Например, Navigation Timing описывает навигацию документа, а Resource Timing — загрузку ресурсов. Сначала нужно получить такую запись из поддерживаемого API, затем явно сопоставить её поля с внутренней схемой. Нельзя переименовать произвольное число в LCP и получить от этого реальную метрику.

\n

Механизм сравнения

\n

Сравнение проходит четыре слоя.

\n
  1. Контракт. Проверяем одинаковые маршрут, сценарий, версию workload и условия. В условия входят, например, тип навигации, cache policy, viewport, браузер и сеть.
  2. Snapshot. Сохраняем baseline с той же схемой полей. Baseline — это не «последний удачный отчёт», а точка отсчёта для конкретного контракта.
  3. Компоненты. Для каждого именованного поля проверяем собственный limit и tolerance. Результат должен показывать имя, baseline, candidate, allowed и delta.
  4. Решение. Overall получает FAIL, если нарушен хотя бы один компонент. Aggregate читается после component checks и остаётся вторичным guard.
\n

Допуск — часть правила, а не скрытая скидка. При limit = 28 и tolerance = 2 порог равен 30. В отчёте нужно сохранить оба значения. Иначе нельзя отличить осознанный допуск от случайно изменённого лимита.

\n
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};
\n

В этом примере baseline равен 85, candidate — 93. Общий allowed равен 98, поэтому aggregate проходит. Но scriptTicks вырос с 26 до 34. Его allowed равен 30. Компонент нарушен, значит итоговая проверка должна вернуть FAIL. Разница между 34 и 28 равна 6. Разница между 34 и allowed равна 4. Обе величины полезны, если отчёт называет их однозначно.

\n
Симптом → причина → проверка → действие
СимптомПричинаПроверкаДействие
Общий PASS, но один участок выросAggregate скрывает component failureСравнить каждый field с его allowedОстановить итог как FAIL и открыть только failed component
До и после нельзя честно сравнитьРазличаются cache, route, browser или scenarioСравнить все поля contractВернуть status measurement-contract-invalid и переснять candidate
Отчёт меняется от запуска к запускуУсловия не зафиксированы или шум выше toleranceПовторить сценарий и записать условияИзменить способ измерения или обосновать tolerance
Красный компонент приводит к глобальной оптимизацииДиагностика не ограничена именем поляПроверить input и границу failed componentСделать одну локальную проверку, не менять весь стек
Учебный отчёт называют browser metricВнутреннее поле не сопоставлено с APIНайти источник и mapping для значенияПереименовать поле или добавить реальный сбор
\n

Почему нужен отрицательный путь

\n

Представим, что baseline снят с cold cache, а candidate — с warm cache. Candidate может оказаться быстрее. Это не доказательство улучшения: изменился вход. То же происходит, если baseline относится к /checkout, а candidate — к /cart, или если один запуск включает авторизацию, а другой нет.

\n

Правильный результат в таком случае — не FAIL и не PASS. Сравнение нужно остановить с причиной measurement-contract-invalid. Component map остаётся пустым, aggregate не получает performance-смысл, а следующий шаг — восстановить один контракт и повторить измерение.

\n

Не стоит автоматически нормализовать несовпадение. Если система молча заменит warm на cold или возьмёт последний baseline, она создаст удобный, но ложный verdict. Лучше потерять один результат, чем принять несопоставимые числа за регрессию или улучшение.

\n
\"Учебное
Учебная иллюстрация: зелёный aggregate не отменяет красный named component. Значения показывают условные ticks, а не результаты production-профиля.
\n

Как связать модель с браузерным измерением

\n

Модель полезна только после явной границы между сбором и решением. Сбор получает официальную запись API. Адаптер выбирает поля, нормализует единицы и сохраняет условия. Сравниватель работает уже с проверенной внутренней схемой.

\n
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 в схему проекта.
\n

Этот код показывает границу, а не готовый production-сборщик. Он не учитывает отправку данных, sampling, privacy, доступность API и различия браузеров. Он также не превращает три timestamp в четыре условных компонента автоматически. Mapping должен описывать формулу, единицы, поддержку браузеров и условия применимости.

\n

Если проекту нужен ресурсный бюджет, следует получить Resource Timing и отдельно решить, какие ресурсы входят в контракт. Документная навигация и загрузка каждого ресурса отвечают на разные вопросы. Смешивать их в один total без правила агрегации нельзя.

\n

Порядок работы

\n
  1. Назвать маршрут и пользовательский сценарий. Не использовать «страница в целом».
  2. Записать условия: браузер, viewport, сеть, cache policy, версия сборки и тип навигации.
  3. Определить поля и единицы. Для каждого поля указать источник, limit и tolerance.
  4. Снять baseline и сохранить его вместе с contract. Не заменять его последним удачным запуском.
  5. Снять candidate в тех же условиях. При изменении условий завершить проверку на validation error.
  6. Сначала проверить наличие, набор и диапазон полей. Затем сравнить компоненты.
  7. Посчитать aggregate только для контекста. Он не должен маскировать component failure.
  8. Для первого нарушения назначить одну ограниченную проверку: конкретный input, ресурс или границу маршрута.
  9. После изменения повторить измерение с тем же контрактом и сравнить новый candidate с тем же baseline.
\n

Ограничения модели

\n

Учебные ticks не дают сведений о реальном количестве пользователей, SLA, полевых перцентилях или влиянии устройства. Даже настоящий browser trace не объясняет сам по себе причину регрессии. Он показывает наблюдение. Причину нужно искать в ресурсах, коде, серверном ответе, cache и сценарии.

\n

Один запуск не описывает шум. Число tolerance нельзя выбрать по привычке. Его обосновывают повторениями, средой, источником данных и ценой ложного срабатывания. Слишком большой допуск прячет регрессию. Слишком маленький превращает проверку в шумный сигнал.

\n

Бюджет также не заменяет продуктовую проверку. Быстрый маршрут может быть функционально неполным, а снижение одной метрики может ухудшить другой сценарий. Поэтому контракт должен ограничивать именно тот путь, который команда защищает, а рядом должны существовать проверки корректности.

\n

Проверяемый критерий готовности

\n

Изменение готово, если маршрут и сценарий названы; условия сохранены; baseline и candidate имеют одну схему; каждое поле имеет limit и tolerance; отчёт показывает component checks отдельно от aggregate; mismatch условий останавливает сравнение; failed component ведёт к одной ограниченной проверке; повторный candidate снят в том же contract.

\n

Зелёный total сам по себе этому критерию не соответствует. Проверка считается полезной только тогда, когда по её результату можно понять, что именно нарушено, что нужно проверить дальше и почему сравнение вообще допустимо.

\n

Проверяемые источники

\n" }