This commit is contained in:
@@ -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 january2022Revisions } from '../scripts/upgrade-2022-01.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.
|
||||
export const editorialRevisions = [
|
||||
@@ -95,4 +96,5 @@ export const editorialRevisions = [
|
||||
...december2021Revisions,
|
||||
...january2022Revisions,
|
||||
...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 > 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 |
@@ -0,0 +1,487 @@
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
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(' ', ' ')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll(''', "'")
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('&', '&')
|
||||
.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));
|
||||
}
|
||||
Reference in New Issue
Block a user