{ "index": 208, "slug": "editorial-2022-03-field-browser-rendering", "title": "Тяжёлый кадр в браузере: как найти причину и проверить обратимую правку", "excerpt": "Если интерфейс дёргается, широкий участок Performance trace ещё не объясняет причину. Разбираем один пользовательский шаг через DOM-результат, владельца изменения, узкий диапазон записи и rollback.", "contentHtml": "

После добавления большого списка простая анимация начинает дёргаться. В панели Performance виден широкий участок главного потока с JavaScript, style, layout и paint. Если назвать весь участок «медленным рендером», команда может удалить нужную мутацию, перенести работу в другой callback или оптимизировать не тот экран. Цена ошибки — потерянный визуальный результат, новый дефект вместо ускорения и повторное расследование после релиза.

\n

Рабочий порядок начинается с наблюдаемого результата. Сначала зафиксируйте, что изменилось в DOM. Затем свяжите одно действие пользователя с узким диапазоном записи. После этого проверьте одну гипотезу маленькой обратимой правкой. Обсуждать эффект можно только после новой записи, в которой ожидаемый DOM-результат сохранился.

\n

Механизм: результат важнее ярлыка в записи

\n

Браузер не получает от приложения готовую команду «сделай layout». Код меняет DOM, CSS-классы, inline-стили, текст или геометрию. В стандарте обновление отображения описано как абстрактный алгоритм пользовательского агента; конкретная реализация и запись зависят от браузера. Поэтому слова style, layout, paint и composite помогают сформулировать вопрос, но сами по себе не указывают владельца.

\n

Владелец находится в коде и в контракте результата. Для примера контракт выглядит так: пользователь отправляет форму один раз; целевая карточка остаётся той же; className меняется с card is-pending на card is-ready; при ошибке класс не меняется; откат возвращает исходное состояние. Если запись стала короче, но класс больше не меняется, оптимизация не прошла функциональную проверку.

\n
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.

\n

Симптом → причина → проверка → действие

\n
Диагностика одного тяжёлого кадра
СимптомПричинаПроверкаДействие
Кнопка отвечает с задержкойВ запись попали соседние таймеры, сеть или расширениеПовторить одну отправку формы на чистом экране и найти соответствующее действиеОтделить диапазон действия от соседних событий
Запись широкая, причина неяснаИщут общий ярлык вместо связи с DOM-результатомСравнить целевой элемент, свойство, исходное и итоговое состоянияВернуться к изменению и назвать одного владельца
После правки график спокойнее, класс не меняетсяУдалили функциональную мутацию вместе с лишней работойПроверить итоговое состояние DOM тем же сценариемОткатить правку и разделить визуальный результат и оптимизацию
Появился layout, но действие не меняло геометриюПроверяется не тот диапазон или другой код прочитал геометриюНайти вызов чтения размеров и его стекПроверить одно чтение геометрии, а не весь layout
Повторная запись даёт другой результат записиИзменились CPU, область просмотра, кэш, данные или версия браузераЗафиксировать среду и повторить сценарий несколько разСравнивать только согласованные условия и сохранять запись
«Улучшение» нельзя отменитьНе определены старое состояние и путь откатаВернуть исходное состояние отдельным запуском и проверить DOMСделать изменение локальным, переключаемым или отдельным коммитом
\n

Пример: одно действие и один результат в DOM

\n

В качестве учебного сценария возьмём форму сохранения карточки. До действия в DOM есть <article class=\"card is-pending\">. Пользователь нажимает кнопку. Обработчик отправляет данные и после успешного ответа добавляет is-ready. Чтение геометрии сразу после записи стиля — проверяемая гипотеза, а не объяснение исходного случая с анимацией и большим списком. Её нужно подтвердить стеком вызовов и новой записью.

\n
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 после изменения?

\n

Отрицательный путь обязателен. Если сервер вернул ошибку, карточка должна остаться is-pending, сообщение об ошибке должно остаться доступным пользователю, а запись нельзя объявлять успешным только потому, что в нём стало меньше событий. Если выбранный элемент уже имел is-ready, действие может оказаться пустым. Тогда новая запись не проверяет переход состояния: сначала нужно вернуть исходное состояние и повторить сценарий.

\n
\"Схема
Учебная схема связывает исходное и итоговое состояние DOM с одним пользовательским действием и одной гипотезой. Ветка пустого действия останавливает разбор, а откат возвращает исходный результат. Схема не является снимком DevTools trace и не содержит рабочих метрик.
\n

Как читать Performance panel

\n

Начинайте запись на уже подготовленной странице. Выполните одно действие и остановите запись сразу после появления ожидаемого результата. Не включайте в один профиль загрузку, несколько кликов и длинный период ожидания. Чистое воспроизведение не делает измерение идеальным, но уменьшает число несвязанных событий.

\n

После записи сначала найдите пользовательский шаг, а не самый длинный прямоугольник. В Chrome DevTools этот шаг виден на дорожке Interactions, а выбранное событие на дорожке Main раскрывает длительность, стек вызовов и ссылку на строку исходника. Затем сопоставьте их с обработчиком. Скриншот кадра помогает увидеть, что видел пользователь, но не доказывает причину. Вкладка Summary показывает разбиение работы, а flame chart — порядок событий и стек вызовов. Ни один из этих видов не заменяет сравнение исходного и итогового DOM.

\n

Если подозрение связано со стилями или геометрией, ограничьте вопрос. «Почему layout длинный?» слишком широко. «Какой вызов прочитал геометрию после записи класса в этом обработчике?» уже можно проверить. Если выбранный диапазон не содержит такого обработчика, остановитесь: запись и код не связаны. Если изменение не даёт ожидаемый результат в DOM, остановитесь ещё раньше.

\n

Обратимая правка

\n

У первой правки должны быть четыре поля: один владелец, ожидаемое итоговое состояние DOM, способ проверить запись и путь отката. Например, временно вынести одно чтение геометрии из обработчика, локально выключить визуальный эффект через переключатель или изменить один селектор. Не меняйте одновременно рендер списка, кэш, анимацию и порядок сетевых запросов. Иначе новая запись не скажет, какое изменение повлияло на результат.

\n

После правки повторите тот же сценарий в той же среде. Проверьте две вещи: карточка по-прежнему получает is-ready, а выбранный участок записи изменился так, как предполагала гипотеза. Если DOM сохранился, но запись не подтверждает гипотезу, это отрицательный результат: выберите другого владельца или верните код. Если DOM сломался, откат должен вернуть исходное поведение, а не просто удалить строку из diff.

\n
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

Порядок действий

\n
  1. Опишите симптом. Запишите действие, видимый сбой и цену ошибки. Например: «после отправки формы карточка появляется с задержкой, а при ошибке состояние остаётся неясным».
  2. Зафиксируйте контракт. Сохраните целевой элемент, свойство, исходное состояние DOM, итоговое состояние для успеха и состояние для ошибки. Укажите, что считается пустым действием.
  3. Подготовьте среду. Зафиксируйте браузер, область просмотра, ограничение CPU, данные, кэш и наличие расширений. Снимите запись на чистом экране.
  4. Запишите одно пользовательское действие. Выполните один шаг. Остановите запись сразу после видимого результата. Не смешивайте несколько гипотез.
  5. Свяжите запись с кодом. Найдите обработчик, изменение и выбранный диапазон. Если связи нет, не называйте причину.
  6. Сформулируйте гипотезу. Назовите одного владельца и один проверяемый вопрос: например, влияет ли конкретное чтение геометрии на работу после изменения.
  7. Внесите одну обратимую правку. Измените небольшой участок, сохраните способ отката и не меняйте контракт результата.
  8. Повторите запись. Сравните согласованные диапазоны, но не переносите условные локальные единицы или локальные числа на рабочий трафик.
  9. Проверьте обе ветки. Успешный путь должен дать ожидаемое итоговое состояние DOM. Ошибка и пустое действие не должны маскироваться под успешный переход.
  10. Зафиксируйте вывод. Оставьте среду, шаг, исходное и итоговое состояние, ссылку на код, диапазон, гипотезу, результат повторной записи и способ отката.
\n

Ограничения

\n

Разбор Performance panel относится к конкретной среде. Браузер, версия DevTools, устройство, ограничение CPU, размер области просмотра, расширения, кэш, данные и состояние страницы меняют запись. Даже повтор одного сценария может дать другой порядок и длительность работы. Поэтому нельзя переносить вывод из Chrome на Firefox или Safari без отдельной проверки и нельзя считать локальную запись полевым измерением пользовательского опыта.

\n

Названия этапов в записи не образуют универсальную последовательность. HTML Standard описывает абстрактное поведение пользовательского агента, а DevTools показывает представление конкретного инструмента. requestAnimationFrame не является гарантией, что всё необходимое обновление отображения завершится до следующей строки. CSS-анимация, compositor, изображения, шрифты и изоляция layout, сеть и расширения могут изменить картину.

\n

Не объявляйте улучшением уменьшение одного участка без функциональной проверки. Работа может переместиться в другой callback, появиться при другом viewport или проявиться только на слабом устройстве. Если после правки исчезла карточка, сообщение или состояние ошибки, более короткая временная шкала ничего не доказывает. Если откат возвращает только класс, но не возвращает сетевой или серверный результат, это локальная отмена, а не полный откат операции.

\n

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

\n

Исследование готово, когда другой инженер может повторить один сценарий в указанной среде, увидеть исходное и итоговое состояние DOM, открыть выбранный диапазон и найти связь с конкретным изменением. Для правки записаны владелец, ожидаемый результат, наблюдаемый эффект и откат. Успешная ветка сохраняет функциональный результат в DOM. Ошибка и пустое действие проходят отдельно и не выдаются за успешное изменение. Если этих артефактов нет, формулировка «ускорили рендер» остаётся предположением.

\n

Минимальный итог можно проверить четырьмя вопросами: что сделал пользователь; какой DOM-результат изменился; какой владелец кода связан с диапазоном; что произошло после обратимой правки. На каждый вопрос должна быть ссылка на повторяемый шаг или сохранённую запись. Числа из учебного примера, цвет блока в записи и субъективное ощущение плавности не заменяют эту проверку.

\n

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

" }