{ "index": 210, "slug": "editorial-2022-03-practice-browser-rendering", "title": "Медленный рендер: как связать DOM-изменение с причиной в кадре", "excerpt": "Если интерфейс дёргается после одного действия, не называйте виновником весь браузер. Зафиксируйте DOM-изменение, отделите JavaScript от работы конвейера отрисовки и проверьте гипотезу одной записью Performance panel.", "contentHtml": "
После нажатия «Сохранить» карточка должна сменить состояние, но экран на мгновение замирает. На быстром компьютере задержка почти незаметна, а на слабом ноутбуке превращается в рывок. Команда видит слово «медленный рендер» и начинает менять всё сразу: переносит обработчик, добавляет 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, которые используются в примере.