diff --git a/editorial/agent-rewrites/210.json b/editorial/agent-rewrites/210.json index 34a9627..5874d39 100644 --- a/editorial/agent-rewrites/210.json +++ b/editorial/agent-rewrites/210.json @@ -2,6 +2,6 @@ "index": 210, "slug": "editorial-2022-03-practice-browser-rendering", "title": "Медленный рендер: как связать DOM-изменение с причиной в кадре", - "excerpt": "Если интерфейс дёргается после одного действия, не называйте виновником весь браузер. Зафиксируйте DOM-изменение, отделите JavaScript от работы rendering pipeline и проверьте гипотезу одной записью Performance panel.", - "contentHtml": "

После нажатия «Сохранить» карточка должна сменить состояние, но экран на мгновение замирает. Иногда задержка заметна только на слабом ноутбуке. Иногда она появляется после добавления списка, тени или анимации. Команда видит слово «медленный рендер» и начинает менять всё сразу: переносит обработчик, добавляет debounce, убирает CSS и обвиняет фреймворк. Цена такой ошибки — потраченные часы и изменённый интерфейс без доказательства, что причина исчезла.

\n

Рабочий тезис проще: исследуйте одно пользовательское действие и один наблюдаемый результат DOM. Сначала зафиксируйте, что сделал JavaScript. Затем проверьте, какая работа последовала за этим изменением в конкретной записи. Слова style, layout, paint и composite удобны как вопросы к записи. Они не гарантируют одинаковый внутренний порядок во всех браузерах и не заменяют trace.

\n

Симптом и граница задачи

\n

Возьмём карточку заказа с кнопкой save-card. До клика у неё класс card is-pending. После успешного ответа интерфейс должен получить card is-ready. Сетевой запрос в этой статье не является предметом измерения. Нас интересует момент, когда обработчик применяет новый класс, и работа, которую браузер выполняет, чтобы показать результат.

\n

У такой задачи есть проверяемая граница: один клик, один target, одно свойство и два состояния. Не «страница тормозит», а «после клика на save-card меняется className с X на Y». Граница не ускоряет страницу сама. Она убирает лишние события из расследования и позволяет повторить тот же сценарий после правки.

\n
\"Учебная
Учебная timeline помогает задать порядок вопросов. Она не измеряет миллисекунды и не является записью Performance panel.
\n

Механизм: от записи JavaScript к кадру

\n

JavaScript меняет состояние документа. Например, обработчик записывает новое значение className. После этого user agent может пересчитать применимые стили, геометрию и визуальное представление. Часть работы может быть объединена, отложена или выполнена иначе. Приложение не получает универсальный контракт «после каждой записи всегда будут пять событий».

\n

Для диагностики полезна учебная цепочка. js-mutation означает изменение DOM. style задаёт вопрос о применимых стилях. layout — вопрос о геометрии. paint — вопрос об обновлении визуального представления. composite — вопрос о сборке результата. Это карта рассуждения, а не названия, которые нужно буквально искать в каждом trace.

\n

Модель помогает увидеть и отрицательный путь. Если обработчик записал тот же класс, новый visual result мог не появиться. Если выбранный элемент не меняется, длинный участок записи может относиться к таймеру, соседней анимации или расширению браузера. Если после правки исчезла задержка, но исчез и обязательный класс is-ready, это не исправление. Функциональный результат и производительность нужно проверять вместе.

\n

Минимальный пример

\n

Ниже — учебный код. Он показывает границу действия и ставит метки, чтобы связать запись с обработчиком. Он не обещает конкретное время, FPS, INP или порядок внутренних событий браузера.

\n
const card = document.querySelector('#save-card');\n\nfunction setCardState(from, to) {\n  if (!card || card.className !== from) {\n    return false;\n  }\n\n  performance.mark('card-state-before');\n  card.className = to;\n  performance.mark('card-state-after');\n  performance.measure(\n    'card-state-change',\n    'card-state-before',\n    'card-state-after'\n  );\n  return true;\n}\n\nbutton.addEventListener('click', () => {\n  setCardState('card is-pending', 'card is-ready');\n});
\n

Метки ограничивают время JavaScript между двумя точками. Они не доказывают длительность layout или paint. Для этого нужна запись исполнения в выбранной версии браузера. В реальном проекте сохраните также состояние до и после. Иначе вы можете сравнить две записи, в которых обработчик сделал разные вещи.

\n

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

\n
Рабочая таблица для одного DOM-изменения
СимптомПричинаПроверкаДействие
После клика интерфейс отвечает рывкомВ одну проблему смешаны handler, layout и соседние событияЗаписать один клик и выделить его диапазонСузить запись до interaction и одного target
Длинный участок есть, но владелец неясенНе зафиксированы before и afterПроверить класс в Elements и место записи в кодеОбновить контракт действия или остановить исследование
После изменения CSS стало «быстрее»Исчез визуальный результат, а не причинаСверить className, геометрию и screenshotОткатить правку и проверить другой участок
В модели нет новой работыfrom и to совпали либо выбран не тот элементПроверить no-op и повторить действие с чистым состояниемНе искать layout в записи без новой мутации
Стадии trace отличаются от схемыУчебные labels приняли за API браузераСверить документацию версии и фактические событияОписать наблюдаемую работу, не переименовывать её насильно
\n

Порядок проверки

\n
  1. Опишите симптом как действие и наблюдаемый эффект: «после клика на save-card экран отвечает неровно, класс должен стать card is-ready».
  2. Зафиксируйте target, свойство, значение до и значение после. Проверьте исходный DOM в Elements, а не только исходный код.
  3. Откройте страницу в согласованном состоянии. Остановите лишние действия, очистите состояние сценария и запишите только один клик в Performance panel.
  4. Найдите interaction и узкий диапазон вокруг неё. Сначала проверьте handler и DOM-result, затем рассматривайте связанные участки rendering.
  5. Выберите одну гипотезу: лишний geometry read, широкий selector, дорогая визуальная область или лишняя работа JavaScript. Не меняйте несколько причин за один эксперимент.
  6. Внесите обратимую правку. Сохраните ожидаемый DOM-result и опишите, какое наблюдение должно измениться, если гипотеза верна.
  7. Повторите тот же сценарий в той же среде. Сравните запись, функциональный результат и отрицательный путь: no-op, ошибка или неверный target.
  8. Сохраните trace или краткую ссылку на результат вместе с версией браузера и условиями запуска. Без этих условий цифры нельзя честно сравнивать.
\n

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

\n

Если DOM после клика не соответствует контракту, остановитесь. Вы исследуете другой путь. Если метка JavaScript есть, а ожидаемой работы после неё нет, не делайте вывод «рендер бесплатный»: работу могли объединить или перенести. Если запись показывает длинный участок вне выбранной interaction, не приписывайте его кнопке. Сначала повторите сценарий с чистым состоянием.

\n

Не отключайте проверку стилей, не удаляйте обязательный визуальный результат и не добавляйте таймер как доказательство. requestAnimationFrame может помочь синхронизировать учебный эксперимент с обновлением кадра, но он не превращает callback в измерение причины. PerformanceObserver сообщает о поддерживаемых performance entries; он также не раскрывает весь внутренний pipeline и не заменяет запись DevTools.

\n

Ограничения

\n

Стоимость работы зависит от DOM, CSS, размера обновляемой области, устройства, браузера и состояния страницы. Один trace не описывает все устройства. Chrome DevTools показывает собственные представления и меняет интерфейс между версиями. Firefox и Safari могут группировать или называть события иначе. Даже в одном браузере кеш, шрифты, расширения, throttling и фоновые задачи меняют результат.

\n

Учебный пример с пятью labels и synthetic units нужен только для проверки порядка рассуждения. Synthetic units не являются миллисекундами, CPU time, FPS, LCP, INP или production-данными. В статье нет утверждения, что конкретная правка ускорила реальный продукт. Реальный вывод появляется только после повторяемой записи на нужном сценарии и подтверждения, что функциональный результат сохранился.

\n

Критерий готовности

\n

Диагностика готова, если выполнены четыре условия: записано одно действие; DOM before и after совпадают с контрактом; выбранный участок trace связан с этим действием и описан фактическими, а не учебными названиями; после обратимой правки повторная запись сравнима с исходной, а карточка сохраняет состояние is-ready. Если хотя бы одно условие не выполнено, результатом является новая гипотеза, а не заявление «рендер исправлен».

\n

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

\n" + "excerpt": "Если интерфейс дёргается после одного действия, не называйте виновником весь браузер. Зафиксируйте DOM-изменение, отделите JavaScript от работы конвейера отрисовки и проверьте гипотезу одной записью Performance panel.", + "contentHtml": "

После нажатия «Сохранить» карточка должна сменить состояние, но экран на мгновение замирает. На быстром компьютере задержка почти незаметна, а на слабом ноутбуке превращается в рывок. Команда видит слово «медленный рендер» и начинает менять всё сразу: переносит обработчик, добавляет debounce, убирает CSS и обвиняет фреймворк. Цена такой ошибки — часы работы и изменённый интерфейс без доказательства, что причина исчезла.

\n

Разбор начинается с более узкого вопроса: какое действие сделал пользователь, какой DOM-результат должен появиться и какая работа попала в тот же участок записи? Сначала фиксируем код и состояние до/после, затем смотрим trace (запись работы браузера) в Performance panel. Слова style, layout, paint и composite помогают задать вопрос к записи, но не образуют универсальный порядок для всех браузеров.

\n

Симптом и граница задачи

\n

Возьмём кнопку save-card, которая после успешного ответа должна получить класс card is-ready. До клика её состояние — card is-pending. Сеть в этом примере не измеряется: считаем, что ответ уже получен и исследуем только обработчик, DOM-изменение и последующую работу браузера.

\n

Граница расследования состоит из одного клика, одного target (элемента-цели), одного свойства и двух значений. Формулировка «страница тормозит» ничего не связывает с кодом. Формулировка «после клика на save-card свойство className меняется с card is-pending на card is-ready» уже допускает повторную проверку.

\n
Учебная схема: клик меняет className карточки, после чего показаны условные вопросы о style, layout, paint и composite; synthetic units отделены от реального trace
Схема задаёт порядок вопросов к одному DOM-изменению. Synthetic units — условные единицы рисунка, а не миллисекунды и не запись Performance panel.
\n

Механизм: от JavaScript к обновлению экрана

\n

JavaScript меняет состояние документа. Когда обработчик присваивает новый класс, браузеру может понадобиться пересчитать применимые стили и layout — геометрию элементов. Затем движок обновляет визуальное представление; конкретная реализация может объединить, отложить или пропустить часть работы. Поэтому приложение не получает контракт «после каждой записи будут ровно пять событий».

\n

Для разговора с коллегой удобна рабочая карта: mutation — изменение DOM; style — пересчёт применимых стилей; layout — вычисление геометрии; paint — подготовка визуальных пикселей; composite — сборка слоёв. Это не обязательные имена событий в trace. Если запись показывает другой набор или порядок, описываем то, что действительно наблюдаем, и сверяем его с версией браузера.

\n

Отдельный кандидат на причину — чтение геометрии сразу после записи. Например, после изменения класса код читает offsetWidth. Это свойство возвращает layout-ширину элемента, а в Chrome чтение после инвалидирования стилей может потребовать немедленного layout. Такая гипотеза проверяется trace, а не самим присутствием свойства в коде: если стили не изменились или элемент не участвует в нужной геометрии, вывод будет другим.

\n

Минимальный воспроизводимый пример

\n

Ниже приведён самодостаточный фрагмент для страницы, где кнопка является карточкой. Сначала поместите разметку в HTML, затем выполните JavaScript после появления кнопки в документе. Строка с offsetWidth оставлена намеренно: она позволяет проверить гипотезу о чтении геометрии после записи. В рабочем коде не добавляйте такое чтение без причины. После первого клика состояние станет card is-ready; для повторного прогона обновите страницу.

\n
<button id='save-card' class='card is-pending' type='button'>Сохранить</button>\n\n// JavaScript\nconst card = document.querySelector('#save-card');\n\nfunction setCardState(from, to) {\n  if (!card || card.className !== from) {\n    return false;\n  }\n\n  performance.mark('card-state-before');\n  card.className = to;\n  const width = card.offsetWidth;\n  console.log('measured width:', width);\n  performance.mark('card-state-after');\n  performance.measure(\n    'card-state-change',\n    'card-state-before',\n    'card-state-after'\n  );\n  return true;\n}\n\ncard.addEventListener('click', () => {\n  setCardState('card is-pending', 'card is-ready');\n});
\n

Функциональный результат здесь проверяем отдельно: после клика в Elements должно быть class='card is-ready'. Метки User Timing показывают длительность собственного участка JavaScript и появляются в Performance panel. Они не измеряют весь layout или paint, поэтому запись должна включать сам клик и выбранный диапазон Main track.

\n

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

\n
Матрица первого расследования
СимптомРабочая гипотезаПроверкаДействие
После клика интерфейс отвечает рывкомВ один вывод смешаны обработчик, layout и соседние событияЗаписать один клик и выделить его interactionСузить trace до действия и одного target
Длинный участок есть, но владелец неясенНе зафиксированы DOM before и afterПроверить класс в Elements и место записи в кодеСначала восстановить контракт, потом искать причину
После чтения геометрии появился LayoutЗапись класса инвалидировала стили, а чтение потребовало размерСопоставить вызов offsetWidth с выбранным событиемРазнести запись и чтение либо убрать ненужное чтение; подтвердить повторной записью
После CSS-правки стало «быстрее»Исчез обязательный визуальный результатСверить класс, геометрию и screenshotНе принимать сокращённый trace без функциональной проверки
В записи нет новой работыЗначения from и to совпали либо выбран не тот элементПроверить no-op и чистое исходное состояниеНе искать layout там, где не было новой mutation
Стадии trace отличаются от схемыУчебные labels приняли за API браузераСверить документацию версии и фактические событияОписать наблюдаемую работу без принудительного переименования
\n

Порядок проверки в Performance panel

\n
  1. Опишите симптом как действие и видимый результат: «после клика на save-card интерфейс отвечает неровно, класс должен стать card is-ready».
  2. Зафиксируйте target, свойство, DOM before и DOM after. Отдельно опишите ожидаемое состояние при ошибке и no-op, когда переход не должен происходить.
  3. Подготовьте страницу: одинаковые данные, viewport, браузер, cache и настройки throttling. Уберите лишние клики и фоновые действия, но запишите условия запуска.
  4. Откройте Performance panel, включите запись runtime и выполните один клик. Остановите запись сразу после появления ожидаемого DOM-результата.
  5. Найдите interaction и узкий диапазон вокруг неё. В Main track проверьте обработчик, User Timing marks и связанные события, а не только самый длинный прямоугольник.
  6. Выберите одну гипотезу. Для geometry read это конкретный вызов offsetWidth, а не общее утверждение «рендер дорогой».
  7. Внесите одну обратимую правку: уберите чтение, разделите записи и чтения или измените один selector. Сохраните ожидаемый DOM-result и способ rollback.
  8. Повторите тот же сценарий в той же среде. Сравните выбранные диапазоны, DOM-result и отрицательный путь. Если trace не подтверждает гипотезу, верните правку и выберите следующий вопрос.
\n

Если гипотеза не подтверждается

\n

Если DOM после клика не соответствует контракту, остановитесь: вы исследуете не тот путь. Если метка JavaScript есть, а ожидаемого layout или paint в выбранном диапазоне нет, это не доказывает, что рендер «бесплатный». Работа могла быть объединена или перенесена. Если длинный участок лежит вне выбранной interaction, не приписывайте его кнопке без связи с кодом.

\n

Не отключайте проверку стилей, не удаляйте обязательный визуальный результат и не добавляйте таймер как доказательство. requestAnimationFrame может помочь привязать учебный шаг к очередной возможности отрисовки, но сам callback не объясняет причину задержки. PerformanceObserver наблюдает поддерживаемые performance entries; он не раскрывает весь внутренний pipeline и не заменяет trace DevTools.

\n

Ограничения измерения

\n

Запись относится к конкретной среде. Версия браузера и DevTools, устройство, viewport, throttling, cache, шрифты, расширения, размер DOM и фоновые задачи меняют длительность и состав работы. Chrome DevTools показывает собственное представление trace; Firefox и Safari могут группировать или называть события иначе. Один локальный прогон не описывает все устройства и не является полевым измерением пользовательского опыта.

\n

Метки на рисунке и числа в учебной модели не являются миллисекундами, CPU time, FPS, LCP, INP или production-данными. Не переносите результат «стало быстрее» из одного trace на весь продукт. Улучшение принимается только вместе с сохранённым функциональным результатом и повторной записью на нужном сценарии.

\n

Критерий готовности

\n

Диагностика готова, когда другой инженер может повторить один сценарий в указанной среде, увидеть DOM before и after, найти обработчик и открыть связанный диапазон trace. Для правки записаны одна гипотеза, ожидаемый эффект и rollback. Успешная ветка сохраняет card is-ready, ошибка и no-op проверяются отдельно. Если хотя бы одного артефакта нет, итогом остаётся новая гипотеза, а не заявление «рендер исправлен».

\n

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

\n" }