function escapeHtml(value) { return String(value) .replaceAll('&', '&') .replaceAll('<', '<') .replaceAll('>', '>') .replaceAll('"', '"') .replaceAll("'", '''); } function paragraph(text) { return '
' + text + '
'; } function heading(text) { return '' + escapeHtml(Array.isArray(lines) ? lines.join('\n') : lines) + '';
}
function figure(src, alt, caption) {
return '/training/checkout/review и сценарий anonymous-cart-with-one-item. Название намеренно длиннее слова «главная»: при другом товаре, авторизации, locale или feature flag это уже может быть другой вход. Если один job смешивает такие варианты, изменение в одном из них способно спрятаться за средним числом другого.'),
paragraph('Дальше фиксируются условия. В учебной модели это version workload, единица teaching-ticks, а browser observation, CI observation, сеть, CPU, cache и trace имеют значение not-collected или not-modelled. Эти поля не делают проверку слабой. Они запрещают дорисовать доказательство задним числом: пока нет trace, нельзя из суммы ticks сделать вывод о браузере; пока нет CI run, нельзя назвать fixture результатом pipeline.'),
dataTable(
'Контракт учебного бюджета одного пользовательского пути',
['Элемент', 'Что хранит fixture', 'Для чего нужен', 'Чего не доказывает'],
[
['Route и scenario', '/training/checkout/review, одна корзина', 'не смешать разные входы', 'что путь уже прошёл в production'],
['Условия', 'version workload, ticks, not-collected', 'остановить сравнение при другой среде', 'свойства браузера или CI runner'],
['Компоненты', 'documentTicks, styleTicks, scriptTicks, renderTicks', 'увидеть вклад по частям', 'стандартные Web Vitals'],
['Допуск', 'limit плюс tolerance у каждого имени', 'сделать правило явным', 'универсальную норму для любого продукта'],
['Aggregate', 'вторичный total guard', 'увидеть суммарный запас', 'право скрыть failed component'],
],
),
heading('Разделите запас до того, как появится candidate'),
paragraph('Слово «бюджет» часто превращают в одну сумму. Для пользовательского пути это недостаточно. Один общий предел замечает только итог, а не перенос затрат между частями. В fixture у scriptTicks limit равен 28 и tolerance равен 2; значит допустимо до 30. У total limit 95 и tolerance 3. Эти числа условны. Их смысл не в удачном размере, а в том, что limit и допуск существуют отдельно и печатаются в результате проверки.'),
paragraph('Почему допуск не стоит прятать в комментарии? Потому что без него нельзя понять, какой сдвиг команда заранее согласилась игнорировать и в каких условиях. Если tolerance нужен из-за нестабильности настоящего замера, эта нестабильность должна быть описана в measurement contract. В fixture она не измеряется: поэтому допуск — проектное правило учебного объекта, не статистическая модель и не утверждение о дисперсии браузера.'),
figure(
'/assets/editorial/2022/performance-budget-allocation-2022.svg',
'Вертикальная схема учебного бюджета пути checkout review: четыре именованные части document, style, script и render имеют свои limit и tolerance. Baseline укладывается во все части, candidate превышает допустимый scriptTicks, хотя общий total остаётся в пределах вторичного порога.',
'Распределение показывает порядок проверки: сначала сравниваются именованные части с явным допуском, затем читается общий total. Зелёная сумма не отменяет красный component check.',
),
heading('Snapshot и candidate должны быть сопоставимы'),
paragraph('Snapshot — не «последний хороший отчёт». Это сохранённый объект с route, scenario, conditions и четырьмя учебными полями. Baseline в fixture содержит 24, 15, 26, 20; candidate меняет только scriptTicks на 34. Сумма candidate равна 93, поэтому aggregate остаётся PASS до allowed 98. Но component rule разрешает максимум 30, и общий результат обязан быть FAIL. Именно это защищает от красивой, но бесполезной зелёной суммы.'),
codeBlock(practiceExample),
paragraph('Код не вызывает Lighthouse, Chrome, PerformanceObserver или сетевой запрос. Он не получает timestamps. Его роль — зафиксировать invariant модели: named component сильнее aggregate. В рабочем проекте аналогичный object можно заполнить данными выбранного теста, но только после того, как команда назвала источник, версию инструмента, контролируемые условия и смысл каждого поля. Подмена этих границ «примерным CI» создаёт ту же ложную уверенность, от которой пытаемся уйти.'),
codeBlock(practiceGuardExample),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'Симптом. Общая проверка стабильно зелёная, но конкретный пользовательский путь постепенно меняется и становится труднее объяснить.',
'Причина. Aggregate объединяет части пути и не показывает, какая из них съела запас; baseline и candidate могут быть собраны при разных условиях.',
'Проверка контракта. Назовите route, сценарий, version workload, единицу, наличие или отсутствие browser/CI observation. Если conditions отличаются, не сравнивайте числа.',
'Проверка snapshot. Сохраните baseline с именованными полями. У каждого поля должны быть limit, tolerance и owner, а не только одна сумма в документации.',
'Проверка candidate. Сначала прочитайте componentChecks, затем aggregate. В fixture scriptTicks = 34 больше allowed 30, даже когда total 93 меньше aggregate allowed 98.',
'Действие. Откройте один input и одну границу, принадлежащую failed component. Для scriptTicks fixture возвращает inspect-script-input-and-one-route-boundary; не начинайте глобальную оптимизацию без дополнительного факта.',
]),
heading('Как превратить модель в проверку без ложного PASS'),
paragraph('В проекте порядок важнее выбора файла. Сначала profile route в контролируемых условиях: один вариант сценария, одна версия приложения, явно записанная конфигурация измерения. Затем сохранить snapshot вместе с conditions. Только после этого compare candidate. И уже после compare назначить focused action. Если начать с порога в CI, получится gate без смысла: он может зелёнеть, потому что route заменили, cache стал другим или в результаты попала другая часть опыта.'),
heading('Что происходит при несовпадении условий'),
paragraph('Fixture создаёт также candidate с изменённым условием cache. У него не появляется новый PASS или FAIL по компонентам: comparison останавливается как measurement-contract-invalid. Это сознательное поведение. Сравнение разных условий не становится корректным от того, что оба объекта содержат числа. Сначала восстановите один scenario и controlled conditions, затем снова снимите candidate. Так в отчёте остаётся отдельная категория «данных для вывода нет», а не случайный технический verdict.'),
paragraph('Эта ветка особенно полезна для CI. У pipeline может быть хороший сигнал о том, что script завершился успешно, но этот факт не тождественен сопоставимому performance observation. Не нужно обесценивать CI: ему следует отдать узкое правило, которое он действительно проверяет. Например, сравнение serialized snapshot с candidate по одному маршруту. Но browser trace, real-user data и service-level решение остаются другими артефактами и требуют собственных условий.'),
heading('Факт платформы и учебная модель'),
paragraph('Исторический 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.'),
paragraph('Модель пакета намеренно уже: она учит не считать скорость, а правильно оформлять сравнение. Ее output можно читать как «в этих условных входах named rule нарушен». Из него нельзя получить LCP, TTFB, браузерный waterfall, real-user result, количество пользователей или SLA. Для фактической интеграции сначала сопоставьте реальные поля с официальной документацией используемого инструмента и добавьте отдельный тест на mapping.'),
heading('Критерий готовности и ограничение'),
paragraph('Работа закончена не при первом зелёном total. Готовность для одного budget change выглядит так: route и scenario названы; conditions сохранены; baseline и candidate имеют один schema; каждый component имеет limit/tolerance; failed component даёт bounded diagnosis; следующий шаг ограничен одной границей. Затем выбранное изменение проверяется повторно в тех же условиях. Если conditions уже другие, это новая карточка сравнения, а не повод переписать baseline.'),
paragraph('Fixture проверяет пятнадцать собственных assertions, включая baseline PASS, named failure candidate, явные tolerances, невозможность aggregate скрыть component, остановку при condition mismatch и отсутствие внешнего наблюдения. Это регрессия контракта текста и кода. Она не заменяет профиль настоящего route, потому что настоящий профиль остаётся отдельной инженерной работой с отдельной средой и доказательством.'),
heading('Историческая граница февраля 2022'),
paragraph('Статья ограничена документами, опубликованными не позднее января 2022 года. W3C sources ниже — рабочие drafts, а не современная рекомендация и не набор новых показателей. Здесь нет поздних метрик взаимодействия, нет текущих советов Lighthouse и нет заявлений об измеренном продукте. Это сохраняет честную дистанцию между исторической рамкой, официальным API и нашей учебной схемой бюджета.'),
],
commonSources,
);
const mechanismArticle = createRevision(
{
slug: 'editorial-2022-02-mechanism-performance-budget',
title: 'Почему общий PASS не равен бюджету пути: модель сравнения и допуск',
categories: ['Производительность', 'Frontend'],
cover: '/assets/editorial/2022/performance-budget-trend-2022.svg',
excerpt: 'Причинная модель бюджета пути: от controlled conditions и snapshot к component checks, aggregate и ограниченной диагностике без подмены учебного расчёта browser trace.',
readingMinutes: 12,
},
[
paragraph('Проблема появляется, когда общий PASS считают доказательством неизменности пути. Путь может получить лишнюю работу в одной части и сохранить приемлемую общую сумму. Цена — неверный выбор действия: команда смотрит на score или total, не видит сдвиг в компоненте и начинает менять всё подряд. Через несколько таких проверок бюджет превращается в табличку, которая ничего не запрещает.'),
paragraph('Причина не в том, что aggregate бесполезен. Он отвечает на другой вопрос: не вышла ли общая учебная сумма за свой предел. Бюджет пути отвечает сначала на вопрос о каждой именованной части и только потом о сумме. Чтобы ответ был воспроизводимым, рядом нужны controlled conditions, baseline snapshot, candidate и явный tolerance. Ни одно из этих слов не означает реальный browser observation: fixture строит только локальные objects с условными ticks.'),
heading('Четыре слоя одной проверки'),
paragraph('Первый слой — measurement contract. Он связывает route, user scenario, version workload и условия. Второй — snapshot baseline, который описывает известный вход именно в этом contract. Третий — component checks: каждый field сравнивается со своим limit плюс tolerance. Четвёртый — decision. Overall FAIL появляется, если хотя бы один named component не проходит; aggregate остаётся вторичным guard. Это простое правило убирает самую частую лазейку: «total зелёный, значит можно не смотреть красную строку». '),
paragraph('В fixture четыре поля называются documentTicks, styleTicks, scriptTicks и renderTicks. Они не названы LCP, TTFB или browser event, потому что не получены из браузера. Это project vocabulary учебной модели. Если настоящий проект берёт browser field, его официальное значение, supported browser и сбор нужно описать отдельно. Нельзя взять знакомое имя метрики и приклеить его к произвольному object только ради убедительного отчёта.'),
dataTable(
'Слои budget comparison и границы вывода',
['Слой', 'Вход', 'Выход', 'Правило', 'Запрещённый вывод'],
[
['Contract', 'route, scenario, declared conditions', 'comparable / invalid', 'любое несовпадение останавливает compare', '«числа сами по себе сопоставимы»'],
['Snapshot', 'baseline timingFields', 'точка отсчёта', 'хранит тот же schema', '«последний отчёт всегда baseline»'],
['Component', 'candidate field и limit+tolerance', 'pass/fail по имени', 'каждый field виден отдельно', '«общая сумма лечит частный fail»'],
['Aggregate', 'сумма fields и total rule', 'вторичный pass/fail', 'читается после component', '«PASS total доказывает путь»'],
['Diagnosis', 'failed named component', 'one bounded next action', 'не расширяет scope без факта', '«найдена глобальная причина скорости»'],
],
),
heading('Допуск — часть контракта, а не скрытая скидка'),
paragraph('Если limit равен 28, а tolerance равен 2, то allowed равен 30. Такой расчёт должен быть виден в output, иначе reviewer не может отличить осознанный допуск от опечатки или смены правила. В fixture candidate имеет scriptTicks: 34. Он превышает allowed на четыре tick. Отдельное поле deltaFromLimit равно шести, потому что описывает расстояние до базового limit, а не до порога с допуском. Оба числа нужны, если заранее объяснить их смысл.'),
paragraph('Допуск нельзя переводить в процент без контекста. У одной части пути абсолютное изменение может быть осмысленным, у другой — нет. Настоящий способ выбрать tolerance зависит от источника наблюдения, повторов, среды и стоимости ложного FAIL. Учебная fixture не знает ни одной из этих вещей. Поэтому она не предлагает «правильные 2 ticks», а требует явного поля. Реальная команда добавляет обоснование рядом со своим mapping и не переносит учебные значения в конфигурацию молча.'),
heading('Почему линия aggregate может оставаться зелёной'),
paragraph('Baseline fixture суммируется до 85 ticks. Candidate суммируется до 93. Aggregate allowed равен 98, поэтому aggregate status — PASS. Однако рост находится в scriptTicks: baseline 26, candidate 34, allowed 30. Эта комбинация сделана специально. Если result позволил бы overall PASS, потому что 93 меньше 98, budget не защищал бы контракт пользователя: он прятал бы изменение в самой части, для которой владелец и limit были заведены.'),
figure(
'/assets/editorial/2022/performance-budget-trend-2022.svg',
'Сравнительная диаграмма baseline и candidate учебного маршрута: total растёт с 85 до 93 и остаётся ниже aggregate allowed 98, но scriptTicks растёт с 26 до 34 и превышает allowed 30. Итоговый verdict показан как fail по named component, а не как глобальная оценка скорости.',
'Линия aggregate нужна для контекста, а не для отмены component failure. На схеме значения подписаны как teaching ticks и отделены от браузерных метрик.',
),
heading('Механизм сравнения в одном объекте'),
codeBlock(mechanismExample),
paragraph('Функция compareRouteWorkload сначала вызывает validation для baseline и candidate. Она проверяет, что все четыре поля существуют, лишних нет, значения неотрицательные, route и scenario совпадают с contract, а conditions равны. Только после этого она строит component checks. Если conditions отличаются, component map пуст, aggregate становится not-comparable, а overall получает measurement-contract-invalid. Это предотвращает тихое смешение «до» и «после». '),
codeBlock(mechanismContractExample),
paragraph('Этот fragment не пытается «нормализовать» изменённый cache. Он оставляет compare недоступным, пока не восстановлен один declared contract. Так разница условий не превращается в число, которому дают performance-смысл.'),
heading('Сначала observation, потом архитектурный вывод'),
paragraph('M5-подход начинается с пользовательского сценария и заканчивается небольшим изменением, а не с тезиса «пакет тяжёлый». Candidate failure говорит только: в declared teaching model field scriptTicks больше allowed. Из этого нельзя вывести, что виновата конкретная библиотека, загрузчик, server response или пользовательское устройство. Чтобы назвать причину, нужно собрать следующий наблюдаемый артефакт на одной границе: dependency list, bundle diff, route trace или профиль — в зависимости от того, что реально доступно.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'Симптом. В отчёте есть общий PASS, но изменения одной части пути продолжают накапливаться без понятного владельца.',
'Причина. Aggregate использовали как final verdict, хотя он не хранит распределение и может пройти при failed component.',
'Проверка contract. Сверьте route, scenario и все declared conditions baseline/candidate. Изменился хотя бы один обязательный элемент — верните not-comparable.',
'Проверка allocation. Для каждого component покажите actual, limit, tolerance, allowed и owner. Отдельно посчитайте total, но не смешивайте его с verdict component.',
'Проверка decision. Overall PASS допустим только когда все names PASS. Overall FAIL обязан перечислить failed component, даже если aggregate остаётся зелёным.',
'Действие. Возьмите первую failed name и сузьте следующую проверку до одного input и одной route boundary. Зафиксируйте, какой дополнительный факт нужен до архитектурного изменения.',
]),
heading('Откуда взять настоящие поля и где не подменять их'),
paragraph('Navigation Timing Level 2 уже к январю 2022 года описывал navigation timing information и PerformanceNavigationTiming. User Timing Level 3 описывал named marks/measures и их metadata. Performance Timeline Level 2 описывал получение entries. Эти документы полезны, когда проект выбирает platform source. Они не предлагают готовый budget для checkout, не говорят, что total должен быть 95, и не определяют связь наших четырех ticks с полями API.'),
paragraph('Поэтому корректная интеграция начинается с mapping-table. В ней для каждого production field указывают источник, version, значение, условия доступности, преобразование и owner. Затем отдельно документируют runner и repeatability. Пока этого нет, честнее оставить language учебным: teaching-ticks, not-collected, not-modelled. Это не уклонение от работы; это защита от визуально похожих, но невалидных цифр.'),
heading('Граница CI и browser observation'),
paragraph('CI может исполнить compare и упасть по возвратному коду. Но факт исполнения job не создаёт browser observation. Browser observation может быть полезным входом в budget, если его получали по определённому протоколу и сохранили вместе с conditions. У fixture оба значения прямо равны not-collected. Так output не позволяет сказать «CI замерил маршрут» или «Chrome подтвердил результат». '),
heading('Критерий корректной эволюции бюджета'),
paragraph('Budget change корректен, если меняется одна известная вещь: новый route scenario, новый component, limit/tolerance или способ получения поля. В каждом случае обновляется contract и baseline, а причина изменения остаётся рядом. Неправильно просто повысить общий total, когда scriptTicks красный: это снимает симптом, но не говорит, согласен ли владелец пути на более дорогую часть. Иногда повышение оправдано, но оно должно быть отдельным решением со стоимостью и ограничением.'),
paragraph('В fixture есть пятнадцать assertions: baseline проходит каждый named budget; candidate падает по scriptTicks; aggregate PASS не скрывает failure; tolerances явны; conditions несовместимы блокируют compare; diagnosis ограничена одним component; никаких browser, CI, trace или network observations не выполняется. Это техническая проверка нашей модели. Она не измеряет реальную страницу и не должна использоваться как её заменитель.'),
heading('Историческая граница февраля 2022'),
paragraph('Все ссылки ниже существовали до февраля 2022 года: Navigation Timing draft от 17 января 2022 года, User Timing draft от сентября 2021-го и Performance Timeline draft от августа 2021-го. Они процитированы как historical sources и сами обозначены W3C как working drafts. В статье нет поздних interaction metrics и нет современных рекомендаций, появившихся после этой рамки. Технический вывод относится только к структуре учебного comparison.'),
],
commonSources,
);
const fieldArticle = createRevision(
{
slug: 'editorial-2022-02-field-performance-budget',
title: 'Красный component при зелёном total: как сузить диагностику бюджета пути',
categories: ['Производительность', 'Frontend'],
cover: '/assets/editorial/2022/performance-budget-diagnosis-2022.svg',
excerpt: 'Полевой маршрут для случая, когда aggregate остаётся PASS, а named component превышает допуск: проверить contract, сохранить факт и выбрать одно ограниченное действие без глобального performance verdict.',
readingMinutes: 11,
},
[
paragraph('На пользовательском пути есть неприятный случай: общий total остаётся зелёным, но одна именованная часть пересекает свой допуск. Его легко списать как «небольшое колебание», особенно если pipeline не упал. Цена такого решения — диагностический долг. Следующее изменение добавит ещё немного работы, а у команды останется только общий график и спор о том, когда именно путь начал меняться.'),
paragraph('Нужно не искать виноватого по одной строке, а правильно ограничить вывод. В учебной fixture candidate имеет total 93 при aggregate allowed 98, но scriptTicks равен 34 при allowed 30. Этот факт достаточен, чтобы заблокировать contract test, и недостаточен, чтобы объявить глобальную деградацию, проблему браузера, user metric или SLA. Хороший диагностический маршрут оставляет оба предложения рядом.'),
heading('Сначала отделите три разных состояния'),
paragraph('Первое состояние — comparable PASS: baseline и candidate имеют одинаковый contract, и все component checks проходят. Второе — comparable FAIL: contract тот же, но хотя бы один named component больше allowed. Третье — not comparable: scenario или conditions изменились, поэтому budget verdict не строится. Самая опасная ошибка — превратить третье состояние во второе. Тогда случайная смена cache, route data или config выглядит как performance regression, хотя данных для такого вывода нет.'),
paragraph('Fixture показывает все три ветки. Baseline сравнивается сам с собой и проходит. Candidate с теми же conditions падает по scriptTicks. Отдельный candidate меняет только cache с not-modelled на changed-training-cache-state; его comparison получает measurement-contract-invalid. В этой ветке нет component failure, потому что программа отказалась подменять различие условий числовым verdict.'),
dataTable(
'Три диагностических состояния budget comparison',
['Состояние', 'Что известно', 'Что не известно', 'Следующий шаг'],
[
['Comparable PASS', 'все names внутри allowed при одном contract', 'что путь быстрый для всех пользователей', 'сохранить snapshot и повторить только при том же contract'],
['Comparable FAIL', 'один named component превышает limit+tolerance', 'глобальная причина или browser effect', 'собрать один факт на границе failed component'],
['Not comparable', 'contract baseline/candidate различается', 'есть ли регрессия', 'восстановить scenario и conditions до нового compare'],
['Aggregate PASS + component FAIL', 'total в пределах, name не в пределах', 'что component можно игнорировать', 'считать overall FAIL и сузить действие'],
],
),
heading('Прочитайте output в правильном порядке'),
paragraph('Первой строкой читается validation: comparable ли объекты. Второй — список failedComponents. Третьей — diagnosis, где field и scope явно ограничены. Aggregate читается последним: он объясняет общий запас, но не принимает решение. Такой порядок помогает reviewer не споткнуться о знакомое зелёное слово PASS. В fixture aggregate находится в candidateComparison.aggregate, а final decision — в candidateComparison.overall.'),
codeBlock(fieldExample),
paragraph('Поле diagnosis.nextAction сознательно скупо: inspect-script-input-and-one-route-boundary. Оно не говорит «удалить библиотеку», «переписать приложение» или «ускорить сервер». По одному учебному числу нельзя выбрать такую архитектуру. Следующее действие должно породить проверяемый факт: например, список route inputs, diff зависимостей или отдельный профиль, если он действительно доступен и собран по понятному протоколу.'),
codeBlock(fieldRecheckExample),
heading('Диаграмма: зелёный total не отменяет красную ветку'),
figure(
'/assets/editorial/2022/performance-budget-diagnosis-2022.svg',
'Диагностическая схема для budget comparison: сначала проверяются route, scenario и declared conditions; при несовпадении сравнение останавливается. При совпадении component checks идут раньше aggregate. Ветка scriptTicks 34 при allowed 30 ведёт к focused action, хотя total 93 при allowed 98 остаётся зелёным.',
'Схема показывает границы вывода: red component создаёт контрактный fail и ограниченную проверку, но не доказывает глобальную скорость, trace браузера или пользовательский результат.',
),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'Симптом. Total budget зелёный, но на маршруте появляется новый красный component или его запас постепенно сокращается.',
'Причина. Итоговая сумма скрывает распределение. Либо baseline/candidate стали несопоставимыми, и цифры вообще нельзя читать вместе.',
'Проверка contract. Сверьте route, scenario, workloadVersion, unit и every declared condition. При любом различии получите measurement-contract-invalid, а не performance verdict.',
'Проверка component. Запишите actual, limit, tolerance и allowed. В этом case scriptTicks 34 против allowed 30; named fail остаётся виден независимо от total.',
'Проверка aggregate. Используйте 93 против allowed 98 только для контекста: total не вышел за вторичный guard, но он не может перевести overall в PASS.',
'Действие. Сохраните failed name и возьмите один input плюс одну boundary для следующего наблюдения. После изменения повторите тот же compare; если contract изменился, оформите новый snapshot.',
]),
heading('Как не сделать из диагностики глобальный performance verdict'),
paragraph('У diagnosis есть поле notClaim. Оно явно запрещает интерпретировать результат как global speed, browser trace, real-user result или SLA verdict. Такое ограничение кажется избыточным, пока в отчёте не появляется знакомое число. Именно тогда человек склонен достроить историю: «34 значит страница медленнее». Нет. В fixture 34 — значение одного условного property при локальном object comparison. Оно говорит ровно то, что проверено: rule именованной части не соблюдён.'),
paragraph('Нужен и обратный запрет. Aggregate PASS не даёт сказать «проблемы нет». Он лишь говорит, что учебный total не пересёк свой aggregate allowed. В реальном проекте этот total может быть полезен как guard против общего роста, но не как единственный gate. Если граница пользователя критична, ей нужен собственный named contract и owner. Если она не критична, тогда лучше не притворяться, что общий score защищает её.'),
heading('Выберите границу следующего наблюдения'),
paragraph('Для scriptTicks не стоит сразу открывать весь application. Начните с route boundary: entry, bundle boundary, dynamic input или другой реальный владелец, который выбран в mapping. Одного артефакта достаточно, чтобы сузить или отвергнуть гипотезу. Если видно, что changed input не относится к route, вернитесь к contract. Если input относится к route, тогда можно подготовить небольшое обратимое изменение и повторить compare. Оба пути лучше, чем широкая «оптимизация производительности». '),
paragraph('Подход сохраняет развитие автора 2022 года: сначала пользовательский опыт и наблюдаемый контракт, затем система границ, затем решение с ценой. Он не делает вид, что автор владеет production telemetry или знает реальную причину без trace. Любой более сильный вывод требует нового доказательства. Это не осторожность ради тона — это способ не закрепить неверную архитектуру после одной зелёной или красной строки.'),
heading('Источники помогают не перепутать поля'),
paragraph('W3C Navigation Timing Level 2 в историческом snapshot описывает platform interface для navigation data. User Timing Level 3 описывает marks/measures и metadata. Performance Timeline Level 2 описывает retrieval performance entries и observer mechanics. В каждом случае источник помогает понять, что значит фактическое поле платформы. Но он не говорит, что scriptTicks fixture равно одному из этих полей или что эти поля уже собраны в этом проекте.'),
paragraph('Перед переносом модели на настоящую страницу полезна маленькая таблица mapping: field учебного контракта, concrete source, data availability, transform, owner и known limitation. Если для одного поля mapping отсутствует, не подставляйте похожий термин. Либо оставьте field учебным, либо сначала соберите новый evidence. Такая дисциплина защищает не только от анахронизма, но и от обычной ошибки: неправильная метрика даёт очень точное число и очень плохое решение.'),
heading('Сигнал для CI и отдельный сигнал для исследования'),
paragraph('CI gate может работать как контрактный предохранитель: сравнить known snapshot и candidate, вывести failed components и завершиться с ошибкой. Но не стоит называть этот outcome «измерением браузерной производительности», если job не выполнял такое наблюдение. В fixture CI observation честно указано как not-collected. Так автоматизация остаётся полезной, но не получает чужой смысл.'),
paragraph('Исследование начинается после fail, а не до него. Для него нужны реальные артефакты, которые отсутствуют в fixture: выбранный browser profile, trace, resource list, application markers или другой источник. Их нельзя выводить из teaching ticks. Напротив, contract budget делает исследование дешевле: он уже назвал route, scenario, failed component, owner boundary и запретил смешать условия. Значит следующий сбор материала может быть меньше и точнее.'),
heading('Что проверить после focused action'),
paragraph('После небольшого изменения не повышайте limit первым движением. Сначала повторите same contract compare. Если scriptTicks вернулся в allowed, зафиксируйте, какой input изменился и что всё ещё не известно. Если не вернулся, следующий шаг должен быть ещё одним проверяемым узким экспериментом, а не каскадом правок. Если route or scenario изменились, не сравнивайте с исходным baseline: оформите новую версию workload и причину пересмотра.'),
heading('Историческая граница февраля 2022'),
paragraph('Материал не использует будущие показатели взаимодействия и не ссылается на актуальные страницы, которые могли поменять советы. Ссылки ниже ведут на dated W3C drafts: январь 2022 для Navigation Timing и 2021 для User Timing и Performance Timeline. Все три документа различают официальные platform interfaces и этап working draft. Учебная диагностика поверх них остаётся собственной моделью пакета.'),
],
commonSources,
);
export const revisions = [practiceArticle, mechanismArticle, fieldArticle].map(({ proseLength, ...revision }) => revision);
export const february2022Revisions = revisions;
function verifyFixture() {
const fixture = runPerformanceBudgetFixture();
const failed = Object.entries(fixture.assertions)
.filter(([, passed]) => passed !== true)
.map(([name]) => name);
if (failed.length > 0) {
console.error('FAIL fixture: ' + failed.join(', '));
process.exitCode = 1;
return;
}
console.log('PASS fixture: ' + Object.keys(fixture.assertions).length + '/' + Object.keys(fixture.assertions).length + ' assertions');
}
if (process.argv.includes('--verify-fixture')) {
verifyFixture();
}
if (process.argv.includes('--print-revisions')) {
console.log(JSON.stringify(revisions));
}