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 и обвиняет фреймворк. Цена такой ошибки — потраченные часы и изменённый интерфейс без доказательства, что причина исчезла.
Рабочий тезис проще: исследуйте одно пользовательское действие и один наблюдаемый результат DOM. Сначала зафиксируйте, что сделал JavaScript. Затем проверьте, какая работа последовала за этим изменением в конкретной записи. Слова style, layout, paint и composite удобны как вопросы к записи. Они не гарантируют одинаковый внутренний порядок во всех браузерах и не заменяют trace.
Возьмём карточку заказа с кнопкой save-card. До клика у неё класс card is-pending. После успешного ответа интерфейс должен получить card is-ready. Сетевой запрос в этой статье не является предметом измерения. Нас интересует момент, когда обработчик применяет новый класс, и работа, которую браузер выполняет, чтобы показать результат.
У такой задачи есть проверяемая граница: один клик, один target, одно свойство и два состояния. Не «страница тормозит», а «после клика на save-card меняется className с X на Y». Граница не ускоряет страницу сама. Она убирает лишние события из расследования и позволяет повторить тот же сценарий после правки.
JavaScript меняет состояние документа. Например, обработчик записывает новое значение className. После этого user agent может пересчитать применимые стили, геометрию и визуальное представление. Часть работы может быть объединена, отложена или выполнена иначе. Приложение не получает универсальный контракт «после каждой записи всегда будут пять событий».
Для диагностики полезна учебная цепочка. js-mutation означает изменение DOM. style задаёт вопрос о применимых стилях. layout — вопрос о геометрии. paint — вопрос об обновлении визуального представления. composite — вопрос о сборке результата. Это карта рассуждения, а не названия, которые нужно буквально искать в каждом trace.
Модель помогает увидеть и отрицательный путь. Если обработчик записал тот же класс, новый visual result мог не появиться. Если выбранный элемент не меняется, длинный участок записи может относиться к таймеру, соседней анимации или расширению браузера. Если после правки исчезла задержка, но исчез и обязательный класс is-ready, это не исправление. Функциональный результат и производительность нужно проверять вместе.
Ниже — учебный код. Он показывает границу действия и ставит метки, чтобы связать запись с обработчиком. Он не обещает конкретное время, FPS, INP или порядок внутренних событий браузера.
\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 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| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| После клика интерфейс отвечает рывком | В одну проблему смешаны handler, layout и соседние события | Записать один клик и выделить его диапазон | Сузить запись до interaction и одного target |
| Длинный участок есть, но владелец неясен | Не зафиксированы before и after | Проверить класс в Elements и место записи в коде | Обновить контракт действия или остановить исследование |
| После изменения CSS стало «быстрее» | Исчез визуальный результат, а не причина | Сверить className, геометрию и screenshot | Откатить правку и проверить другой участок |
| В модели нет новой работы | from и to совпали либо выбран не тот элемент | Проверить no-op и повторить действие с чистым состоянием | Не искать layout в записи без новой мутации |
| Стадии trace отличаются от схемы | Учебные labels приняли за API браузера | Сверить документацию версии и фактические события | Описать наблюдаемую работу, не переименовывать её насильно |
save-card экран отвечает неровно, класс должен стать card is-ready».Если DOM после клика не соответствует контракту, остановитесь. Вы исследуете другой путь. Если метка JavaScript есть, а ожидаемой работы после неё нет, не делайте вывод «рендер бесплатный»: работу могли объединить или перенести. Если запись показывает длинный участок вне выбранной interaction, не приписывайте его кнопке. Сначала повторите сценарий с чистым состоянием.
\nНе отключайте проверку стилей, не удаляйте обязательный визуальный результат и не добавляйте таймер как доказательство. requestAnimationFrame может помочь синхронизировать учебный эксперимент с обновлением кадра, но он не превращает callback в измерение причины. PerformanceObserver сообщает о поддерживаемых performance entries; он также не раскрывает весь внутренний pipeline и не заменяет запись DevTools.
Стоимость работы зависит от DOM, CSS, размера обновляемой области, устройства, браузера и состояния страницы. Один trace не описывает все устройства. Chrome DevTools показывает собственные представления и меняет интерфейс между версиями. Firefox и Safari могут группировать или называть события иначе. Даже в одном браузере кеш, шрифты, расширения, throttling и фоновые задачи меняют результат.
\nУчебный пример с пятью labels и synthetic units нужен только для проверки порядка рассуждения. Synthetic units не являются миллисекундами, CPU time, FPS, LCP, INP или production-данными. В статье нет утверждения, что конкретная правка ускорила реальный продукт. Реальный вывод появляется только после повторяемой записи на нужном сценарии и подтверждения, что функциональный результат сохранился.
\nДиагностика готова, если выполнены четыре условия: записано одно действие; DOM before и after совпадают с контрактом; выбранный участок trace связан с этим действием и описан фактическими, а не учебными названиями; после обратимой правки повторная запись сравнима с исходной, а карточка сохраняет состояние is-ready. Если хотя бы одно условие не выполнено, результатом является новая гипотеза, а не заявление «рендер исправлен».
После нажатия «Сохранить» карточка должна сменить состояние, но экран на мгновение замирает. На быстром компьютере задержка почти незаметна, а на слабом ноутбуке превращается в рывок. Команда видит слово «медленный рендер» и начинает менять всё сразу: переносит обработчик, добавляет debounce, убирает CSS и обвиняет фреймворк. Цена такой ошибки — часы работы и изменённый интерфейс без доказательства, что причина исчезла.
Разбор начинается с более узкого вопроса: какое действие сделал пользователь, какой DOM-результат должен появиться и какая работа попала в тот же участок записи? Сначала фиксируем код и состояние до/после, затем смотрим trace (запись работы браузера) в Performance panel. Слова style, layout, paint и composite помогают задать вопрос к записи, но не образуют универсальный порядок для всех браузеров.
Возьмём кнопку save-card, которая после успешного ответа должна получить класс card is-ready. До клика её состояние — card is-pending. Сеть в этом примере не измеряется: считаем, что ответ уже получен и исследуем только обработчик, DOM-изменение и последующую работу браузера.
Граница расследования состоит из одного клика, одного target (элемента-цели), одного свойства и двух значений. Формулировка «страница тормозит» ничего не связывает с кодом. Формулировка «после клика на save-card свойство className меняется с card is-pending на card is-ready» уже допускает повторную проверку.
JavaScript меняет состояние документа. Когда обработчик присваивает новый класс, браузеру может понадобиться пересчитать применимые стили и layout — геометрию элементов. Затем движок обновляет визуальное представление; конкретная реализация может объединить, отложить или пропустить часть работы. Поэтому приложение не получает контракт «после каждой записи будут ровно пять событий».
\nДля разговора с коллегой удобна рабочая карта: mutation — изменение DOM; style — пересчёт применимых стилей; layout — вычисление геометрии; paint — подготовка визуальных пикселей; composite — сборка слоёв. Это не обязательные имена событий в trace. Если запись показывает другой набор или порядок, описываем то, что действительно наблюдаем, и сверяем его с версией браузера.
\nОтдельный кандидат на причину — чтение геометрии сразу после записи. Например, после изменения класса код читает offsetWidth. Это свойство возвращает layout-ширину элемента, а в Chrome чтение после инвалидирования стилей может потребовать немедленного layout. Такая гипотеза проверяется trace, а не самим присутствием свойства в коде: если стили не изменились или элемент не участвует в нужной геометрии, вывод будет другим.
Ниже приведён самодостаточный фрагмент для страницы, где кнопка является карточкой. Сначала поместите разметку в HTML, затем выполните JavaScript после появления кнопки в документе. Строка с offsetWidth оставлена намеренно: она позволяет проверить гипотезу о чтении геометрии после записи. В рабочем коде не добавляйте такое чтение без причины. После первого клика состояние станет card is-ready; для повторного прогона обновите страницу.
<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.
| Симптом | Рабочая гипотеза | Проверка | Действие |
|---|---|---|---|
| После клика интерфейс отвечает рывком | В один вывод смешаны обработчик, layout и соседние события | Записать один клик и выделить его interaction | Сузить trace до действия и одного target |
| Длинный участок есть, но владелец неясен | Не зафиксированы DOM before и after | Проверить класс в Elements и место записи в коде | Сначала восстановить контракт, потом искать причину |
| После чтения геометрии появился Layout | Запись класса инвалидировала стили, а чтение потребовало размер | Сопоставить вызов offsetWidth с выбранным событием | Разнести запись и чтение либо убрать ненужное чтение; подтвердить повторной записью |
| После CSS-правки стало «быстрее» | Исчез обязательный визуальный результат | Сверить класс, геометрию и screenshot | Не принимать сокращённый trace без функциональной проверки |
| В записи нет новой работы | Значения from и to совпали либо выбран не тот элемент | Проверить no-op и чистое исходное состояние | Не искать layout там, где не было новой mutation |
| Стадии trace отличаются от схемы | Учебные labels приняли за API браузера | Сверить документацию версии и фактические события | Описать наблюдаемую работу без принудительного переименования |
save-card интерфейс отвечает неровно, класс должен стать card is-ready».offsetWidth, а не общее утверждение «рендер дорогой».Если DOM после клика не соответствует контракту, остановитесь: вы исследуете не тот путь. Если метка JavaScript есть, а ожидаемого layout или paint в выбранном диапазоне нет, это не доказывает, что рендер «бесплатный». Работа могла быть объединена или перенесена. Если длинный участок лежит вне выбранной interaction, не приписывайте его кнопке без связи с кодом.
\nНе отключайте проверку стилей, не удаляйте обязательный визуальный результат и не добавляйте таймер как доказательство. requestAnimationFrame может помочь привязать учебный шаг к очередной возможности отрисовки, но сам callback не объясняет причину задержки. PerformanceObserver наблюдает поддерживаемые performance entries; он не раскрывает весь внутренний pipeline и не заменяет trace DevTools.
Запись относится к конкретной среде. Версия браузера и DevTools, устройство, viewport, throttling, cache, шрифты, расширения, размер DOM и фоновые задачи меняют длительность и состав работы. Chrome DevTools показывает собственное представление trace; Firefox и Safari могут группировать или называть события иначе. Один локальный прогон не описывает все устройства и не является полевым измерением пользовательского опыта.
\nМетки на рисунке и числа в учебной модели не являются миллисекундами, CPU time, FPS, LCP, INP или production-данными. Не переносите результат «стало быстрее» из одного trace на весь продукт. Улучшение принимается только вместе с сохранённым функциональным результатом и повторной записью на нужном сценарии.
\nДиагностика готова, когда другой инженер может повторить один сценарий в указанной среде, увидеть DOM before и after, найти обработчик и открыть связанный диапазон trace. Для правки записаны одна гипотеза, ожидаемый эффект и rollback. Успешная ветка сохраняет card is-ready, ошибка и no-op проверяются отдельно. Если хотя бы одного артефакта нет, итогом остаётся новая гипотеза, а не заявление «рендер исправлен».
PerformanceMark и измерения PerformanceMeasure, которые используются в примере.