{ "index": 209, "slug": "editorial-2022-03-mechanism-browser-rendering", "title": "Как браузер рисует изменение интерфейса: модель и границы", "excerpt": "Разбираем путь от изменения DOM до видимого кадра: как найти forced layout в записи, отделить производительность от функционального результата и выбрать проверяемое изменение.", "contentHtml": "
Кнопка меняет класс карточки, но интерфейс отвечает с рывком. В Performance panel виден длинный участок работы, и команда сразу обвиняет JavaScript или CSS. Такой диагноз может убрать не задержку, а само состояние «готово». Пользователь увидит более короткую запись, но не получит результат действия.
\nДля расследования нужна причинная цепочка: кто изменил DOM, какие стили стали применимы, потребовалась ли новая геометрия, какая область перерисовалась и как браузер собрал кадр. Это инженерная модель, а не обещание одинакового набора событий во всех движках. Её задача — связать симптом с одной записью и одной проверяемой гипотезой.
\nРассмотрим учебный сценарий. После отправки формы обработчик заменяет состояние карточки с is-pending на is-ready. Вместе с классом меняются цвет и подпись. Если обработчик выбрал не тот элемент или класс не изменился, искать дорогую отрисовку рано: сначала сломан функциональный контракт.
После записи браузер должен определить применимые стили. Если изменение влияет на размеры или положение, движку может понадобиться layout — расчёт геометрии. Затем он подготавливает пиксели изменившейся области и собирает итоговые слои для вывода. В Chrome DevTools эти этапы видны через события и связанные записи, но конкретное отображение зависит от браузера, версии, устройства и настроек захвата.
\nПоэтому слова style, layout, paint и composite полезны как названия вопросов. Они не доказывают сами по себе, что один этап вызвал другой. Доказательство появляется, когда запись связывает обработчик, изменение DOM и наблюдаемый результат.
Предположим, на странице есть кнопка [data-ready-button], карточка [data-card] и подпись .card__status. Обработчик ниже меняет только состояние карточки и текст. Проверяемый результат — класс is-ready и слово «Готово» после одного клика.
const button = document.querySelector('[data-ready-button]');\nconst card = document.querySelector('[data-card]');\nconst status = card?.querySelector('.card__status');\n\nif (!(button instanceof HTMLButtonElement) ||\n !(card instanceof HTMLElement) ||\n !(status instanceof HTMLElement)) {\n throw new Error('Card controls are missing');\n}\n\nbutton.addEventListener('click', () => {\n const changed = card.classList.replace('is-pending', 'is-ready');\n if (!changed) return;\n status.textContent = 'Готово';\n});\nДо записи зафиксируйте class карточки и текст подписи. После записи проверьте оба значения в Elements или через консоль. Если classList.replace вернул false, старого класса не было, и причина находится в состоянии или селекторе, а не в стоимости paint.
В таком сценарии JavaScript запускает изменение, но не равен всей работе браузера. Он также не доказывает, что любое изменение класса приведёт к полному layout. Объём работы определяется стилями, деревом элементов и текущим состоянием страницы.
\nБраузер может отложить часть расчётов до удобной точки. Но чтение геометрии после записи стиля требует актуального значения. Если новое значение ещё не рассчитано, движку приходится завершить нужные style и layout прямо во время текущей задачи. В DevTools это часто описывают как forced synchronous layout или forced reflow.
\nfunction resizeCard() {\n card.style.width = '320px';\n const height = card.offsetHeight;\n card.style.height = `${height}px`;\n}\nЗдесь сначала записывается ширина, а затем читается offsetHeight. Если ширина меняет перенос строк, высота зависит от этой записи. Так чтение становится границей, на которой браузер обязан получить новую геометрию. Сам фрагмент не гарантирует одинаковую стоимость в каждом движке: её нужно увидеть в конкретной записи.
Безопасная оптимизация начинается с вопроса о результате. Если нужна высота до изменения, прочитайте её до записи и затем выполните записи вместе:
\nfunction keepCurrentHeight() {\n const currentHeight = card.offsetHeight;\n\n card.style.width = '320px';\n card.style.height = `${currentHeight}px`;\n}\nЭтот вариант подходит только для контракта «сохранить текущую высоту». Он не заменяет измерение новой высоты, если ширина должна изменить переносы и размер карточки. В последнем случае измерение после записи — часть задачи, а не автоматический дефект. Профиль нужен, чтобы понять, стала ли эта граница проблемой для реального взаимодействия.
\n| Наблюдение | Рабочая гипотеза | Что проверить | Следующее действие |
|---|---|---|---|
| После клика нет ожидаемого класса или текста | Неверный элемент, селектор или исходное состояние | Сравнить DOM до и после; проверить результат обработчика | Исправить контракт состояния и повторить сценарий |
| После записи класса появляется долгий Layout | Стиль изменил геометрию или затронул много узлов | Выделить одну запись взаимодействия и посмотреть связанные Layout и Recalculate Style | Проверить одно правило или свойство, затем повторить запись |
| Layout находится внутри обработчика | После записи читается размер или позиция | Перейти из события к строке с offsetHeight, getBoundingClientRect или похожим чтением | Разнести чтения и записи, если это сохраняет требуемый результат |
| График стал короче, но карточка не готова | Оптимизация убрала функциональную операцию | Сверить класс, текст, доступность и визуальное состояние | Вернуть результат и сузить гипотезу до конкретной работы |
| В двух браузерах разные события | Различаются движок, версия или способ инструментирования | Зафиксировать браузер, версию, устройство и настройки записи | Сравнивать пользовательский результат и стоимость, а не одинаковые названия событий |
Начинайте слева: какой элемент изменился, какое значение было до записи и какой результат должен увидеть пользователь. Затем переходите к стилям и геометрии в том же диапазоне записи. Если работа находится после конкретной mutation, это ещё гипотеза. Нужно проверить, что она повторяется в том же сценарии и исчезает или уменьшается после одного контролируемого изменения.
\nОтдельно смотрите на paint и composite. Небольшой paint не делает решение автоматически хорошим: изменение может нарушить состояние или вызвать последующую работу. И наоборот, наличие Layout не означает, что нужно срочно заменить всё на transform. Сначала определите, какую геометрию требует интерфейс и нельзя ли уменьшить область или частоту изменения.
Если нужен числовой вывод, фиксируйте условия измерения. Браузер, версия, ограничение CPU, размер окна просмотра, число элементов, шрифты и данные влияют на результат. Без этих условий запись показывает наблюдение, но не универсальный бюджет для всех устройств.
\nПервый ложный след — операция без изменения (no-op). Код выполняется, но записывает то же значение. Новый DOM-result не появляется, поэтому один клик не доказывает дорогую отрисовку. Проверьте двойной обработчик, повторный рендер и реальное отличие состояния до и после.
\nВторой ложный след — некорректное обновление (invalid update). Селектор не нашёл элемент, класс не совпал или значение рассчитано из устаревшего состояния. Такой путь нельзя считать быстрым кадром с нулевой стоимостью. Сначала исправьте контракт и повторите запись.
\nТретий случай — работа есть, но она не является проблемой. Layout может быть необходим, если меняется высота сообщения или появляется новый блок. Вопрос не в том, можно ли убрать событие любой ценой, а в том, укладывается ли взаимодействие в требования и сохраняется ли результат.
\nСтоимость зависит от размера и структуры DOM, сложности правил, изменённой области, шрифтов, изображений, анимаций, устройства и конкурирующей работы. Даже одинаковое CSS-свойство может вести себя по-разному в другом контексте. Производительность нельзя приписать слову «CSS» или одной строке JavaScript без записи.
\nСборка слоёв также не означает «бесплатно». Она использует ресурсы устройства. Принудительное создание композитных слоёв может увеличить расход памяти и стоимость растеризации. Поэтому перенос свойства в transform — гипотеза для измерения, а не универсальный рецепт.
HTML Standard описывает ожидаемое отображение документа, но не устанавливает для разработчика фиксированный внутренний pipeline с названиями trace-событий. Это важная граница источников: документация объясняет термины и инструменты, а ответ для своего проекта даёт только собственная воспроизводимая запись.
\nРасследование можно закрыть, когда есть один воспроизводимый сценарий, ссылка на обработчик, DOM до и после, выделенный диапазон записи и подтверждённая или опровергнутая гипотеза. После правки карточка сохраняет ожидаемый результат, а выбранная работа уменьшилась или стала объяснимой без переноса симптома.
\nЕсли измерение не показало существенной разницы, это тоже результат. Верните лишнюю сложность, оставьте функционально корректный код и запишите, при каких данных и на каком устройстве проверка проводилась. Так следующая оптимизация начнётся с факта, а не с ярлыка «layout тормозит».
\n