diff --git a/editorial/agent-rewrites/208.json b/editorial/agent-rewrites/208.json index dceb344..f24e63b 100644 --- a/editorial/agent-rewrites/208.json +++ b/editorial/agent-rewrites/208.json @@ -3,5 +3,5 @@ "slug": "editorial-2022-03-field-browser-rendering", "title": "Тяжёлый кадр в браузере: как найти причину и проверить обратимую правку", "excerpt": "Если интерфейс дёргается, широкий участок Performance trace ещё не объясняет причину. Разбираем один пользовательский шаг через DOM-результат, владельца изменения, узкий диапазон записи и rollback.", - "contentHtml": "
Пользователь нажимает «Сохранить», карточка должна перейти из card is-pending в card is-ready, но интерфейс на мгновение замирает. В Performance panel виден широкий участок main thread с JavaScript, style, layout и paint. Если назвать весь участок «медленным рендером», команда может удалить нужную мутацию, перенести работу в другой callback или оптимизировать не тот экран. Цена ошибки — потерянный визуальный результат, новый дефект вместо ускорения и повторное расследование после релиза.
Тезис простой: тяжёлый кадр нужно разбирать от наблюдаемого результата к конкретному владельцу изменения. Сначала зафиксируйте, что изменилось в DOM. Затем свяжите одно действие пользователя с узким диапазоном trace. После этого проверьте одну гипотезу маленькой обратимой правкой. Только новая запись, в которой сохранился DOM-результат, позволяет обсуждать эффект правки.
\nБраузер не получает от приложения готовую команду «сделай layout». Код меняет DOM, CSS-классы, inline-стили, текст или геометрию. User agent сам обновляет представление документа и планирует нужные этапы. В trace эти этапы показываются как события выбранной версии браузера. Поэтому слова style, layout, paint и composite помогают сформулировать вопрос, но сами по себе не указывают владельца.
Владелец находится в коде и в контракте результата. Для примера контракт выглядит так: пользователь выполняет один submit; target карточки остаётся тем же; className меняется с card is-pending на card is-ready; при ошибке класс не меняется; rollback возвращает исходное состояние. Если trace стал короче, но класс больше не меняется, оптимизация не прошла функциональную проверку.
const before = card.className;\n\nfunction onSave(card, response) {\n if (!response.ok) {\n return { result: 'error', className: before };\n }\n\n const nextClassName = 'card is-ready';\n card.className = nextClassName;\n return { result: 'ready', className: nextClassName };\n}\n\nfunction rollback(card, before) {\n card.className = before;\n}\nКод учебный. Он не измеряет длительность, не вызывает настоящий network request и не доказывает, что браузер нарисовал карточку в конкретном кадре. Его задача — показать границу наблюдения: до оптимизации известны before и after, а у изменения есть явная отмена. В рабочем компоненте тот же контракт нужно связать с реальным обработчиком, состоянием ошибки и снимком DOM.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Кнопка отвечает с задержкой | В запись попали соседние timers, сеть или расширение | Повторить один submit на чистом экране и выделить его interaction | Отделить диапазон действия от соседних событий |
| Trace широкий, причина неясна | Ищут общий ярлык вместо связи с DOM-результатом | Сравнить target, property, before и after | Вернуться к mutation и назвать одного владельца |
| После правки график спокойнее, класс не меняется | Удалили функциональную мутацию вместе с лишней работой | Проверить DOM after тем же сценарием | Откатить правку и разделить визуальный результат и оптимизацию |
| Появился layout, но действие не меняло геометрию | Проверяется не тот range или другой код прочитал geometry | Найти вызов чтения размеров и его стек | Проверить один geometry read, а не весь layout |
| Повторная запись даёт другой trace | Изменились CPU, viewport, cache, данные или версия браузера | Зафиксировать среду и повторить сценарий несколько раз | Сравнивать только согласованные условия и сохранять запись |
| «Улучшение» нельзя отменить | Не определены старое состояние и путь rollback | Вернуть before отдельным запуском и проверить DOM | Сделать изменение локальным, переключаемым или отдельным commit |
Представим форму сохранения карточки. До действия в DOM есть <article class=\"card is-pending\">. Пользователь нажимает кнопку. Обработчик отправляет данные и после успешного ответа добавляет is-ready. В учебном сценарии тяжёлую работу связывают с чтением геометрии сразу после записи стиля. Это гипотеза, а не утверждение о любом браузере: она должна быть подтверждена стеком вызовов и новой записью.
function updateCard(card) {\n card.classList.add('is-ready');\n const height = card.getBoundingClientRect().height;\n logLayoutDependentValue(height);\n}\nТакой порядок может потребовать дополнительной работы по обновлению стилей и layout, но по одному фрагменту нельзя заключить, что именно он вызвал задержку. Возможно, размер уже был нужен другому коду. Возможно, запись попала в другой пользовательский сценарий. Проверка должна ответить на узкий вопрос: связан ли этот обработчик и этот read с выбранным участком записи, и сохраняется ли is-ready после изменения?
Отрицательный путь обязателен. Если сервер вернул ошибку, карточка должна остаться is-pending, сообщение об ошибке должно остаться доступным пользователю, а trace нельзя объявлять успешным только потому, что в нём стало меньше событий. Если выбранный элемент уже имел is-ready, действие может оказаться no-op. Тогда новая запись не проверяет переход состояния: сначала нужно вернуть исходный state и повторить сценарий.
Начинайте запись на уже подготовленной странице. Выполните одно действие и остановите запись сразу после появления ожидаемого результата. Не включайте в один профиль загрузку, несколько кликов и длинный период ожидания. Чистое воспроизведение не делает измерение идеальным, но уменьшает число несвязанных событий.
\nПосле записи сначала найдите пользовательский шаг, а не самый длинный прямоугольник. Выделите interaction и проверьте, какие вызовы находятся под ней на main thread. Затем сопоставьте их с обработчиком. Скриншот кадра помогает увидеть, что видел пользователь, но не доказывает причину. Summary показывает разбиение работы, а flame chart — порядок событий и стек вызовов. Ни один из этих видов не заменяет сравнение DOM before/after.
\nЕсли подозрение связано со стилями или геометрией, ограничьте вопрос. «Почему layout длинный?» слишком широко. «Какой вызов прочитал геометрию после записи класса в этом обработчике?» уже можно проверить. Если выбранный range не содержит такого обработчика, остановитесь: trace и код не связаны. Если mutation не даёт ожидаемый DOM-result, остановитесь ещё раньше.
\nУ первой правки должны быть четыре поля: один owner, ожидаемый DOM after, способ проверить trace и путь rollback. Например, временно вынести один geometry read из обработчика, локально выключить визуальный эффект через feature toggle или изменить один selector. Не меняйте одновременно рендер списка, кеш, анимацию и порядок сетевых запросов. Иначе новая запись не скажет, какое изменение повлияло на результат.
\nПосле правки повторите тот же сценарий в той же среде. Проверьте две вещи: карточка по-прежнему получает is-ready, а выбранный участок записи изменился так, как предполагала гипотеза. Если DOM сохранился, но trace не подтверждает гипотезу, это не провал. Это отрицательный результат: выберите другой owner или верните код. Если DOM сломался, rollback должен вернуть исходное поведение, а не просто удалить строку из diff.
const baseClassName = 'card is-pending';\n\nfunction applyForward(card) {\n card.className = 'card is-ready';\n}\n\nfunction applyRollback(card) {\n card.className = baseClassName;\n}\n\nfunction assertResult(card, expected) {\n if (card.className !== expected) {\n throw new Error(`unexpected DOM result: ${card.className}`);\n }\n}\nЭтот пример проверяет только наблюдаемое состояние. Он не измеряет FPS, не моделирует планировщик и не доказывает отсутствие layout. Production-решение требует реальной записи и подходящего набора пользовательских сценариев. Учебный код полезен как каркас вопроса, но не как evidence.
\nРазбор Performance panel относится к конкретной среде. Браузер, версия DevTools, устройство, throttling, размер viewport, расширения, cache, данные и состояние страницы меняют запись. Даже повтор одного сценария может дать другой порядок и длительность работы. Поэтому нельзя переносить вывод из Chrome на Firefox или Safari без отдельной проверки и нельзя считать локальную запись полевым измерением пользовательского опыта.
\nНазвания этапов в trace не образуют универсальный авторский pipeline. HTML Standard описывает поведение user agent, а DevTools показывает представление записи конкретного инструмента. requestAnimationFrame не является гарантией, что весь нужный rendering завершится до следующей строки. CSS-анимация, compositor, изображения, шрифты, layout containment, сеть и расширения могут изменить картину.
Не объявляйте улучшением уменьшение одного участка без функциональной проверки. Работа может переместиться в другой callback, появиться при другом viewport или проявиться только на слабом устройстве. Если после правки исчезла карточка, сообщение или error state, более короткая timeline ничего не доказывает. Если rollback возвращает только класс, но не возвращает сетевой или серверный результат, это локальная отмена, а не полный rollback операции.
\nИсследование готово, когда другой инженер может повторить один сценарий в указанной среде, увидеть DOM before/after, открыть выбранный range и найти связь с конкретной mutation. Для правки записаны owner, ожидаемый результат, наблюдаемый эффект и rollback. Успешная ветка сохраняет функциональный DOM-result. Ошибка и no-op проходят отдельно и не выдаются за успешное изменение. Если этих артефактов нет, формулировка «ускорили рендер» остаётся предположением.
\nМинимальный итог можно проверить четырьмя вопросами: что сделал пользователь; какой DOM-результат изменился; какой код-владелец связан с range; что произошло после обратимой правки. На каждый вопрос должна быть ссылка на повторяемый шаг или сохранённую запись. Числа из учебного примера, цвет блока в trace и субъективное ощущение плавности не заменяют эту проверку.
\nПосле добавления большого списка простая анимация начинает дёргаться. В панели Performance виден широкий участок главного потока с JavaScript, style, layout и paint. Если назвать весь участок «медленным рендером», команда может удалить нужную мутацию, перенести работу в другой callback или оптимизировать не тот экран. Цена ошибки — потерянный визуальный результат, новый дефект вместо ускорения и повторное расследование после релиза.
\nРабочий порядок начинается с наблюдаемого результата. Сначала зафиксируйте, что изменилось в DOM. Затем свяжите одно действие пользователя с узким диапазоном записи. После этого проверьте одну гипотезу маленькой обратимой правкой. Обсуждать эффект можно только после новой записи, в которой ожидаемый DOM-результат сохранился.
\nБраузер не получает от приложения готовую команду «сделай layout». Код меняет DOM, CSS-классы, inline-стили, текст или геометрию. В стандарте обновление отображения описано как абстрактный алгоритм пользовательского агента; конкретная реализация и запись зависят от браузера. Поэтому слова style, layout, paint и composite помогают сформулировать вопрос, но сами по себе не указывают владельца.
Владелец находится в коде и в контракте результата. Для примера контракт выглядит так: пользователь отправляет форму один раз; целевая карточка остаётся той же; className меняется с card is-pending на card is-ready; при ошибке класс не меняется; откат возвращает исходное состояние. Если запись стала короче, но класс больше не меняется, оптимизация не прошла функциональную проверку.
function onSave(card, response) {\n const before = card.className;\n\n if (!response.ok) {\n card.className = before;\n return { result: 'error', before, after: card.className };\n }\n\n const after = 'card is-ready';\n card.className = after;\n return { result: 'ready', before, after: card.className };\n}\n\nfunction rollback(card, before) {\n card.className = before;\n}\nКод учебный. Он не измеряет длительность, не вызывает настоящий сетевой запрос и не доказывает, что браузер нарисовал карточку в конкретном кадре. Его задача — показать границу наблюдения: до оптимизации известны before и after, а у изменения есть явная отмена. В рабочем компоненте тот же контракт нужно связать с реальным обработчиком, состоянием ошибки и снимком DOM.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Кнопка отвечает с задержкой | В запись попали соседние таймеры, сеть или расширение | Повторить одну отправку формы на чистом экране и найти соответствующее действие | Отделить диапазон действия от соседних событий |
| Запись широкая, причина неясна | Ищут общий ярлык вместо связи с DOM-результатом | Сравнить целевой элемент, свойство, исходное и итоговое состояния | Вернуться к изменению и назвать одного владельца |
| После правки график спокойнее, класс не меняется | Удалили функциональную мутацию вместе с лишней работой | Проверить итоговое состояние DOM тем же сценарием | Откатить правку и разделить визуальный результат и оптимизацию |
| Появился layout, но действие не меняло геометрию | Проверяется не тот диапазон или другой код прочитал геометрию | Найти вызов чтения размеров и его стек | Проверить одно чтение геометрии, а не весь layout |
| Повторная запись даёт другой результат записи | Изменились CPU, область просмотра, кэш, данные или версия браузера | Зафиксировать среду и повторить сценарий несколько раз | Сравнивать только согласованные условия и сохранять запись |
| «Улучшение» нельзя отменить | Не определены старое состояние и путь отката | Вернуть исходное состояние отдельным запуском и проверить DOM | Сделать изменение локальным, переключаемым или отдельным коммитом |
В качестве учебного сценария возьмём форму сохранения карточки. До действия в DOM есть <article class=\"card is-pending\">. Пользователь нажимает кнопку. Обработчик отправляет данные и после успешного ответа добавляет is-ready. Чтение геометрии сразу после записи стиля — проверяемая гипотеза, а не объяснение исходного случая с анимацией и большим списком. Её нужно подтвердить стеком вызовов и новой записью.
function updateCard(card) {\n const before = card.className;\n card.classList.add('is-ready');\n const height = card.getBoundingClientRect().height;\n return { before, after: card.className, height };\n}\nТакой порядок может потребовать дополнительной работы по обновлению стилей и layout, но по одному фрагменту нельзя заключить, что именно он вызвал задержку. Возможно, размер уже был нужен другому коду. Возможно, запись попала в другой пользовательский сценарий. Проверка должна ответить на узкий вопрос: связан ли этот обработчик и это чтение с выбранным участком записи, и сохраняется ли is-ready после изменения?
Отрицательный путь обязателен. Если сервер вернул ошибку, карточка должна остаться is-pending, сообщение об ошибке должно остаться доступным пользователю, а запись нельзя объявлять успешным только потому, что в нём стало меньше событий. Если выбранный элемент уже имел is-ready, действие может оказаться пустым. Тогда новая запись не проверяет переход состояния: сначала нужно вернуть исходное состояние и повторить сценарий.
Начинайте запись на уже подготовленной странице. Выполните одно действие и остановите запись сразу после появления ожидаемого результата. Не включайте в один профиль загрузку, несколько кликов и длинный период ожидания. Чистое воспроизведение не делает измерение идеальным, но уменьшает число несвязанных событий.
\nПосле записи сначала найдите пользовательский шаг, а не самый длинный прямоугольник. В Chrome DevTools этот шаг виден на дорожке Interactions, а выбранное событие на дорожке Main раскрывает длительность, стек вызовов и ссылку на строку исходника. Затем сопоставьте их с обработчиком. Скриншот кадра помогает увидеть, что видел пользователь, но не доказывает причину. Вкладка Summary показывает разбиение работы, а flame chart — порядок событий и стек вызовов. Ни один из этих видов не заменяет сравнение исходного и итогового DOM.
\nЕсли подозрение связано со стилями или геометрией, ограничьте вопрос. «Почему layout длинный?» слишком широко. «Какой вызов прочитал геометрию после записи класса в этом обработчике?» уже можно проверить. Если выбранный диапазон не содержит такого обработчика, остановитесь: запись и код не связаны. Если изменение не даёт ожидаемый результат в DOM, остановитесь ещё раньше.
\nУ первой правки должны быть четыре поля: один владелец, ожидаемое итоговое состояние DOM, способ проверить запись и путь отката. Например, временно вынести одно чтение геометрии из обработчика, локально выключить визуальный эффект через переключатель или изменить один селектор. Не меняйте одновременно рендер списка, кэш, анимацию и порядок сетевых запросов. Иначе новая запись не скажет, какое изменение повлияло на результат.
\nПосле правки повторите тот же сценарий в той же среде. Проверьте две вещи: карточка по-прежнему получает is-ready, а выбранный участок записи изменился так, как предполагала гипотеза. Если DOM сохранился, но запись не подтверждает гипотезу, это отрицательный результат: выберите другого владельца или верните код. Если DOM сломался, откат должен вернуть исходное поведение, а не просто удалить строку из diff.
const baseClassName = 'card is-pending';\n\nfunction applyForward(card) {\n card.className = 'card is-ready';\n}\n\nfunction applyRollback(card) {\n card.className = baseClassName;\n}\n\nfunction assertResult(card, expected) {\n if (card.className !== expected) {\n throw new Error(`unexpected DOM result: ${card.className}`);\n }\n}\nЭтот пример проверяет только наблюдаемое состояние. Он не измеряет FPS, не моделирует планировщик и не доказывает отсутствие layout. Решение для рабочего кода требует реальной записи и подходящего набора пользовательских сценариев. Учебный код полезен как каркас вопроса, но не заменяет доказательство.
\nРазбор Performance panel относится к конкретной среде. Браузер, версия DevTools, устройство, ограничение CPU, размер области просмотра, расширения, кэш, данные и состояние страницы меняют запись. Даже повтор одного сценария может дать другой порядок и длительность работы. Поэтому нельзя переносить вывод из Chrome на Firefox или Safari без отдельной проверки и нельзя считать локальную запись полевым измерением пользовательского опыта.
\nНазвания этапов в записи не образуют универсальную последовательность. HTML Standard описывает абстрактное поведение пользовательского агента, а DevTools показывает представление конкретного инструмента. requestAnimationFrame не является гарантией, что всё необходимое обновление отображения завершится до следующей строки. CSS-анимация, compositor, изображения, шрифты и изоляция layout, сеть и расширения могут изменить картину.
Не объявляйте улучшением уменьшение одного участка без функциональной проверки. Работа может переместиться в другой callback, появиться при другом viewport или проявиться только на слабом устройстве. Если после правки исчезла карточка, сообщение или состояние ошибки, более короткая временная шкала ничего не доказывает. Если откат возвращает только класс, но не возвращает сетевой или серверный результат, это локальная отмена, а не полный откат операции.
\nИсследование готово, когда другой инженер может повторить один сценарий в указанной среде, увидеть исходное и итоговое состояние DOM, открыть выбранный диапазон и найти связь с конкретным изменением. Для правки записаны владелец, ожидаемый результат, наблюдаемый эффект и откат. Успешная ветка сохраняет функциональный результат в DOM. Ошибка и пустое действие проходят отдельно и не выдаются за успешное изменение. Если этих артефактов нет, формулировка «ускорили рендер» остаётся предположением.
\nМинимальный итог можно проверить четырьмя вопросами: что сделал пользователь; какой DOM-результат изменился; какой владелец кода связан с диапазоном; что произошло после обратимой правки. На каждый вопрос должна быть ссылка на повторяемый шаг или сохранённую запись. Числа из учебного примера, цвет блока в записи и субъективное ощущение плавности не заменяют эту проверку.
\n