DarkRiDDeR11 мин

Бюджет пользовательского пути: общий PASS больше не скрывает регрессию

ПроизводительностьFrontend

Проблема начинается на странице оформления: пользователь ждёт, пока станет доступен следующий шаг. Изменения приходят маленькими: новый код, стиль, виджет, правило рендера. Каждое по отдельности проходит общий check. Через несколько релизов путь стал тяжелее, но CI всё ещё отдаёт зелёный PASS. Цена такого сигнала проста: он говорит, что не пересечён порог, но не говорит, какая часть пути забрала запас и где искать изменение.

Не надо отвечать на это обещанием «ускорить сайт». Сначала нужен один воспроизводимый контракт для конкретного пути: route, сценарий, условия наблюдения, snapshot и именные части бюджета. Тогда проверка отвечает на скромный вопрос: candidate остаётся внутри заранее названного допуска или нет. Она не выдаёт учебный расчёт за браузерный trace, реальную пользовательскую метрику, SLA или результат существующего CI.

Начинаем с пути, а не с общей оценки

Бюджет полезен только у входа, который можно назвать. В fixture это /training/checkout/review и сценарий anonymous-cart-with-one-item. Название намеренно длиннее слова «главная»: при другом товаре, авторизации, locale или feature flag это уже может быть другой вход. Если один job смешивает такие варианты, изменение в одном из них способно спрятаться за средним числом другого.

Дальше фиксируются условия. В учебной модели это version workload, единица teaching-ticks, а browser observation, CI observation, сеть, CPU, cache и trace имеют значение unavailable-in-example или outside-example. Эти поля не делают проверку слабой. Они запрещают дорисовать доказательство задним числом: пока нет trace, нельзя из суммы ticks сделать вывод о браузере; пока нет CI run, нельзя назвать fixture результатом pipeline.

Контракт учебного бюджета одного пользовательского пути
ЭлементЧто хранит fixtureДля чего нуженЧего не доказывает
Route и scenario/training/checkout/review, одна корзинане смешать разные входычто путь уже прошёл в production
Условияversion workload, ticks, unavailable-in-exampleостановить сравнение при другой средесвойства браузера или CI runner
КомпонентыdocumentTicks, styleTicks, scriptTicks, renderTicksувидеть вклад по частямстандартные Web Vitals
Допускlimit плюс tolerance у каждого именисделать правило явнымуниверсальную норму для любого продукта
Aggregateвторичный total guardувидеть суммарный запасправо скрыть failed component

Разделите запас до того, как появится candidate

Слово «бюджет» часто превращают в одну сумму. Для пользовательского пути это недостаточно. Один общий предел замечает только итог, а не перенос затрат между частями. В fixture у scriptTicks limit равен 28 и tolerance равен 2; значит допустимо до 30. У total limit 95 и tolerance 3. Эти числа условны. Их смысл не в удачном размере, а в том, что limit и допуск существуют отдельно и печатаются в результате проверки.

Почему допуск не стоит прятать в комментарии? Потому что без него нельзя понять, какой сдвиг команда заранее согласилась игнорировать и в каких условиях. Если tolerance нужен из-за нестабильности настоящего замера, эта нестабильность должна быть описана в measurement contract. В fixture она не измеряется: поэтому допуск — проектное правило учебного объекта, не статистическая модель и не утверждение о дисперсии браузера.

Вертикальная схема учебного бюджета пути checkout review: четыре именованные части document, style, script и render имеют свои limit и tolerance. Baseline укладывается во все части, candidate превышает допустимый scriptTicks, хотя общий total остаётся в пределах вторичного порога.
Распределение показывает порядок проверки: сначала сравниваются именованные части с явным допуском, затем читается общий total. Зелёная сумма не отменяет красный component check.

Snapshot и candidate должны быть сопоставимы

Snapshot — не «последний хороший отчёт». Это сохранённый объект с route, scenario, conditions и четырьмя учебными полями. Baseline в fixture содержит 24, 15, 26, 20; candidate меняет только scriptTicks на 34. Сумма candidate равна 93, поэтому aggregate остаётся PASS до allowed 98. Но component rule разрешает максимум 30, и общий результат обязан быть FAIL. Именно это защищает от красивой, но бесполезной зелёной суммы.

import { runPerformanceBudgetFixture } from "./upgrade-2022-02.mjs";

const fixture = runPerformanceBudgetFixture();
const baseline = fixture.baselineComparison;
const candidate = fixture.candidateComparison;

console.log(baseline.overall.status);
// pass
console.log(candidate.aggregate.status);
// pass — aggregate не заметил сдвиг одной части
console.log(candidate.overall.status);
// fail — named component budget имеет приоритет

if (candidate.componentChecks.scriptTicks.status !== "fail") {
  throw new Error("fixture must expose the named regression");
}

// In-memory training model: no browser, CI, trace, network or Web Vitals.

Код не вызывает Lighthouse, Chrome, PerformanceObserver или сетевой запрос. Он не получает timestamps. Его роль — зафиксировать invariant модели: named component сильнее aggregate. В рабочем проекте аналогичный object можно заполнить данными выбранного теста, но только после того, как команда назвала источник, версию инструмента, контролируемые условия и смысл каждого поля. Подмена этих границ «примерным CI» создаёт ту же ложную уверенность, от которой пытаемся уйти.

const firstFailure = Object.values(candidate.componentChecks)
  .find((check) => check.status === "fail");

if (candidate.aggregate.status === "pass" && firstFailure) {
  console.log(firstFailure.component);
  // scriptTicks: aggregate must not erase this result
}

Маршрут: симптом → причина → проверка → действие

  1. Симптом. Общая проверка стабильно зелёная, но конкретный пользовательский путь постепенно меняется и становится труднее объяснить.
  2. Причина. Aggregate объединяет части пути и не показывает, какая из них съела запас; baseline и candidate могут быть собраны при разных условиях.
  3. Проверка контракта. Назовите route, сценарий, version workload, единицу, наличие или отсутствие browser/CI observation. Если conditions отличаются, не сравнивайте числа.
  4. Проверка snapshot. Сохраните baseline с именованными полями. У каждого поля должны быть limit, tolerance и owner, а не только одна сумма в документации.
  5. Проверка candidate. Сначала прочитайте componentChecks, затем aggregate. В fixture scriptTicks = 34 больше allowed 30, даже когда total 93 меньше aggregate allowed 98.
  6. Действие. Откройте один input и одну границу, принадлежащую failed component. Для scriptTicks fixture возвращает inspect-script-input-and-one-route-boundary; не начинайте глобальную оптимизацию без дополнительного факта.

Как превратить модель в проверку без ложного PASS

В проекте порядок важнее выбора файла. Сначала profile route в контролируемых условиях: один вариант сценария, одна версия приложения, явно записанная конфигурация измерения. Затем сохранить snapshot вместе с conditions. Только после этого compare candidate. И уже после compare назначить focused action. Если начать с порога в CI, получится gate без смысла: он может зелёнеть, потому что route заменили, cache стал другим или в результаты попала другая часть опыта.

Что происходит при несовпадении условий

Fixture создаёт также candidate с изменённым условием cache. У него не появляется новый PASS или FAIL по компонентам: comparison останавливается как measurement-contract-invalid. Это сознательное поведение. Сравнение разных условий не становится корректным от того, что оба объекта содержат числа. Сначала восстановите один scenario и controlled conditions, затем снова снимите candidate. Так в отчёте остаётся отдельная категория «данных для вывода нет», а не случайный технический verdict.

Эта ветка особенно полезна для CI. У pipeline может быть хороший сигнал о том, что script завершился успешно, но этот факт не тождественен сопоставимому performance observation. Не нужно обесценивать CI: ему следует отдать узкое правило, которое он действительно проверяет. Например, сравнение serialized snapshot с candidate по одному маршруту. Но browser trace, real-user data и service-level решение остаются другими артефактами и требуют собственных условий.

Факт платформы и учебная модель

Исторический W3C Navigation Timing Level 2 от 17 января 2022 года описывает PerformanceNavigationTiming и timing information navigation. User Timing Level 3 от сентября 2021 года описывает именованные marks и measures. Performance Timeline Level 2 от августа 2021 года описывает хранение и получение performance entries. Это официальные смыслы платформенных интерфейсов, а не источник наших documentTicks и scriptTicks.

Модель пакета намеренно уже: она учит не считать скорость, а правильно оформлять сравнение. Ее output можно читать как «в этих условных входах named rule нарушен». Из него нельзя получить LCP, TTFB, браузерный waterfall, real-user result, количество пользователей или SLA. Для фактической интеграции сначала сопоставьте реальные поля с официальной документацией используемого инструмента и добавьте отдельный тест на mapping.

Критерий готовности и ограничение

Работа закончена не при первом зелёном total. Готовность для одного budget change выглядит так: route и scenario названы; conditions сохранены; baseline и candidate имеют один schema; каждый component имеет limit/tolerance; failed component даёт bounded diagnosis; следующий шаг ограничен одной границей. Затем выбранное изменение проверяется повторно в тех же условиях. Если conditions уже другие, это новая карточка сравнения, а не повод переписать baseline.

Fixture проверяет пятнадцать собственных assertions, включая baseline PASS, named failure candidate, явные tolerances, невозможность aggregate скрыть component, остановку при condition mismatch и отсутствие внешнего наблюдения. Это регрессия контракта текста и кода. Она не заменяет профиль настоящего route, потому что настоящий профиль остаётся отдельной инженерной работой с отдельной средой и доказательством.

Историческая граница февраля 2022

Статья ограничена документами, опубликованными не позднее января 2022 года. W3C sources ниже — рабочие drafts, а не современная рекомендация и не набор новых показателей. Здесь нет поздних метрик взаимодействия, нет текущих советов Lighthouse и нет заявлений об измеренном продукте. Это сохраняет честную дистанцию между исторической рамкой, официальным API и нашей учебной схемой бюджета.

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