revise March 2022 browser rendering articles
Build and deploy / deploy (push) Successful in 15s

This commit is contained in:
2026-07-31 13:39:49 +03:00
parent a4b475a308
commit f6afaff958
7 changed files with 752 additions and 1 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# Производство редакционных партий # Производство редакционных партий
На 31 июля 2026 года строгий аудит проходит 148 из 358 созданных материалов. Остальные 210 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить. На 31 июля 2026 года строгий аудит проходит 151 из 358 созданных материалов. Остальные 207 не считаются «почти готовыми»: их нужно заменить, а не косметически удлинить.
## Одна партия ## Одна партия
+188
View File
@@ -0,0 +1,188 @@
# П49 — browser rendering, март 2022: self-review sidecar-пакета
## Граница пакета
Созданы только пять новых файлов, без изменения registry, README, `articles.json`,
редакционного стандарта, очереди, Git и файлов других пакетов:
1. `web/scripts/upgrade-2022-03.mjs`;
2. `web/public/assets/editorial/2022/browser-rendering-timeline-2022.svg`;
3. `web/public/assets/editorial/2022/browser-rendering-pipeline-2022.svg`;
4. `web/public/assets/editorial/2022/browser-rendering-diagnosis-rollback-2022.svg`;
5. этот review.
Скрипт экспортирует только `revisions`. В каждом overlay-объекте есть только
поля публикации; `date` и `author` отсутствуют. `--print-revisions` печатает
тот же import-safe массив, а `--verify-fixture` запускает локальную модель.
## Историческая рамка и источники
Материал ограничен мартом 2022 года. Проверены три первичных или официальных
источника с датированным либо неизменяемым URL:
- [WHATWG HTML, immutable commit 9d42d2c от 27 февраля 2022 года](https://github.com/whatwg/html/commit/9d42d2ca8523758893cdc8d7de6e0815e4178b81);
- [W3C Timing control for script-based animations, Working Group Note от 22 сентября 2015 года](https://www.w3.org/TR/2015/NOTE-animation-timing-20150922/);
- [W3C Performance Timeline Level 2, Working Draft от 19 августа 2021 года](https://www.w3.org/TR/2021/WD-performance-timeline-2-20210819/).
HTTP preflight всех трёх URL завершился `200`. Из источников взяты только
узкие факты: rendering обновляется user agent, `requestAnimationFrame`
запрашивает планирование animation frame update, а Performance Timeline
описывает работу с performance entries. Они не устанавливают единственный
browser pipeline, не задают labels fixture и не дают числовой бюджет. В статьях
нет INP, поздних интерфейсных возможностей DevTools, современных рекомендаций
по Web Vitals или утверждений о production-метриках.
## Проход 1 — факты и техника
Проверял: границу между документированным поведением, реальной записью
Performance panel и учебной моделью.
`planRenderFrame` и `runBrowserRenderingFixture` используют только локальные
object/array. В них нет DOM, browser API, `requestAnimationFrame`, сети,
таймера, файлов, CPU profile, trace или Web Vitals. Значения `units` всегда
названы `synthetic-work-units`; это не milliseconds, duration, FPS, CPU или
результат настоящего кадра.
Fixture формирует один валидный DOM contract:
```text
save-card.className: card is-pending -> card is-ready
```
Для changed route она создаёт фиксированный порядок `js-mutation → style →
layout → paint → composite`, total `11 synthetic units` и условную
`jank-boundary` при limit `10`. Это не диагноз jank пользователя, а
детерминированная тестовая ветка. Error route отвергает неизвестное property
или неверный contract; no-op route не создаёт frame; rollback возвращает
исходный DOM отдельным frame.
Выполнено:
```text
node --check web/scripts/upgrade-2022-03.mjs
node web/scripts/upgrade-2022-03.mjs --verify-fixture
```
Результат: **16/16 assertions**.
Assertions покрывают: валидный target/property/from/to, новый DOM result,
сохранение text, stable frame id, точный порядок всех стадий, названные причины
стадий, явную synthetic unit, детерминированную сумму, условную boundary без
претензии на duration, no-op без frame/mutation, error без mutation,
rollback к base DOM и отдельный rollback frame с тем же порядком стадий.
Все code snippets соответствуют реальным полям и режимам script. Пример с
rollback прямо объясняет, что это не browser undo, а проектный проверяемый
контракт. Пример diagnosis не выдаёт labels модели за event names Performance
panel.
## Проход 2 — редактура, голос и объём
Проверял: М5 начала 2022 года — системный практик, который связывает
пользовательское действие, границу системы и проверяемый следующий шаг, но не
приписывает себе инцидент, платформенный масштаб или неизмеренный эффект.
| Slug | Основной текст, знаки | Роль | Каркас | Вердикт |
| --- | ---: | --- | --- | --- |
| `editorial-2022-03-practice-browser-rendering` | 7 655 | Практический маршрут | проблема/цена, таблица, fixture, timeline, маршрут, ограничения | Принято |
| `editorial-2022-03-mechanism-browser-rendering` | 9 514 | Причинная модель | проблема/цена, таблица, pipeline, error/no-op, rollback | Принято |
| `editorial-2022-03-field-browser-rendering` | 9 313 | Полевой разбор | проблема/цена, полевой лист, diagnosis, rollback, следующий шаг | Принято |
Во всех статьях проблема и цена находятся в первых двух абзацах. Техническая
речь выдерживает маршрут «симптом → причина → проверка → действие»; общие
фразы заменены на target, property, before/after, owner, range и rollback.
Тексты не утверждают «CSS всегда дорогой», «class всегда вызывает layout» или
«страница стала быстрее». Термины `style`, `layout`, `paint` и `composite`
после первого появления ограничены учебными labels и вопросом к настоящей
записи. Анахронизмы после марта 2022 года удалены.
Выполнено:
```text
npm --prefix web run audit:draft -- scripts/upgrade-2022-03.mjs
```
Результат: все три статьи прошли диапазон 5 000–15 000, проблему в начале,
таблицу, figure с alt/подписью, код, ordered route, минимум два источника и
проверку import-safe overlay.
## Проход 3 — визуал и выпуск sidecar
Проверял: самостоятельность трёх иллюстраций, читаемость на ширине 375 px,
SVG safety и границы выпуска sidecar.
- `browser-rendering-timeline-2022.svg` отвечает на вопрос «какой порядок и
где условная boundary?»; в нём labels и units не маскируются под duration.
- `browser-rendering-pipeline-2022.svg` отвечает на вопрос «какой вопрос
относится к каждой стадии?»; пунктирная рамка отделяет модель от event
names браузера.
- `browser-rendering-diagnosis-rollback-2022.svg` отвечает на вопрос «когда
остановиться и как проверить обратимость?»; он различает error, no-op,
boundary и rollback.
Для всех SVG выполнены:
```text
xmllint --noout <each-svg>
rg -n '<script|<foreignObject|(?:href|src)="https?://|data:image' <each-svg>
```
XML проверка прошла. Safety scan не нашёл script, foreignObject, внешних URL
или `data:image`. В каждом SVG есть `title`, `desc`, `role="img"`; в статьях
есть отдельные развёрнутые `alt` и `figcaption`. Рендеры Sharp шириной 375 px
просмотрены: крупные заголовки, связи и предупреждения читаются, текст не
обрезан, а три схемы не повторяют один вопрос.
Production build, registry integration, изменение базы статей, публикация и
push намеренно не выполнялись: они лежат за границей автономного пакета.
## Вердикт
Sidecar-пакет П49 готов к независимому редакторскому приёму. Перед
интеграцией нужен отдельный проход: повторно проверить исторические ссылки,
добавить импорт и spread в registry, обновить счётчик архива, запустить
targeted audit и production build. Эти файлы намеренно не изменены здесь.
## Независимый редакторский приём
### Проход 1 — источники и модель
До интеграции выявлена фактическая ошибка: исходный URL объявлял документ
W3C Recommendation от 6 октября 2015 года, но отвечал `404`. Он удалён из
текста, кода и review. Вместо него поставлена проверяемая
[W3C Working Group Note от 22 сентября 2015 года](https://www.w3.org/TR/2015/NOTE-animation-timing-20150922/).
Её статус и дата названы без повышения до Recommendation. Повторный HTTP
preflight этого URL, immutable WHATWG commit и датированного Performance
Timeline draft завершился `200`.
Затем вручную просмотрена локальная fixture. `planRenderFrame` принимает
только declared DOM-contract, не обращается к DOM или browser API и маркирует
все five stages как `synthetic-work-units`. Граница `10` не названа временем
кадра или dropped-frame наблюдением. CLI подтвердил 16 из 16 assertions;
import вернул ровно три revision-объекта без `date` и `author`.
### Проход 2 — текст и полнота
Перепроверены первые абзацы, связи «симптом → причина → проверка → действие»,
rollback и исторические ограничения. Убрана смешанная фраза `later change`:
она заменена на «следующая правка». Строгий целевой аудит после интеграции
подтвердил 7 655 / 9 514 / 9 313 знаков, по одной figure и таблице в каждой
статье, а также 1 / 2 / 2 исполнимых code examples соответственно. Ни один
материал не выдаёт учебные labels за события Performance panel либо за
результат реального trace.
### Проход 3 — визуал и выпуск
Все три SVG повторно прошли `xmllint`; safety scan не нашёл script,
`foreignObject`, внешних URL или `data:image`. Рендеры Sharp шириной 375 px
просмотрены отдельно: timeline объясняет условную границу, pipeline отделяет
вопросы модели от имён browser events, diagnosis показывает остановки и
rollback. После добавления мартовского import/spread registry содержит 142
ревизии. `npm run build` завершён успешно: сгенерировано 374 статические
страницы.
## Финальный вердикт
П49 принята и интегрирована после трёх независимых проходов. В публикацию
попадают только пять файлов партии и два точечных файла интеграции; материалы
следующих месяцев и несвязанные файлы рабочего дерева не включаются.
+2
View File
@@ -45,6 +45,7 @@ import { revisions as november2021Revisions } from '../scripts/upgrade-2021-11.m
import { revisions as december2021Revisions } from '../scripts/upgrade-2021-12.mjs'; import { revisions as december2021Revisions } from '../scripts/upgrade-2021-12.mjs';
import { revisions as january2022Revisions } from '../scripts/upgrade-2022-01.mjs'; import { revisions as january2022Revisions } from '../scripts/upgrade-2022-01.mjs';
import { revisions as february2022Revisions } from '../scripts/upgrade-2022-02.mjs'; import { revisions as february2022Revisions } from '../scripts/upgrade-2022-02.mjs';
import { revisions as march2022Revisions } from '../scripts/upgrade-2022-03.mjs';
// This layer replaces archived source entries without losing their stable slug and date. // This layer replaces archived source entries without losing their stable slug and date.
export const editorialRevisions = [ export const editorialRevisions = [
@@ -95,4 +96,5 @@ export const editorialRevisions = [
...december2021Revisions, ...december2021Revisions,
...january2022Revisions, ...january2022Revisions,
...february2022Revisions, ...february2022Revisions,
...march2022Revisions,
]; ];
@@ -0,0 +1,25 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 720" role="img" aria-labelledby="title desc">
<title id="title">Диагностика DOM-изменения с остановками и rollback</title>
<desc id="desc">Блок-схема проверяет DOM result is-pending в is-ready. Если контракт неверен или изменение no-op, расследование останавливается. Валидный путь выбирает одну interaction и вопросы к pipeline. Пересечение учебной границы ведёт к одной обратимой гипотезе и rollback, который возвращает is-pending отдельным кадром.</desc>
<defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0 0L10 5L0 10Z" fill="#7ed4ff"/></marker><marker id="warn" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0 0L10 5L0 10Z" fill="#ff867c"/></marker></defs>
<rect width="1200" height="720" fill="#111827"/>
<text x="56" y="66" fill="#f8fafc" font-family="Arial, sans-serif" font-size="36" font-weight="700">Диагностика: остановить ложный вывод раньше правки</text>
<g font-family="Arial, sans-serif" font-size="22">
<rect x="70" y="122" width="260" height="92" rx="15" fill="#173b57" stroke="#76c8ff" stroke-width="2"/><text x="96" y="162" fill="#e6f6ff" font-weight="700">DOM contract</text><text x="96" y="194" fill="#c7ebff">pending → ready</text>
<path d="M330 168H408" stroke="#7ed4ff" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="416" y="122" width="270" height="92" rx="15" fill="#1a504a" stroke="#72d7b5" stroke-width="2"/><text x="446" y="162" fill="#eafff7" font-weight="700">result совпал?</text><text x="446" y="194" fill="#c5f4df">target / property / after</text>
<path d="M686 168H762" stroke="#7ed4ff" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="770" y="122" width="350" height="92" rx="15" fill="#243b63" stroke="#9fc3ff" stroke-width="2"/><text x="796" y="162" fill="#edf4ff" font-weight="700">одна interaction</text><text x="796" y="194" fill="#cedfff">выделить узкий range</text>
<path d="M550 214V286" stroke="#ff867c" stroke-width="4" marker-end="url(#warn)"/>
<rect x="366" y="294" width="366" height="80" rx="14" fill="#5f3337" stroke="#ff867c" stroke-width="2"/><text x="394" y="343" fill="#ffe4e1" font-weight="700">нет → stop: invalid contract</text>
<path d="M945 214V286" stroke="#7ed4ff" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="770" y="294" width="350" height="94" rx="14" fill="#534525" stroke="#ffd36a" stroke-width="2"/><text x="798" y="334" fill="#fff4cf" font-weight="700">mutation → style → layout</text><text x="798" y="365" fill="#fff4cf">→ paint → composite</text>
<path d="M945 388V450" stroke="#7ed4ff" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="770" y="458" width="350" height="86" rx="14" fill="#482f62" stroke="#d2abff" stroke-width="2"/><text x="796" y="494" fill="#f6edff" font-weight="700">boundary crossed?</text><text x="796" y="524" fill="#dcc5ff">11 synthetic units &gt; 10</text>
<path d="M770 500H684" stroke="#ff867c" stroke-width="4" marker-end="url(#warn)"/>
<rect x="330" y="456" width="344" height="88" rx="14" fill="#5f3337" stroke="#ff867c" stroke-width="2"/><text x="357" y="492" fill="#ffe4e1" font-weight="700">no-op → stop</text><text x="357" y="522" fill="#ffe4e1">нет нового DOM result</text>
<path d="M945 544V594" stroke="#7ed4ff" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="770" y="602" width="350" height="68" rx="14" fill="#195047" stroke="#72d7b5" stroke-width="2"/><text x="794" y="645" fill="#eafff7" font-weight="700">один owner + rollback → pending</text>
</g>
<text x="70" y="692" fill="#b8c8e0" font-family="Arial, sans-serif" font-size="18">Все units synthetic. Схема не является trace, benchmark, milliseconds или доказательством jank пользователя.</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 720" role="img" aria-labelledby="title desc">
<title id="title">Учебная pipeline от DOM contract к вопросам для записи</title>
<desc id="desc">Пять последовательных блоков JS mutation, style, layout, paint и composite. Под каждым блоком указан вопрос к выбранному диапазону записи. Пунктирная рамка отмечает, что это модель, а не обязательные имена событий браузера.</desc>
<defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0 0L10 5L0 10Z" fill="#68d6b0"/></marker></defs>
<rect width="1200" height="720" fill="#0b1722"/>
<text x="60" y="70" fill="#f3fbff" font-family="Arial, sans-serif" font-size="36" font-weight="700">Не «виновник», а последовательность вопросов</text>
<text x="60" y="108" fill="#b8d6e8" font-family="Arial, sans-serif" font-size="22">DOM contract сначала, trace range потом, изменение — только после гипотезы</text>
<rect x="44" y="142" width="1112" height="410" rx="22" fill="none" stroke="#4d7593" stroke-width="3" stroke-dasharray="12 10"/>
<text x="72" y="180" fill="#8ed6ff" font-family="Arial, sans-serif" font-size="20">LABELS УЧЕБНОЙ МОДЕЛИ, НЕ ОБЯЗАТЕЛЬНЫЕ EVENT NAMES</text>
<g font-family="Arial, sans-serif">
<rect x="70" y="230" width="184" height="218" rx="16" fill="#173d57"/><text x="91" y="276" fill="#f4fcff" font-size="24" font-weight="700">JS mutation</text><text x="91" y="320" fill="#b9eaff" font-size="19">Что записал</text><text x="91" y="347" fill="#b9eaff" font-size="19">handler?</text><text x="91" y="408" fill="#85d5ff" font-size="20">className X → Y</text>
<rect x="282" y="230" width="184" height="218" rx="16" fill="#195047"/><text x="333" y="276" fill="#effff9" font-size="24" font-weight="700">style</text><text x="303" y="320" fill="#bff4de" font-size="19">Какой selector</text><text x="303" y="347" fill="#bff4de" font-size="19">state изменён?</text><text x="322" y="408" fill="#8de0bb" font-size="20">вопрос к range</text>
<rect x="494" y="230" width="184" height="218" rx="16" fill="#5d4b27"/><text x="543" y="276" fill="#fff9e2" font-size="24" font-weight="700">layout</text><text x="515" y="320" fill="#ffeba5" font-size="19">Какая geometry</text><text x="515" y="347" fill="#ffeba5" font-size="19">стала нужна?</text><text x="534" y="408" fill="#ffd56c" font-size="20">не verdict</text>
<rect x="706" y="230" width="184" height="218" rx="16" fill="#653a36"/><text x="762" y="276" fill="#fff1ed" font-size="24" font-weight="700">paint</text><text x="727" y="320" fill="#ffc8bd" font-size="19">Какой visual</text><text x="727" y="347" fill="#ffc8bd" font-size="19">area обновился?</text><text x="746" y="408" fill="#ffab9b" font-size="20">проверить факт</text>
<rect x="918" y="230" width="184" height="218" rx="16" fill="#44376b"/><text x="937" y="276" fill="#f8f2ff" font-size="24" font-weight="700">composite</text><text x="939" y="320" fill="#ddcffd" font-size="19">Какая финальная</text><text x="939" y="347" fill="#ddcffd" font-size="19">работа видна?</text><text x="956" y="408" fill="#cbb9ff" font-size="20">не «дёшево»</text>
</g>
<path d="M254 339H274M466 339H486M678 339H698M890 339H910" stroke="#68d6b0" stroke-width="4" marker-end="url(#arrow)"/>
<rect x="70" y="594" width="1030" height="70" rx="14" fill="#122b38"/>
<text x="98" y="638" fill="#d4f6e9" font-family="Arial, sans-serif" font-size="24">Факт записи + DOM result → один owner → обратимое действие → повтор того же сценария</text>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

@@ -0,0 +1,29 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 720" role="img" aria-labelledby="title desc">
<title id="title">Учебная timeline одного DOM-изменения и граница условной работы</title>
<desc id="desc">Нажатие Сохранить меняет className карточки. Далее пять блоков модели: JS mutation, style, layout, paint, composite. Их условные единицы равны 1, 2, 4, 3 и 1, сумма 11 пересекает учебную границу 10. Подпись поясняет, что это не миллисекунды и не trace браузера.</desc>
<rect width="1200" height="720" fill="#0d1628"/>
<text x="64" y="72" fill="#f8fafc" font-family="Arial, sans-serif" font-size="36" font-weight="700">Один DOM-result → один учебный кадр</text>
<text x="64" y="110" fill="#b8c8e0" font-family="Arial, sans-serif" font-size="22">Контракт: save-card.className card is-pending → card is-ready</text>
<rect x="64" y="158" width="286" height="100" rx="14" fill="#183454" stroke="#55c2ff" stroke-width="2"/>
<text x="88" y="200" fill="#d9f2ff" font-family="Arial, sans-serif" font-size="23" font-weight="700">input</text>
<text x="88" y="232" fill="#f8fafc" font-family="Arial, sans-serif" font-size="20">click «Сохранить»</text>
<path d="M370 208H420" stroke="#55c2ff" stroke-width="4" marker-end="url(#arrow)"/>
<defs><marker id="arrow" markerWidth="10" markerHeight="10" refX="8" refY="5" orient="auto"><path d="M0 0L10 5L0 10Z" fill="#55c2ff"/></marker></defs>
<g font-family="Arial, sans-serif">
<rect x="424" y="158" width="142" height="100" rx="12" fill="#1d4960"/><text x="442" y="198" fill="#f8fafc" font-size="20" font-weight="700">JS mutation</text><text x="462" y="230" fill="#a8e6ff" font-size="22">1 unit</text>
<rect x="584" y="158" width="122" height="100" rx="12" fill="#24584f"/><text x="608" y="198" fill="#f8fafc" font-size="20" font-weight="700">style</text><text x="602" y="230" fill="#b8f3d3" font-size="22">2 units</text>
<rect x="724" y="158" width="132" height="100" rx="12" fill="#63522b"/><text x="744" y="198" fill="#fff6cf" font-size="20" font-weight="700">layout</text><text x="742" y="230" fill="#ffe89b" font-size="22">4 units</text>
<rect x="874" y="158" width="128" height="100" rx="12" fill="#6a3f39"/><text x="896" y="198" fill="#fff1ed" font-size="20" font-weight="700">paint</text><text x="892" y="230" fill="#ffb8ab" font-size="22">3 units</text>
<rect x="1020" y="158" width="132" height="100" rx="12" fill="#473b73"/><text x="1034" y="198" fill="#f4efff" font-size="20" font-weight="700">composite</text><text x="1040" y="230" fill="#d9ccff" font-size="22">1 unit</text>
</g>
<path d="M566 208H578M706 208H718M856 208H868M1002 208H1014" stroke="#8fa9c8" stroke-width="3" marker-end="url(#arrow)"/>
<rect x="64" y="324" width="1070" height="170" rx="16" fill="#14233c" stroke="#2f527e" stroke-width="2"/>
<text x="96" y="376" fill="#f8fafc" font-family="Arial, sans-serif" font-size="28" font-weight="700">Сумма: 11 synthetic work units</text>
<line x1="98" y1="428" x2="1040" y2="428" stroke="#6f8cab" stroke-width="12" stroke-linecap="round"/>
<line x1="98" y1="428" x2="955" y2="428" stroke="#f2ad4d" stroke-width="12" stroke-linecap="round"/>
<line x1="877" y1="394" x2="877" y2="462" stroke="#ff6b6b" stroke-width="5"/>
<text x="834" y="486" fill="#ffb4b4" font-family="Arial, sans-serif" font-size="20">boundary 10</text>
<text x="980" y="414" fill="#fff0d7" font-family="Arial, sans-serif" font-size="22" font-weight="700">11</text>
<text x="96" y="550" fill="#ffcf87" font-family="Arial, sans-serif" font-size="25" font-weight="700">jank-boundary — только label учебной модели</text>
<text x="64" y="648" fill="#b8c8e0" font-family="Arial, sans-serif" font-size="22">Не milliseconds · не CPU · не FPS · не Web Vitals · не Performance trace</text>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

+487
View File
@@ -0,0 +1,487 @@
function escapeHtml(value) {
return String(value)
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#039;');
}
function paragraph(text) {
return '<p>' + text + '</p>';
}
function heading(text) {
return '<h2>' + text + '</h2>';
}
function codeBlock(lines) {
return '<pre><code>' + escapeHtml(Array.isArray(lines) ? lines.join('\n') : lines) + '</code></pre>';
}
function figure(src, alt, caption) {
return '<figure><img src="' + src + '" alt="' + alt + '" loading="lazy" /><figcaption>' + caption + '</figcaption></figure>';
}
function orderedList(items) {
return '<ol>' + items.map((item) => '<li>' + item + '</li>').join('') + '</ol>';
}
function dataTable(caption, headers, rows) {
const head = '<thead><tr>' + headers.map((header) => '<th scope="col">' + header + '</th>').join('') + '</tr></thead>';
const body = '<tbody>' + rows.map((row) => '<tr>' + row.map((cell) => '<td>' + cell + '</td>').join('') + '</tr>').join('') + '</tbody>';
return '<div class="table-scroll"><table><caption>' + caption + '</caption>' + head + body + '</table></div>';
}
function sourceList(items) {
return '<ul>' + items.map((item) => '<li><a href="' + item.url + '" target="_blank" rel="noopener noreferrer">' + item.title + '</a> — ' + item.note + '</li>').join('') + '</ul>';
}
function plainText(content) {
return content
.replace(/<[^>]+>/g, ' ')
.replaceAll('&nbsp;', ' ')
.replaceAll('&quot;', '"')
.replaceAll('&#039;', "'")
.replaceAll('&lt;', '<')
.replaceAll('&gt;', '>')
.replaceAll('&amp;', '&')
.replace(/\s+/g, ' ')
.trim();
}
function bodyText(content) {
return plainText(content.replace(/<h2>Проверяемые источники<\/h2>[\s\S]*?(?=<h2>|$)/, ''));
}
function createRevision(meta, bodyParts, sources) {
if (sources.length < 2) {
throw new Error(meta.slug + ': нужно минимум два первичных или официальных источника');
}
const contentHtml = bodyParts.join('\n') + '\n' + heading('Проверяемые источники') + '\n' + sourceList(sources);
const proseLength = bodyText(contentHtml).length;
if (proseLength < 5000 || proseLength > 15000) {
throw new Error(meta.slug + ': основной текст вне 5 000–15 000 знаков: ' + proseLength);
}
return { ...meta, contentHtml, proseLength };
}
const htmlLivingStandardSnapshot = {
title: 'WHATWG HTML: immutable snapshot 2022-03-01',
url: 'https://github.com/whatwg/html/commit/9d42d2ca8523758893cdc8d7de6e0815e4178b81',
note: 'первичный снимок HTML Standard от 27 февраля 2022 года. Он задаёт алгоритм update the rendering и подчёркивает, что шаги зависят от user agent; он не устанавливает числовой бюджет и не обещает одинаковую последовательность внутренних событий во всех браузерах.',
};
const animationTimingNote = {
title: 'W3C Timing control for script-based animations, Working Group Note от 22 сентября 2015 года',
url: 'https://www.w3.org/TR/2015/NOTE-animation-timing-20150922/',
note: 'датированная W3C Working Group Note от 22 сентября 2015 года, доступная к марту 2022-го. Она описывает requestAnimationFrame как запрос к user agent запланировать animation frame update; это не Recommendation. Fixture не реализует этот API и не измеряет частоту кадров.',
};
const performanceTimelineDraft = {
title: 'W3C Performance Timeline Level 2, Working Draft от 19 августа 2021 года',
url: 'https://www.w3.org/TR/2021/WD-performance-timeline-2-20210819/',
note: 'датированный первичный W3C draft о performance entries и их получении. Он не описывает labels fixture, не задаёт teaching boundary и не превращает synthetic units в настоящий trace или данные Performance panel.',
};
const commonSources = [htmlLivingStandardSnapshot, animationTimingNote, performanceTimelineDraft];
const fixtureCommand = [
'# Проверяется локальная учебная модель, а не страница в Chrome.',
'node web/scripts/upgrade-2022-03.mjs --verify-fixture',
'',
'# PASS fixture: 16/16 assertions',
'# В output: changed, jankBoundary, noOp, error и rollback.',
].join('\n');
const routeExample = [
'// Fixture хранит этот контракт внутри script; пример показывает его вход.',
'const update = {',
' target: "save-card",',
' property: "className",',
' from: "card is-pending",',
' to: "card is-ready",',
'};',
'',
'// changed route: js-mutation → style → layout → paint → composite',
'// units: 1 + 2 + 4 + 3 + 1 = 11 synthetic units',
'// boundary 10 ⇒ teaching label jank-boundary, not milliseconds.',
'',
'node web/scripts/upgrade-2022-03.mjs --verify-fixture',
].join('\n');
const rollbackExample = [
'// В модели обратимое действие — отдельный контракт, не Ctrl+Z браузера.',
'const apply = { property: "className", from: "card is-pending", to: "card is-ready" };',
'const rollback = { property: "className", from: "card is-ready", to: "card is-pending" };',
'',
'// После rollback result.dom.className снова "card is-pending".',
'// Отмена проходит тот же порядок стадий и получает свой frameId.',
'// Никаких DOM API и actual Performance recording этот пример не создаёт.',
].join('\n');
const diagnosticExample = [
'// Сначала читается изменение, затем его одна frame-запись.',
'const question = {',
' mutation: "className: card is-pending → card is-ready",',
' frameId: "teaching-frame-07",',
' inspectOrder: ["js-mutation", "style", "layout", "paint", "composite"],',
'};',
'',
'// В реальной записи ищут соответствующий interaction и события этой записи.',
'// Эти labels не являются именами событий Performance panel.',
].join('\n');
const frameOrder = Object.freeze(['js-mutation', 'style', 'layout', 'paint', 'composite']);
const baseDom = Object.freeze({ id: 'save-card', className: 'card is-pending', text: 'Сохранить' });
const boundary = 10;
function copyDom(dom) {
return { id: dom.id, className: dom.className, text: dom.text };
}
function stage(name, units, reason) {
return Object.freeze({ name, units, unit: 'synthetic-work-units', reason });
}
/**
* Учебная модель одного render-frame. Она не создаёт DOM, браузер, trace,
* Performance panel, requestAnimationFrame, сеть, таймер, CPU profile или
* Web Vitals. `units` — спроектированные целые числа для проверки контракта,
* а не milliseconds, duration, FPS или измерение реального устройства.
*/
function planRenderFrame(input, currentDom = baseDom, frameId = 'teaching-frame-07') {
const initial = copyDom(currentDom);
const acceptedProperty = input?.property === 'className';
const acceptedTarget = input?.target === currentDom.id;
const declaredFromMatches = input?.from === currentDom.className;
const hasStringTarget = typeof input?.to === 'string' && input.to.length > 0;
if (!acceptedProperty || !acceptedTarget || !declaredFromMatches || !hasStringTarget) {
return Object.freeze({
kind: 'error',
frameId: null,
dom: Object.freeze(initial),
stages: Object.freeze([]),
totalUnits: 0,
boundary: Object.freeze({ limit: boundary, result: 'not-evaluated' }),
diagnostic: Object.freeze({
reason: 'invalid-update-contract',
expected: 'target save-card, property className, matching from and non-empty to',
}),
traceClaim: 'not-a-browser-trace',
});
}
if (input.to === currentDom.className) {
return Object.freeze({
kind: 'no-op',
frameId: null,
dom: Object.freeze(initial),
stages: Object.freeze([]),
totalUnits: 0,
boundary: Object.freeze({ limit: boundary, result: 'not-evaluated-no-change' }),
diagnostic: Object.freeze({ reason: 'same-className-no-frame-planned' }),
traceClaim: 'not-a-browser-trace',
});
}
const stages = Object.freeze([
stage('js-mutation', 1, 'apply-declared-className-change'),
stage('style', 2, 'resolve-style-after-className-change'),
stage('layout', 4, 'place-card-after-style-change'),
stage('paint', 3, 'prepare-card-pixels'),
stage('composite', 1, 'combine-prepared-card-layer'),
]);
const totalUnits = stages.reduce((total, item) => total + item.units, 0);
return Object.freeze({
kind: 'changed',
frameId,
dom: Object.freeze({ ...initial, className: input.to }),
stages,
totalUnits,
boundary: Object.freeze({
limit: boundary,
result: totalUnits > boundary ? 'jank-boundary' : 'within-teaching-boundary',
explanation: 'synthetic-work-units only; not a duration or dropped-frame observation',
}),
diagnostic: Object.freeze({
mutation: 'className ' + input.from + ' → ' + input.to,
performancePanelQuestion: 'find one real recording interaction, then inspect its browser-reported work without mapping labels by name',
inspectOrder: frameOrder,
}),
traceClaim: 'not-a-browser-trace',
});
}
function runBrowserRenderingFixture() {
const forwardInput = Object.freeze({
target: 'save-card',
property: 'className',
from: 'card is-pending',
to: 'card is-ready',
});
const forward = planRenderFrame(forwardInput);
const rollback = planRenderFrame(
Object.freeze({
target: 'save-card',
property: 'className',
from: 'card is-ready',
to: 'card is-pending',
}),
forward.dom,
'teaching-frame-08',
);
const noOp = planRenderFrame(
Object.freeze({
target: 'save-card',
property: 'className',
from: 'card is-pending',
to: 'card is-pending',
}),
);
const error = planRenderFrame(
Object.freeze({
target: 'save-card',
property: 'textContent',
from: 'Сохранить',
to: 'Готово',
}),
);
const assertions = Object.freeze({
changedRouteAccepted: forward.kind === 'changed',
changedResultUpdatesDeclaredDomProperty: forward.dom.className === 'card is-ready',
changedRouteKeepsText: forward.dom.text === 'Сохранить',
changedFrameHasStableId: forward.frameId === 'teaching-frame-07',
frameStagesHaveExpectedOrder: JSON.stringify(forward.stages.map((item) => item.name)) === JSON.stringify(frameOrder),
frameStagesAreAllNamed: forward.stages.every((item) => item.reason.length > 0),
syntheticUnitsAreExplicit: forward.stages.every((item) => item.unit === 'synthetic-work-units'),
syntheticTotalIsDeterministic: forward.totalUnits === 11,
jankBoundaryIsDefinedAndCrossed: forward.boundary.limit === 10 && forward.boundary.result === 'jank-boundary',
boundaryDoesNotClaimMilliseconds: forward.boundary.explanation.includes('not a duration'),
noOpDoesNotPlanFrame: noOp.kind === 'no-op' && noOp.stages.length === 0 && noOp.frameId === null,
noOpKeepsDom: noOp.dom.className === baseDom.className,
errorRouteRejectsUnknownProperty: error.kind === 'error' && error.diagnostic.reason === 'invalid-update-contract',
errorRouteDoesNotMutateDom: error.dom.className === baseDom.className && error.stages.length === 0,
rollbackReturnsBaseDom: rollback.kind === 'changed' && rollback.dom.className === baseDom.className,
rollbackGetsSeparateFrameAndSameOrder: rollback.frameId === 'teaching-frame-08' && JSON.stringify(rollback.stages.map((item) => item.name)) === JSON.stringify(frameOrder),
});
return Object.freeze({
kind: 'teaching-browser-render-frame-fixture-v1',
boundary: Object.freeze({ unit: 'synthetic-work-units', limit: boundary, browserObservation: 'not-collected', trace: 'not-collected', webVitals: 'not-collected' }),
forward,
rollback,
noOp,
error,
assertions,
});
}
const practiceArticle = createRevision(
{
slug: 'editorial-2022-03-practice-browser-rendering',
title: 'Медленный рендер без гадания: связать DOM-изменение с одним кадром',
categories: ['Frontend', 'Производительность'],
cover: '/assets/editorial/2022/browser-rendering-timeline-2022.svg',
excerpt: 'Практический маршрут для симптома «страница тормозит»: назвать одно DOM-изменение, собрать одну запись и проверить, где в кадре начинается работа — без подмены модели настоящим trace.',
readingMinutes: 12,
},
[
paragraph('Проблема «медленный рендер» обычно скрывает четыре разные работы: JavaScript меняет DOM, движок пересчитывает style, раскладывает boxes, готовит paint и собирает результат. Пока всё это называют одним словом, исправление выбирают наугад: переносят обработчик, меняют CSS или добавляют debounce. Цена — дорогая правка, после которой пользовательский сценарий не стал понятнее, а следующая запись Performance panel всё ещё не отвечает, какое действие создало тяжёлый кадр.'),
paragraph('Начинать стоит не с цветов в панели, а с одного наблюдаемого изменения. Например: нажатие «Сохранить» меняет у карточки <code>className</code> с <code>card is-pending</code> на <code>card is-ready</code>. У этого изменения есть исходный DOM-result, место в обработчике и ожидаемый новый result. Если такой контракт записан, его можно связать с одной interaction в реальной записи и просмотреть события выбранного кадра по порядку. Если контракта нет, trace остаётся длинной лентой, в которой всё похоже на причину.'),
heading('Сначала назвать границу, а не оптимизацию'),
paragraph('Полезная единица работы — «одна DOM-мутация в одном пользовательском действии». Не «вся страница», не «React медленный» и не «CSS тяжёлый». Для выбранной мутации выписываем target, property, old value, new value и способ повторить действие. В статье target — <code>save-card</code>, property — <code>className</code>. Это не универсальная форма для любого приложения. Она нужна, чтобы reviewer мог отличить исходную работу от соседней анимации, сетевого ответа или рендера другого компонента.'),
paragraph('Дальше важно не превратить имена стадий в обещание браузера. HTML Standard описывает update the rendering как алгоритм user agent; его подробности и планирование не дают автору права требовать одинаковый внутренний список событий у всех движков. Поэтому ниже есть учебная модель. Она говорит, какой вопрос задать о результате изменения: была ли JS-мутация, потребовалась ли работа style/layout/paint/composite, где образовалась граница диагностики. Она не говорит, как Chrome, Firefox или Safari обязаны назвать одно событие в trace.'),
dataTable(
'Контракт одного изменения до открытия Performance panel',
['Поле', 'Пример', 'Зачем нужно', 'Чего не доказывает'],
[
['Действие', 'нажатие «Сохранить»', 'повторить один сценарий', 'что все другие пути быстры'],
['DOM before', '<code>card is-pending</code>', 'зафиксировать исходное состояние', 'что style уже вычислен'],
['DOM after', '<code>card is-ready</code>', 'проверить результат обработчика', 'что был фактический paint'],
['Граница записи', 'одна interaction и один frame range', 'сузить ленту событий', 'причину без чтения деталей'],
['Следующий вопрос', 'какая работа следует за mutation', 'выбрать один объект проверки', 'готовое архитектурное решение'],
],
),
heading('Учебный контракт кадра'),
paragraph('Fixture пакета не открывает браузер. Она работает с локальными object и array, не вызывает DOM API, <code>requestAnimationFrame</code>, сеть, таймер, profiler или Performance API. Для валидной смены класса она строит пять labels: <code>js-mutation</code>, <code>style</code>, <code>layout</code>, <code>paint</code> и <code>composite</code>. У каждого label есть synthetic work units. Это проектные значения, специально выбранные для теста порядка и границы. Они не являются milliseconds, CPU, FPS, LCP, INP, Web Vitals или данными настоящей записи.'),
codeBlock(fixtureCommand),
paragraph('В forward route сумма равна 11 synthetic units: 1 + 2 + 4 + 3 + 1. Учебная граница равна 10, поэтому fixture отмечает <code>jank-boundary</code>. Это слово здесь не означает dropped frame и не является измерением задержки. Оно означает ровно одно: заранее описанное правило модели пересечено. Такая фиксация полезна для примера, потому что тест может проверить условие, а текст не обещает скорость устройства, которой никто не измерял.'),
figure(
'/assets/editorial/2022/browser-rendering-timeline-2022.svg',
'Учебная timeline одного изменения: нажатие меняет className карточки с is-pending на is-ready. Затем показаны пять стадий модели — JS mutation, style, layout, paint и composite — с synthetic units 1, 2, 4, 3, 1. Сумма 11 пересекает учебную jank-boundary 10. Рядом явно написано, что это не milliseconds и не trace браузера.',
'Timeline задаёт порядок вопроса для диагностики. Она не рисует и не измеряет настоящий кадр Performance panel.',
),
heading('Как связать изменение с реальной записью'),
paragraph('В настоящем проекте маршрут короче, чем кажется. Откройте страницу в согласованном состоянии, выполните только выбранное действие и остановите recording. Затем найдите interaction по времени и выделите узкий диапазон вокруг неё. Сначала вернитесь к коду: действительно ли обработчик меняет тот DOM-result, который записан в контракте? Затем читайте подробности выбранного диапазона, а не всю ленту. Если изменение оказалось другим, не переносите вывод с учебной схемы: обновите контракт или возьмите новый scenario.'),
orderedList([
'<strong>Симптом.</strong> После конкретного действия экран отвечает неровно, но «медленный рендер» не указывает место в коде.',
'<strong>Причина.</strong> В одну причину смешаны DOM mutation, style, layout, paint и JavaScript до них.',
'<strong>Проверка входа.</strong> Запишите target, property, before, after и один способ повторить действие. Убедитесь в DevTools Elements, что after действительно появился.',
'<strong>Проверка записи.</strong> Соберите одну interaction, выделите её диапазон и найдите работу, связанную с этим действием. Не называйте учебные labels буквальными именами trace events.',
'<strong>Действие.</strong> Выберите один следующий объект: handler, CSS rule, geometry read или paint boundary. Подготовьте одну обратимую правку.',
'<strong>Повтор.</strong> Снова выполните тот же контракт. Если DOM result изменился, старую запись нельзя использовать как доказательство нового поведения.',
]),
heading('Где часто теряется причина'),
paragraph('Первый сбой — брать запись после серии кликов. В ней соседствуют несколько mutations, timers, network callbacks и события интерфейса. Даже если внутри есть длинный участок, привязать его к одной строке кода уже трудно. Второй сбой — смотреть только итоговую шкалу и не проверять DOM after. Тогда мы не знаем, рисовали ли именно ожидаемый элемент или обработчик вообще выбрал другую ветку. Третий — считать, что style, layout и paint всегда существуют как отдельные и одинаково подписанные карточки. Это удобная ментальная модель, но не API контракта между приложением и браузером.'),
paragraph('Сильнее работает обратная формулировка: «после className X → Y я хочу понять, какая работа возникла в выбранной записи и можно ли убрать её без изменения result». Она запрещает лечить симптом удалением видимого эффекта. Если карточка должна стать <code>is-ready</code>, действие не считается успешным, пока after-state не сохранён. Производительность и функциональный result проверяются вместе, а не в разных спорящих задачах.'),
heading('Ограничения модели'),
paragraph('Учебный порядок из пяти labels не раскрывает все случаи. Например, изменение может не требовать layout, браузер может отложить часть работы, а compositing зависит от свойств и движка. Модель также не содержит изображений, шрифтов, iframe, анимации, cache, процессора или расширений. Поэтому она не отвечает на вопрос «какой CSS property всегда дешёвый» и не годится для сравнения устройств. Её единственная роль — удержать связь между declared DOM result, порядком проверки и обратимым действием.'),
paragraph('Источники ниже существуют до марта 2022 года: immutable HTML snapshot от 27 февраля, датированная W3C Working Group Note для animation timing и W3C draft Performance Timeline 2021 года. Они помогают не выдать модель за стандартную шкалу или trace. Ни один из них не задаёт synthetic units и boundary fixture. Перед переносом на приложение зафиксируйте версию браузера, сценарий и способ записи отдельно: это уже свойства реального исследования, которых в пакете намеренно нет.'),
heading('Следующий проверяемый шаг'),
paragraph('Возьмите один реальный button или input, составьте таблицу before/after и сделайте единственную запись вокруг одного действия. Результатом следующего шага должен стать не «страница быстрее», а проверяемый артефакт: ссылка на handler, DOM-result и один выделенный диапазон trace. Затем внесите обратимое изменение и повторите тот же сценарий. Если after-state сохранён, а связанная работа стала понятнее, диагностика уже стала дешевле — без придуманной production-метрики.'),
],
commonSources,
);
const mechanismArticle = createRevision(
{
slug: 'editorial-2022-03-mechanism-browser-rendering',
title: 'Style, layout и paint: не три виновника, а границы одного кадра',
categories: ['Frontend', 'Производительность'],
cover: '/assets/editorial/2022/browser-rendering-pipeline-2022.svg',
excerpt: 'Причинная модель для DOM-изменения: отделить JavaScript, style, layout, paint и composite, чтобы не выдавать учебную схему за внутренний контракт браузера.',
readingMinutes: 13,
},
[
paragraph('Проблема возникает, когда на карточке меняется класс, а команда спорит, «это JavaScript» или «это CSS». Оба ответа могут быть неполными. JavaScript может только записать className; дальше браузер решает, нужна ли работа со style, геометрией, пикселями и слоями. Цена неверной модели заметна сразу: удаляют часть handler, хотя DOM-result должен остаться, либо переписывают style rule, который не относится к выбранному действию. В итоге меняется код, но причина в Performance panel остаётся неподтверждённой.'),
paragraph('Нужна простая причинная граница: изменение значения — это вход; visual result — это выход; между ними есть работа, которую нужно наблюдать на одной записи. Нельзя честно назвать одинаковый конвейер для каждого браузера и свойства. Но можно не смешивать вопросы. Что изменил handler? Какой result стал виден? Какая работа находится после этого действия в выбранном диапазоне? Какое минимальное обратимое действие проверит гипотезу? Такой порядок даёт техническую речь без лозунга «избегайте layout». '),
heading('Пять labels модели и их ответственность'),
paragraph('В модели <code>js-mutation</code> применяет объявленное изменение <code>className</code>. <code>style</code> означает, что нужно определить применимые стили после нового класса. <code>layout</code> представляет расчёт положения карточки после style. <code>paint</code> представляет подготовку её пикселей. <code>composite</code> представляет сборку подготовленного результата. Это не протокол браузера и не словарь Performance panel. Это пять вопросов, которые удобно держать рядом с DOM contract, чтобы не назвать одну широкую область причиной до исследования.'),
dataTable(
'Причинная модель кадра: что проверяем после DOM mutation',
['Label модели', 'Вход', 'Результат модели', 'Вопрос к записи', 'Неправильный вывод'],
[
['<code>js-mutation</code>', 'handler и old/new className', 'обновлённый DOM object', 'какой код сделал mutation?', '«всё после клика — JavaScript»'],
['<code>style</code>', 'новый selector state', 'resolved style в модели', 'есть ли связанная работа со style?', '«class всегда означает одну цену»'],
['<code>layout</code>', 'style, влияющий на геометрию', 'placement карточки', 'какая геометрия стала нужна?', '«layout всегда отдельная карточка»'],
['<code>paint</code>', 'визуальное представление', 'prepared pixels', 'какой визуальный участок обновился?', '«paint равен CSS rule»'],
['<code>composite</code>', 'prepared result', 'собранный frame result', 'какая финальная работа видна?', '«это автоматически дешёвый этап»'],
],
),
paragraph('Слово «модель» здесь делает важную работу. HTML Standard говорит о том, что user agent обновляет rendering, но не обещает авторскому коду доступ к одной неизменной внутренней очереди. Trace format Chromium описывает, как события следа представляются в записи, а не контракт «после className всегда идёт именно такой набор работ». Поэтому между документом, записью и проектным решением есть граница: стандарт и source помогают назвать предмет; запись показывает поведение выбранной версии; решение выбирает изменение и способ его отменить.'),
figure(
'/assets/editorial/2022/browser-rendering-pipeline-2022.svg',
'Учебная pipeline-схема: DOM contract className is-pending → is-ready входит в JS mutation, затем проходит через style, layout, paint и composite. Под каждым блоком указаны вопрос к выбранному диапазону записи и synthetic work units. Пунктирная рамка предупреждает, что labels не являются обязательными именами браузерных событий.',
'Pipeline разделяет вопросы о причине. Она не утверждает, что любой браузер отдаёт пять одинаковых trace-событий.',
),
heading('Почему порядок важнее суммы'),
paragraph('Fixture ставит units 1, 2, 4, 3 и 1. Их сумма 11 пересекает teaching boundary 10. Числа специально не похожи на duration: это не миллисекунды, не CPU time и не FPS. Важен порядок. Если мы видим, что договорённая mutation не произошла, бессмысленно обсуждать paint. Если mutation есть, но DOM after не тот, сначала исправляется функциональный путь. Если result правильный, можно перейти к следующему вопросу и ограничить гипотезу. Сумма в такой модели нужна только для теста определённой ветки, а не для рейтинга производительности.'),
codeBlock(routeExample),
paragraph('Валидный route проходит строго через пять labels. В fixture проверяется exact order и то, что у каждой стадии есть причина. Это полезно как защита от случайной правки самой модели: нельзя переставить paint перед layout и продолжать рассказывать тот же учебный маршрут. Но fixture не проверяет движок. Её нельзя запускать вместо Performance panel, сравнивать с отчётом Lighthouse или приклеивать к CI как замер страницы. Она проверяет только контракт статьи: что заявленные вход, стадийность, boundary и result не противоречат друг другу.'),
heading('Jank boundary — условие остановки, а не диагноз'),
paragraph('Название <code>jank-boundary</code> здесь намеренно ограничено. Оно срабатывает, когда 11 условных единиц больше 10. Оно не говорит, что пользователь увидел рывок, что кадр пропущен или что устройство не успело выполнить работу. В настоящем исследовании такие выводы требуют настоящей записи, условий запуска и наблюдения конкретного браузера. В учебной модели boundary отвечает на иной вопрос: есть ли заранее определённый случай, в котором автор обязан не продолжать оптимизацию на словах, а перейти к следующей проверке.'),
paragraph('Это похоже на type check. Компилятор не измеряет удобство интерфейса, но останавливает конкретное несоответствие. Здесь ограничение останавливает расплывчатый вывод: «после клика что-то много происходило». Мы получаем ровно: «для declared change модель прошла определённую границу; теперь надо собрать реальный evidence на этой границе». Такая дисциплина не делает браузер предсказуемее, зато не позволяет из красивой схемы сделать выдуманный benchmark.'),
heading('Две ветки, которые должны быть пустыми'),
paragraph('No-op route важен не меньше changed route. Если <code>to</code> совпадает с текущим <code>className</code>, fixture возвращает <code>no-op</code>, не выдаёт frameId и оставляет stages пустыми. В модели это означает: новое действие не объявлено, поэтому конвейер не планируется. Это не утверждение, что реальный браузер никогда не делает никакой работы рядом с одинаковой записью свойства. Это защита контракта: статья не должна изображать кадр, если сама заявляет отсутствие DOM result.'),
paragraph('Error route столь же полезен. Если передан <code>textContent</code> вместо разрешённого <code>className</code>, либо target/from не совпадают с контрактом, модель возвращает <code>invalid-update-contract</code> и также не меняет DOM object. Ошибка не превращается в «нулевой быстрый кадр». Она означает, что у автора нет основания строить причинную историю. В реальной задаче это момент вернуться к handler, selector или тесту UI, а не искать удачный цвет в диаграмме.'),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'<strong>Симптом.</strong> Один клик меняет интерфейс неровно, а команда называет причину то JS, то CSS.',
'<strong>Причина.</strong> Не записаны mutation и result, поэтому разные стадии и соседняя работа попали в одну историю.',
'<strong>Проверка контракта.</strong> Сверьте target, property, from и to. Если это не className <code>save-card</code>, fixture честно идёт в error route.',
'<strong>Проверка порядка.</strong> Для changed route удерживайте цепочку mutation → style → layout → paint → composite как порядок вопросов, не как имена trace events.',
'<strong>Проверка реальности.</strong> В Performance panel выделите только interaction и сопоставьте её с кодом. Если доменный result другой, начните новый сценарий.',
'<strong>Действие.</strong> Поменяйте один owner: handler, правило style или geometry read. Сохраните способ rollback до повторной записи.',
]),
heading('Обратимость не должна ломать визуальный result'),
paragraph('Проверка улучшения без отмены легко обманывает. Можно убрать className change и увидеть меньше работы, но карточка больше не сообщает готовность. Поэтому fixture создаёт второй frame: rollback меняет <code>card is-ready</code> обратно в <code>card is-pending</code>, получает отдельный <code>teaching-frame-08</code> и проходит тот же порядок labels. Assertion требует, чтобы final DOM равнялся base DOM. Это не browser undo; это минимальная проверка, что мы умеем вернуть исходный функциональный контракт.'),
codeBlock(rollbackExample),
paragraph('В приложении rollback может быть feature flag, локальная правка selector или отдельный commit. Форма не важна, пока она возвращает ожидаемый DOM result и может быть повторена. Важно не смешивать rollback с отсутствием изменения: no-op ничего не проверяет, потому что не создаёт нового result. Обратимое изменение создаёт result, наблюдает его, а затем осознанно возвращает старый. Именно поэтому оно пригодно для исследования, а не только для безопасного релиза.'),
heading('Ограничения и историческая рамка'),
paragraph('Март 2022 — подходящая рамка для разговора о DevTools trace и frame update, но не повод использовать будущие интерфейсные метрики или поздние UI Performance panel. Здесь нет INP, interaction breakdown, современных DevTools labels и обещаний о RAIL. Ссылки зафиксированы до заданного месяца: WHATWG commit от 27 февраля 2022 года, W3C Working Group Note 2015 года и W3C Performance Timeline draft августа 2021-го. Они первичны или официальны, но не превращают эту схему в нормативную декомпозицию render pipeline.'),
paragraph('Следующий проверяемый шаг: добавьте в задачу ровно одну строку «mutation, before, after, owner, rollback». После первой реальной записи оставьте ссылку на выделенный range и короткую гипотезу уровня «проверить geometry read в handler», а не «убрать layout». Если следующий trace не подтверждает связь, вернитесь к контракту. Это быстрее, чем защищать оптимизацию, которая была выбрана до того, как появилось доказательство.'),
],
commonSources,
);
const fieldArticle = createRevision(
{
slug: 'editorial-2022-03-field-browser-rendering',
title: 'Разбор одного тяжёлого кадра: от DOM result к обратимой проверке',
categories: ['Производительность', 'Frontend'],
cover: '/assets/editorial/2022/browser-rendering-diagnosis-rollback-2022.svg',
excerpt: 'Полевой маршрут для Performance panel: не лечить «медленный рендер» по цвету, а сузить один DOM-result, одну interaction, один вопрос к trace и обратимую правку.',
readingMinutes: 13,
},
[
paragraph('В записи Performance panel легко найти широкий участок работы и ещё легче назвать его «проблемой рендера». Но из такой фразы нельзя получить безопасную правку. Неизвестно, какой пользовательский шаг начался раньше, какой DOM-result был нужен и что сломается, если просто убрать часть кода. Цена — оптимизация, которая делает график спокойнее, но оставляет кнопку в старом состоянии, убирает сообщение об ошибке или переносит работу в другой момент без объяснения.'),
paragraph('Полевой разбор начинается с малого: один input, один DOM-result и одна запись. Для примера пользователь нажимает «Сохранить», а карточка меняет <code>className</code> с <code>card is-pending</code> на <code>card is-ready</code>. Мы не утверждаем, что такой сценарий произошёл в реальном продукте. Это учебный case. Его задача — показать маршрут, где каждое утверждение можно заменить артефактом своего проекта: DOM snapshot, ссылка на handler, выбранный range trace и обратимая правка.'),
heading('Сначала зафиксировать то, что нельзя потерять'),
paragraph('До записи сформулируйте result в одном предложении: «после submit карточка остаётся той же, но className становится is-ready». Сохраните before/after в Elements или в UI-test expectation. Это удерживает границу функциональности. Если следующая правка убирает visual state, она не проходит, даже если trace выглядит лучше. Производительность не является отдельным экраном без результата; пользователь нажал кнопку именно ради изменения интерфейса.'),
dataTable(
'Полевой лист для одного исследования',
['Артефакт', 'Что записать', 'Критерий готовности', 'Типичная ловушка'],
[
['Сценарий', 'одно действие, исходный экран', 'другой человек повторяет его', 'записать длинную серию кликов'],
['DOM contract', 'target/property/before/after', 'after виден после действия', 'считать код handler доказательством результата'],
['Recording range', 'одна interaction и узкий диапазон', 'range соотносится со сценарием', 'читать всю timeline сразу'],
['Гипотеза', 'один owner и один вопрос', 'можно опровергнуть следующей записью', 'объявить виновным весь CSS или framework'],
['Rollback', 'как вернуть old result', 'после отмены DOM равен before', 'путать отмену с no-op'],
],
),
paragraph('Fixture вводит ровно те же поля, но оставляет их учебными. Changed route получает frame id, пять labels и total 11 synthetic work units. Boundary 10 даёт <code>jank-boundary</code>. Никаких duration, CPU, frame rate или реального UI в ней нет. Поэтому нельзя скопировать число 11 в performance ticket. Можно скопировать структуру вопроса: почему после этой mutation в выбранной записи находится работа, какой owner способен её поменять и как проверить, что DOM result не потерян.'),
figure(
'/assets/editorial/2022/browser-rendering-diagnosis-rollback-2022.svg',
'Диагностическая схема: сначала проверяется DOM result className is-pending → is-ready. Затем выбирается одна interaction и порядок вопросов JS mutation, style, layout, paint, composite. При пересечении учебной boundary создаётся гипотеза об одном owner и обратимое изменение. Ветка no-op останавливает расследование; rollback возвращает is-pending отдельным кадром. Все units отмечены как synthetic, не trace и не milliseconds.',
'Схема показывает, когда надо остановиться: при неверном DOM result, no-op или invalid contract нельзя делать performance verdict.',
),
heading('Читать запись от действия к коду'),
paragraph('Открыв Performance panel, сначала сузьте время: начните recording на уже подготовленном экране, выполните одно действие и остановите его сразу после visual result. Выберите interaction и найдите связанный участок на main thread. Затем вернитесь в код и спросите, что именно меняет DOM. Не делайте обратное — не берите самый широкий блок и не ищите ему удобное объяснение. У широкого блока могут быть unrelated timers, обработка сети, расширение браузера или соседняя animation. Без связи со сценарием у него нет владельца для правки.'),
paragraph('После связи переходите по одному вопросу. Если handler читает геометрию после записи style, проверяем этот read. Если DOM result появляется раньше, а видимая работа следует позже, проверяем CSS rule и область update. Если mutation вообще не соответствует before/after, не продолжаем investigation: это другой сценарий. Такой порядок не обещает быстрый результат, но экономит время на спорах. Каждый шаг либо сужает причину, либо честно возвращает к отсутствующему артефакту.'),
codeBlock(diagnosticExample),
heading('Три остановки, которые защищают от ложного вывода'),
paragraph('Первая остановка — error route. В fixture неизвестное property, target или from возвращают <code>invalid-update-contract</code>, stages пусты и DOM не меняется. Это означает: модель не знает, что исследует. В проекте аналогом будет handler, который не является владельцем нужного result, или тест, который не воспроизводит действие. Нельзя назвать такой маршрут «быстрым»: он просто не прошёл входную проверку.'),
paragraph('Вторая остановка — no-op. Если old value равен new value, model возвращает <code>same-className-no-frame-planned</code>. Реальный браузер может иметь другую сопутствующую работу, но конкретный contract не создал нового visual result. Поэтому recording надо пересобрать: возможно, действие уже было выполнено, state пришёл из cache или выбран неверный элемент. Продолжать искать layout в таком case — значит исследовать не то, что пользователь сделал.'),
paragraph('Третья остановка — teaching boundary. Она не говорит «нашли jank», а запрещает довольствоваться суммой. Когда 11 больше 10, требуется одно ограниченное действие и повтор. Сумма не выбирает действие сама. Владелец может быть handler, selector, geometry read или код, который ставит className. Без связи с реальной записью она остаётся условной веткой fixture. Это ограничение полезно тем, что не позволяет превратить диаграмму в результат расследования.'),
heading('Обратимое действие: одна гипотеза, один owner'),
paragraph('Хорошая первая правка маленькая и отменяемая. Например, временно вынести один geometry read из handler, изменить локальное правило или отключить один visual effect за feature toggle. Нельзя заранее обещать, что это уменьшит layout или paint: сначала надо посмотреть новую запись. У правки есть owner, expected DOM after и путь rollback. Если change не сохраняет result, она не проходит функциональный gate. Если после change связь с записью неясна, evidence недостаточен для сильного вывода.'),
paragraph('Fixture проверяет обратимость явно. Forward переводит карточку в <code>is-ready</code>; rollback — обратно в <code>is-pending</code>. У rollback отдельный frame id и тот же учебный порядок стадий. Итоговый DOM должен совпасть с base DOM. Этот test не говорит, что браузер отменяет frame или что в реальном интерфейсе нет побочных эффектов. Он удерживает более скромный контракт: автор умеет вернуть наблюдаемое состояние и не выдаёт «удалить изменение» за оптимизацию.'),
codeBlock(rollbackExample),
heading('Маршрут: симптом → причина → проверка → действие'),
orderedList([
'<strong>Симптом.</strong> В конкретном сценарии интерфейс отвечает неровно, но trace содержит слишком много событий.',
'<strong>Причина.</strong> Нет связи между user action, DOM result и выбранным range; слова style/layout/paint стали общими ярлыками.',
'<strong>Проверка result.</strong> Зафиксируйте target, property, before и after. Если after не тот, расследование заканчивается до Performance panel.',
'<strong>Проверка range.</strong> Повторите один action, выделите одну interaction и сопоставьте её с handler. Отделите этот range от соседних callbacks.',
'<strong>Проверка гипотезы.</strong> Назовите один owner и один вопрос, который можно опровергнуть новой записью. Не переносите synthetic labels на реальные event names.',
'<strong>Действие и rollback.</strong> Внесите маленькую обратимую правку, повторите сценарий и проверьте и DOM after, и новый range.',
]),
heading('Что положить в результат исследования'),
paragraph('Хороший результат помещается в несколько строк: версия браузера и условия записи; шаг действия; DOM before/after; ссылка на handler; выбранный range; один наблюдаемый факт; одна гипотеза; commit или toggle для rollback. Здесь нет места фразе «ускорили рендер», пока нет измерения, метода и сравниваемого варианта. Даже если реальная запись стала лучше, сначала нужно назвать, что именно изменилось и при каких условиях. Это язык системного практика 2022 года: достаточно строгий, чтобы другой инженер продолжил проверку, и достаточно скромный, чтобы не придумать эффект.'),
paragraph('Исторические источники помогают обозначить границы. WHATWG snapshot от 27 февраля 2022 года показывает, что rendering обновляет user agent, а не приложение по фиксированному авторскому списку. W3C Working Group Note объясняет назначение animation frame callback, но fixture не вызывает <code>requestAnimationFrame</code>. Performance Timeline draft описывает работу с performance entries, но учебный result не является entry или trace. На этом материале нельзя обосновать актуальный интерфейс DevTools или поздние performance guidance; для настоящей записи всегда проверяйте версию конкретного браузера.'),
heading('Следующий проверяемый шаг'),
paragraph('Выберите один button на своей странице и проведите маршрут один раз без оптимизации. Итогом будут не числа, а четыре артефакта: DOM before/after, ссылка на mutation, один range и одна отменяемая гипотеза. Запустите fixture пакета, чтобы увидеть, что no-op, error и rollback не маскируются под успешный кадр. Затем проведите реальную запись по тому же контракту. Если она не подтверждает связь, это полезный результат: нужно менять сценарий или вопрос, а не публиковать удобную причину.'),
],
commonSources,
);
export const revisions = [practiceArticle, mechanismArticle, fieldArticle].map(({ proseLength, ...revision }) => revision);
function verifyFixture() {
const fixture = runBrowserRenderingFixture();
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));
}