DarkRiDDeR13 мин

Разбор одного тяжёлого кадра: от DOM result к обратимой проверке

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

В записи Performance panel легко найти широкий участок работы и ещё легче назвать его «проблемой рендера». Но из такой фразы нельзя получить безопасную правку. Неизвестно, какой пользовательский шаг начался раньше, какой DOM-result был нужен и что сломается, если просто убрать часть кода. Цена — оптимизация, которая делает график спокойнее, но оставляет кнопку в старом состоянии, убирает сообщение об ошибке или переносит работу в другой момент без объяснения.

Полевой разбор начинается с малого: один input, один DOM-result и одна запись. Для примера пользователь нажимает «Сохранить», а карточка меняет className с card is-pending на card is-ready. Мы не утверждаем, что такой сценарий произошёл в реальном продукте. Это учебный case. Его задача — показать маршрут, где каждое утверждение можно заменить артефактом своего проекта: DOM snapshot, ссылка на handler, выбранный range trace и обратимая правка.

Сначала зафиксировать то, что нельзя потерять

До записи сформулируйте result в одном предложении: «после submit карточка остаётся той же, но className становится is-ready». Сохраните before/after в Elements или в UI-test expectation. Это удерживает границу функциональности. Если следующая правка убирает visual state, она не проходит, даже если trace выглядит лучше. Производительность не является отдельным экраном без результата; пользователь нажал кнопку именно ради изменения интерфейса.

Полевой лист для одного исследования
АртефактЧто записатьКритерий готовностиТипичная ловушка
Сценарийодно действие, исходный экрандругой человек повторяет егозаписать длинную серию кликов
DOM contracttarget/property/before/afterafter виден после действиясчитать код handler доказательством результата
Recording rangeодна interaction и узкий диапазонrange соотносится со сценариемчитать всю timeline сразу
Гипотезаодин owner и один вопросможно опровергнуть следующей записьюобъявить виновным весь CSS или framework
Rollbackкак вернуть old resultпосле отмены DOM равен beforeпутать отмену с no-op

Fixture вводит ровно те же поля, но оставляет их учебными. Changed route получает frame id, пять labels и total 11 synthetic work units. Boundary 10 даёт jank-boundary. Никаких duration, CPU, frame rate или реального UI в ней нет. Поэтому нельзя скопировать число 11 в performance ticket. Можно скопировать структуру вопроса: почему после этой mutation в выбранной записи находится работа, какой owner способен её поменять и как проверить, что DOM result не потерян.

Диагностическая схема: сначала проверяется 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.

Читать запись от действия к коду

Открыв Performance panel, сначала сузьте время: начните recording на уже подготовленном экране, выполните одно действие и остановите его сразу после visual result. Выберите interaction и найдите связанный участок на main thread. Затем вернитесь в код и спросите, что именно меняет DOM. Не делайте обратное — не берите самый широкий блок и не ищите ему удобное объяснение. У широкого блока могут быть unrelated timers, обработка сети, расширение браузера или соседняя animation. Без связи со сценарием у него нет владельца для правки.

После связи переходите по одному вопросу. Если handler читает геометрию после записи style, проверяем этот read. Если DOM result появляется раньше, а видимая работа следует позже, проверяем CSS rule и область update. Если mutation вообще не соответствует before/after, не продолжаем investigation: это другой сценарий. Такой порядок не обещает быстрый результат, но экономит время на спорах. Каждый шаг либо сужает причину, либо честно возвращает к отсутствующему артефакту.

// Сначала читается изменение, затем его одна 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.

Три остановки, которые защищают от ложного вывода

Первая остановка — error route. В fixture неизвестное property, target или from возвращают invalid-update-contract, stages пусты и DOM не меняется. Это означает: модель не знает, что исследует. В проекте аналогом будет handler, который не является владельцем нужного result, или тест, который не воспроизводит действие. Нельзя назвать такой маршрут «быстрым»: он просто не прошёл входную проверку.

Вторая остановка — no-op. Если old value равен new value, model возвращает same-className-no-frame-planned. Реальный браузер может иметь другую сопутствующую работу, но конкретный contract не создал нового visual result. Поэтому recording надо пересобрать: возможно, действие уже было выполнено, state пришёл из cache или выбран неверный элемент. Продолжать искать layout в таком case — значит исследовать не то, что пользователь сделал.

Третья остановка — teaching boundary. Она не говорит «нашли jank», а запрещает довольствоваться суммой. Когда 11 больше 10, требуется одно ограниченное действие и повтор. Сумма не выбирает действие сама. Владелец может быть handler, selector, geometry read или код, который ставит className. Без связи с реальной записью она остаётся условной веткой fixture. Это ограничение полезно тем, что не позволяет превратить диаграмму в результат расследования.

Обратимое действие: одна гипотеза, один owner

Хорошая первая правка маленькая и отменяемая. Например, временно вынести один geometry read из handler, изменить локальное правило или отключить один visual effect за feature toggle. Нельзя заранее обещать, что это уменьшит layout или paint: сначала надо посмотреть новую запись. У правки есть owner, expected DOM after и путь rollback. Если change не сохраняет result, она не проходит функциональный gate. Если после change связь с записью неясна, evidence недостаточен для сильного вывода.

Fixture проверяет обратимость явно. Forward переводит карточку в is-ready; rollback — обратно в is-pending. У rollback отдельный frame id и тот же учебный порядок стадий. Итоговый DOM должен совпасть с base DOM. Этот test не говорит, что браузер отменяет frame или что в реальном интерфейсе нет побочных эффектов. Он удерживает более скромный контракт: автор умеет вернуть наблюдаемое состояние и не выдаёт «удалить изменение» за оптимизацию.

// В модели обратимое действие — отдельный контракт, не 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 этот пример не создаёт.

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

  1. Симптом. В конкретном сценарии интерфейс отвечает неровно, но trace содержит слишком много событий.
  2. Причина. Нет связи между user action, DOM result и выбранным range; слова style/layout/paint стали общими ярлыками.
  3. Проверка result. Зафиксируйте target, property, before и after. Если after не тот, расследование заканчивается до Performance panel.
  4. Проверка range. Повторите один action, выделите одну interaction и сопоставьте её с handler. Отделите этот range от соседних callbacks.
  5. Проверка гипотезы. Назовите один owner и один вопрос, который можно опровергнуть новой записью. Не переносите synthetic labels на реальные event names.
  6. Действие и rollback. Внесите маленькую обратимую правку, повторите сценарий и проверьте и DOM after, и новый range.

Что положить в результат исследования

Хороший результат помещается в несколько строк: версия браузера и условия записи; шаг действия; DOM before/after; ссылка на handler; выбранный range; один наблюдаемый факт; одна гипотеза; commit или toggle для rollback. Здесь нет места фразе «ускорили рендер», пока нет измерения, метода и сравниваемого варианта. Даже если реальная запись стала лучше, сначала нужно назвать, что именно изменилось и при каких условиях. Это язык системного практика 2022 года: достаточно строгий, чтобы другой инженер продолжил проверку, и достаточно скромный, чтобы не придумать эффект.

Исторические источники помогают обозначить границы. WHATWG snapshot от 27 февраля 2022 года показывает, что rendering обновляет user agent, а не приложение по фиксированному авторскому списку. W3C Working Group Note объясняет назначение animation frame callback, но fixture не вызывает requestAnimationFrame. Performance Timeline draft описывает работу с performance entries, но учебный result не является entry или trace. На этом материале нельзя обосновать актуальный интерфейс DevTools или поздние performance guidance; для настоящей записи всегда проверяйте версию конкретного браузера.

Следующий проверяемый шаг

Выберите один button на своей странице и проведите маршрут один раз без оптимизации. Итогом будут не числа, а четыре артефакта: DOM before/after, ссылка на mutation, один range и одна отменяемая гипотеза. Запустите fixture пакета, чтобы увидеть, что no-op, error и rollback не маскируются под успешный кадр. Затем проведите реальную запись по тому же контракту. Если она не подтверждает связь, это полезный результат: нужно менять сценарий или вопрос, а не публиковать удобную причину.

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

  • WHATWG HTML: immutable snapshot 2022-03-01 — первичный снимок HTML Standard от 27 февраля 2022 года. Он задаёт алгоритм update the rendering и подчёркивает, что шаги зависят от user agent; он не устанавливает числовой бюджет и не обещает одинаковую последовательность внутренних событий во всех браузерах.
  • W3C Timing control for script-based animations, Working Group Note от 22 сентября 2015 года — датированная W3C Working Group Note от 22 сентября 2015 года, доступная к марту 2022-го. Она описывает requestAnimationFrame как запрос к user agent запланировать animation frame update; это не Recommendation. Fixture не реализует этот API и не измеряет частоту кадров.
  • W3C Performance Timeline Level 2, Working Draft от 19 августа 2021 года — датированный первичный W3C draft о performance entries и их получении. Он не описывает labels fixture, не задаёт teaching boundary и не превращает synthetic units в настоящий trace или данные Performance panel.