From 699ed741223bf27caec1bbfb723f4bcc66def4ac Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 20:01:14 +0300 Subject: [PATCH] =?UTF-8?q?editorial-208:=20=D1=83=D0=BB=D1=83=D1=87=D1=88?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=82=D1=8C=D1=8E=20?= =?UTF-8?q?=D0=BE=20=D0=B4=D0=B8=D0=B0=D0=B3=D0=BD=D0=BE=D1=81=D1=82=D0=B8?= =?UTF-8?q?=D0=BA=D0=B5=20=D0=B1=D1=80=D0=B0=D1=83=D0=B7=D0=B5=D1=80=D0=BD?= =?UTF-8?q?=D0=BE=D0=B3=D0=BE=20=D1=80=D0=B5=D0=BD=D0=B4=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- editorial/agent-rewrites/208.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 или оптимизировать не тот экран. Цена ошибки — потерянный визуальный результат, новый дефект вместо ускорения и повторное расследование после релиза.

\n

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

\n

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

\n

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

\n

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

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

\n

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

\n
Диагностика одного тяжёлого кадра
СимптомПричинаПроверкаДействие
Кнопка отвечает с задержкойВ запись попали соседние 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
\n

Пример: одна interaction и один DOM-result

\n

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

\n
function updateCard(card) {\n  card.classList.add('is-ready');\n  const height = card.getBoundingClientRect().height;\n  logLayoutDependentValue(height);\n}
\n

Такой порядок может потребовать дополнительной работы по обновлению стилей и layout, но по одному фрагменту нельзя заключить, что именно он вызвал задержку. Возможно, размер уже был нужен другому коду. Возможно, запись попала в другой пользовательский сценарий. Проверка должна ответить на узкий вопрос: связан ли этот обработчик и этот read с выбранным участком записи, и сохраняется ли is-ready после изменения?

\n

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

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

Как читать Performance panel

\n

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

\n

После записи сначала найдите пользовательский шаг, а не самый длинный прямоугольник. Выделите interaction и проверьте, какие вызовы находятся под ней на main thread. Затем сопоставьте их с обработчиком. Скриншот кадра помогает увидеть, что видел пользователь, но не доказывает причину. Summary показывает разбиение работы, а flame chart — порядок событий и стек вызовов. Ни один из этих видов не заменяет сравнение DOM before/after.

\n

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

\n

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

\n

У первой правки должны быть четыре поля: один owner, ожидаемый DOM after, способ проверить trace и путь rollback. Например, временно вынести один geometry read из обработчика, локально выключить визуальный эффект через feature toggle или изменить один selector. Не меняйте одновременно рендер списка, кеш, анимацию и порядок сетевых запросов. Иначе новая запись не скажет, какое изменение повлияло на результат.

\n

После правки повторите тот же сценарий в той же среде. Проверьте две вещи: карточка по-прежнему получает is-ready, а выбранный участок записи изменился так, как предполагала гипотеза. Если DOM сохранился, но trace не подтверждает гипотезу, это не провал. Это отрицательный результат: выберите другой owner или верните код. Если DOM сломался, rollback должен вернуть исходное поведение, а не просто удалить строку из 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. Production-решение требует реальной записи и подходящего набора пользовательских сценариев. Учебный код полезен как каркас вопроса, но не как evidence.

\n

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

\n
  1. Опишите симптом. Запишите действие, видимый сбой и цену ошибки. Например: «после submit карточка появляется с задержкой, а при ошибке состояние остаётся неясным».
  2. Зафиксируйте контракт. Сохраните target, property, DOM before, DOM after для успеха и DOM after для ошибки. Укажите, что считается no-op.
  3. Подготовьте среду. Зафиксируйте браузер, viewport, CPU throttling, данные, cache и наличие расширений. Снимите запись на чистом экране.
  4. Запишите одну interaction. Выполните один пользовательский шаг. Остановите запись сразу после visual result. Не смешивайте несколько гипотез.
  5. Свяжите trace с кодом. Найдите обработчик, mutation и выбранный range. Если связи нет, не называйте причину.
  6. Сформулируйте гипотезу. Назовите одного owner и один проверяемый вопрос: например, влияет ли конкретный geometry read на работу после mutation.
  7. Внесите одну обратимую правку. Измените небольшой участок, сохраните способ rollback и не меняйте контракт результата.
  8. Повторите запись. Сравните согласованные диапазоны, но не переносите synthetic units или локальные числа на production.
  9. Проверьте обе ветки. Успешный путь должен дать ожидаемый DOM after. Ошибка и no-op не должны маскироваться под успешный переход.
  10. Зафиксируйте вывод. Оставьте среду, шаг, before/after, ссылку на код, range, гипотезу, результат повторной записи и способ rollback.
\n

Ограничения

\n

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

\n

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

\n

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

\n

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

\n

Исследование готово, когда другой инженер может повторить один сценарий в указанной среде, увидеть DOM before/after, открыть выбранный range и найти связь с конкретной mutation. Для правки записаны owner, ожидаемый результат, наблюдаемый эффект и rollback. Успешная ветка сохраняет функциональный DOM-result. Ошибка и no-op проходят отдельно и не выдаются за успешное изменение. Если этих артефактов нет, формулировка «ускорили рендер» остаётся предположением.

\n

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

\n

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

" + "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

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

" }