From a1131daa16d5c8dc3bbb126f37f321ddf5648fd7 Mon Sep 17 00:00:00 2001 From: "E.Gavrilov" Date: Thu, 3 Sep 2026 20:13:12 +0300 Subject: [PATCH] =?UTF-8?q?editorial:=20=D1=83=D0=BB=D1=83=D1=87=D1=88?= =?UTF-8?q?=D0=B8=D1=82=D1=8C=20=D1=80=D0=B0=D0=B7=D0=B1=D0=BE=D1=80=20?= =?UTF-8?q?=D1=80=D0=B5=D0=BD=D0=B4=D0=B5=D1=80=D0=B8=D0=BD=D0=B3=D0=B0=20?= =?UTF-8?q?=D0=B1=D1=80=D0=B0=D1=83=D0=B7=D0=B5=D1=80=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- editorial/agent-rewrites/209.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/editorial/agent-rewrites/209.json b/editorial/agent-rewrites/209.json index 08cdb2c..4b5f376 100644 --- a/editorial/agent-rewrites/209.json +++ b/editorial/agent-rewrites/209.json @@ -1,7 +1,7 @@ { "index": 209, "slug": "editorial-2022-03-mechanism-browser-rendering", - "title": "Почему браузер не успевает отрисовать изменение интерфейса", - "excerpt": "Разбор рывка после изменения DOM: как отделить работу JavaScript от style, layout, paint и composite, проверить одну запись и не исправить производительность ценой сломанного состояния интерфейса.", - "contentHtml": "

Кнопка меняет класс карточки, но экран отвечает с рывком. В Performance panel виден длинный участок работы. Команда называет виновником то JavaScript, то CSS и сразу меняет код. Это опасный диагноз. Если убрать обработчик, интерфейс может стать плавнее только потому, что карточка перестанет переходить в состояние «готово». Цена ошибки — потерянный пользовательский результат, лишний релиз и новая задержка, которую теперь труднее связать с причиной.

\n

Тезис статьи простой: изменение DOM — это вход, а видимое состояние — выход. Между ними браузер выполняет несколько видов работы. Их удобно разделить на вопросы: кто сделал mutation, какие стили стали применимы, нужна ли новая геометрия, какую область надо закрасить и как собрать результат на экране. Этот порядок помогает сузить гипотезу. Он не является обещанием одинакового внутреннего конвейера во всех браузерах.

\n

Что происходит после изменения DOM

\n

Рассмотрим учебный, но реалистичный сценарий. После отправки формы обработчик меняет класс карточки с card is-pending на card is-ready. Новый класс меняет цвет, высоту и подпись. Сначала нужно доказать сам факт изменения. Если className не изменился или выбран не тот элемент, искать дорогой paint бессмысленно.

\n
const card = document.querySelector('.card');\n\nfunction markReady() {\n  card.className = 'card is-ready';\n}\n\nbutton.addEventListener('click', markReady);
\n

После mutation браузер пересчитывает применимые стили. Если свойства влияют на геометрию, он может пересчитать положение и размеры элементов. Затем он подготавливает пиксели для изменившейся области. В некоторых случаях отдельные слои можно собрать без полной перекраски. В DevTools эти этапы могут отображаться разными событиями, объединяться или отсутствовать в выбранном диапазоне. Поэтому названия style, layout, paint и composite ниже — рабочие labels для расследования, а не API-контракт.

\n

JavaScript часто запускает цепочку, но не равен всей цепочке. Обратная ошибка тоже встречается: строку CSS считают причиной только потому, что после неё виден layout. Нужна связь с действием пользователя, конкретным DOM-result и выбранным диапазоном записи.

\n

Один пример: как чтение геометрии усиливает работу

\n

Проблема становится заметнее, когда код сначала меняет стиль, а затем немедленно читает геометрию. Браузер ещё может откладывать расчёт. Чтение offsetHeight требует актуального значения, поэтому движок вынужден завершить нужную часть расчёта прямо внутри обработчика.

\n
function resizeCard() {\n  card.style.width = '320px';\n  const height = card.offsetHeight;\n  card.style.height = `${height}px`;\n}
\n

Этот пример учебный. Он не доказывает, что каждый вызов приведёт к forced layout в каждом движке. Он показывает условие для проверки: запись стиля и чтение геометрии идут рядом, а между ними нет границы, на которой можно увидеть фактическую работу. В реальном расследовании надо открыть запись выбранного браузера и проверить, какой обработчик вызвал layout и какие узлы попали в расчёт.

\n

Без чтения записи нельзя объявлять свойство «дорогим» навсегда. Иногда браузер обновит только часть дерева. Иногда layout уже был нужен по другой причине. Иногда изменение попадёт в отдельный слой и не потребует полной перекраски. Поэтому заменять всё на transform или удалять visual state без проверки результата — такой же плохой путь, как игнорировать layout.

\n

Как читать причинную цепочку

\n
Симптом → причина → проверка → действие
СимптомВозможная причинаПроверкаДействие
После клика меняется не тот элементНеверный target или selectorСравнить DOM before/after в Elements и код обработчикаИсправить контракт изменения; не оптимизировать trace
Долгий участок появляется после записи классаНовый стиль требует расчёта геометрии или paintВыделить одну interaction и посмотреть связанные Layout/Paint eventsПроверить одно свойство или правило и повторить запись
Layout находится внутри обработчикаЧтение геометрии после записи стиляПерейти из события к строке кода, которая читает размер или позициюРазнести запись и чтение, либо проверить другой способ обновления
График стал короче, но UI не меняетсяОптимизация убрала функциональный resultСверить ожидаемый className, текст и размеры after-stateОткатить изменение и выбрать более узкую гипотезу
В разных браузерах видны разные событияРазные движки и версии по-разному показывают внутреннюю работуЗафиксировать браузер, версию, сценарий и диапазон записиСравнивать результат и пользовательский симптом, а не одинаковые labels
\n

Иллюстрация одной проверки

\n
\"Схема
Схема разделяет вопросы расследования. Она не утверждает, что выбранный браузер всегда создаёт пять отдельных событий с такими именами.
\n

Начните слева с DOM-контракта: какой элемент меняется, какое свойство записывается, что было до изменения и какой результат ожидается. Затем связывайте каждый следующий вопрос с одной и той же записью. Если after-state не совпал с ожиданием, проблема ещё функциональная. Если результат совпал, ищите работу, которая возникла после конкретной mutation. Только после этого выбирайте правку.

\n

Порядок расследования

\n
  1. Опишите симптом одним предложением: действие, видимый сбой и цену для пользователя.
  2. Зафиксируйте DOM before и after. Укажите target, свойство и обработчик.
  3. Откройте Performance panel в согласованном браузере и запишите только один сценарий, без серии лишних кликов.
  4. Найдите interaction и сузьте диапазон до действия. Не делайте вывод по всей временной шкале.
  5. Сопоставьте событие с кодом. Проверьте, есть ли после mutation чтение геометрии, массовое изменение DOM или правило, меняющее размеры.
  6. Сформулируйте одну гипотезу и измените только один owner: обработчик, правило CSS или способ обновления свойства.
  7. Повторите тот же сценарий. Сравните не только длительность работы, но и DOM after, визуальный результат и отсутствие нового симптома.
  8. Сохраните способ отката. Если гипотеза не подтверждается, верните прежний код и начните с другого участка цепочки.
\n

Отрицательный путь: когда оптимизировать нечего

\n

Есть два честных случая остановки. Первый — no-op: код записывает то же значение, которое уже установлено. Нового DOM-result нет, поэтому нельзя строить историю «дорогого кадра» только по факту клика. Проверьте, не повторяется ли действие из-за двойного обработчика или лишнего рендера.

\n

Второй — invalid update. Обработчик выбирает отсутствующий элемент, передаёт не то свойство или рассчитывает значение из устаревшего состояния. Такой путь нельзя считать быстрым кадром с нулевой стоимостью. Сначала исправьте контракт и повторите запись. Иначе оптимизация скроет функциональную ошибку.

\n

Ограничения модели

\n

Учебная цепочка не отвечает на вопрос, какое CSS-свойство всегда дёшево. Стоимость зависит от дерева, размера изменённой области, движка, версии браузера, устройства, шрифтов, изображений, анимации и соседней работы. composite также не означает автоматически «бесплатно»: сборка слоёв использует ресурсы устройства и может стать узким местом.

\n

Нельзя переносить примерные числа из диаграмм или одного trace в production-бюджет. Нельзя объявлять проблему доказанной по цвету события в панели. Нельзя сравнивать две записи, если изменились браузер, throttling, состояние данных или сам DOM-result. Источники объясняют терминологию и инструмент. Они не заменяют запись именно вашего сценария.

\n

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

\n

Расследование готово, когда есть один воспроизводимый сценарий, ссылка на обработчик, DOM before/after, выделенный диапазон записи и одна подтверждённая или опровергнутая гипотеза. После правки тот же сценарий даёт ожидаемый after-state, а выбранная работа уменьшилась или стала понятнее без переноса симптома в другое место. Если это учебный пример, так и пометьте его. Не называйте условные значения миллисекундами и не выдавайте их за результат production-измерения.

\n

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

" + "title": "Как браузер рисует изменение интерфейса: модель и границы", + "excerpt": "Разбираем путь от изменения DOM до видимого кадра: как найти forced layout в записи, отделить производительность от функционального результата и выбрать проверяемое изменение.", + "contentHtml": "

Кнопка меняет класс карточки, но интерфейс отвечает с рывком. В Performance panel виден длинный участок работы, и команда сразу обвиняет JavaScript или CSS. Такой диагноз может убрать не задержку, а само состояние «готово». Пользователь увидит более короткую запись, но не получит результат действия.

\n

Для расследования нужна причинная цепочка: кто изменил DOM, какие стили стали применимы, потребовалась ли новая геометрия, какая область перерисовалась и как браузер собрал кадр. Это инженерная модель, а не обещание одинакового набора событий во всех движках. Её задача — связать симптом с одной записью и одной проверяемой гипотезой.

\n

Что именно меняется после DOM-операции

\n

Рассмотрим учебный сценарий. После отправки формы обработчик заменяет состояние карточки с is-pending на is-ready. Вместе с классом меняются цвет и подпись. Если обработчик выбрал не тот элемент или класс не изменился, искать дорогую отрисовку рано: сначала сломан функциональный контракт.

\n

После записи браузер должен определить применимые стили. Если изменение влияет на размеры или положение, движку может понадобиться layout — расчёт геометрии. Затем он подготавливает пиксели изменившейся области и собирает итоговые слои для вывода. В Chrome DevTools эти этапы видны через события и связанные записи, но конкретное отображение зависит от браузера, версии, устройства и настроек захвата.

\n

Поэтому слова style, layout, paint и composite полезны как названия вопросов. Они не доказывают сами по себе, что один этап вызвал другой. Доказательство появляется, когда запись связывает обработчик, изменение DOM и наблюдаемый результат.

\n

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

\n

Предположим, на странице есть кнопка [data-ready-button], карточка [data-card] и подпись .card__status. Обработчик ниже меняет только состояние карточки и текст. Проверяемый результат — класс is-ready и слово «Готово» после одного клика.

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

\n

В таком сценарии JavaScript запускает изменение, но не равен всей работе браузера. Он также не доказывает, что любое изменение класса приведёт к полному layout. Объём работы определяется стилями, деревом элементов и текущим состоянием страницы.

\n

Почему чтение геометрии может стать синхронным

\n

Браузер может отложить часть расчётов до удобной точки. Но чтение геометрии после записи стиля требует актуального значения. Если новое значение ещё не рассчитано, движку приходится завершить нужные style и layout прямо во время текущей задачи. В DevTools это часто описывают как forced synchronous layout или forced reflow.

\n
function resizeCard() {\n  card.style.width = '320px';\n  const height = card.offsetHeight;\n  card.style.height = `${height}px`;\n}
\n

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

\n

Безопасная оптимизация начинается с вопроса о результате. Если нужна высота до изменения, прочитайте её до записи и затем выполните записи вместе:

\n
function keepCurrentHeight() {\n  const currentHeight = card.offsetHeight;\n\n  card.style.width = '320px';\n  card.style.height = `${currentHeight}px`;\n}
\n

Этот вариант подходит только для контракта «сохранить текущую высоту». Он не заменяет измерение новой высоты, если ширина должна изменить переносы и размер карточки. В последнем случае измерение после записи — часть задачи, а не автоматический дефект. Профиль нужен, чтобы понять, стала ли эта граница проблемой для реального взаимодействия.

\n

Как читать одну запись, а не весь график

\n
Симптом, гипотеза и следующая проверка
НаблюдениеРабочая гипотезаЧто проверитьСледующее действие
После клика нет ожидаемого класса или текстаНеверный элемент, селектор или исходное состояниеСравнить DOM до и после; проверить результат обработчикаИсправить контракт состояния и повторить сценарий
После записи класса появляется долгий LayoutСтиль изменил геометрию или затронул много узловВыделить одну запись взаимодействия и посмотреть связанные Layout и Recalculate StyleПроверить одно правило или свойство, затем повторить запись
Layout находится внутри обработчикаПосле записи читается размер или позицияПерейти из события к строке с offsetHeight, getBoundingClientRect или похожим чтениемРазнести чтения и записи, если это сохраняет требуемый результат
График стал короче, но карточка не готоваОптимизация убрала функциональную операциюСверить класс, текст, доступность и визуальное состояниеВернуть результат и сузить гипотезу до конкретной работы
В двух браузерах разные событияРазличаются движок, версия или способ инструментированияЗафиксировать браузер, версию, устройство и настройки записиСравнивать пользовательский результат и стоимость, а не одинаковые названия событий
\n

Иллюстрация модели

\n
Пять вопросов при расследовании изменения интерфейса: JavaScript mutation, style, layout, paint и composite
Схема показывает порядок вопросов от mutation до сборки кадра. Пунктирная рамка подчёркивает: это учебная модель, а не обязательные имена событий браузера.
\n

Начинайте слева: какой элемент изменился, какое значение было до записи и какой результат должен увидеть пользователь. Затем переходите к стилям и геометрии в том же диапазоне записи. Если работа находится после конкретной mutation, это ещё гипотеза. Нужно проверить, что она повторяется в том же сценарии и исчезает или уменьшается после одного контролируемого изменения.

\n

Отдельно смотрите на paint и composite. Небольшой paint не делает решение автоматически хорошим: изменение может нарушить состояние или вызвать последующую работу. И наоборот, наличие Layout не означает, что нужно срочно заменить всё на transform. Сначала определите, какую геометрию требует интерфейс и нельзя ли уменьшить область или частоту изменения.

\n

Порядок воспроизводимого расследования

\n
  1. Опишите действие, видимый симптом и цену ошибки: например, «после клика карточка должна стать готовой, но отклик задерживается».
  2. Зафиксируйте DOM до и после: элемент, класс, текст, размеры и обработчик.
  3. Откройте Performance panel в одном согласованном браузере. Запишите один чистый сценарий без лишних кликов и фоновых действий.
  4. Найдите запись взаимодействия и ограничьте диапазон этой задачей. Не делайте вывод по всей временной шкале.
  5. Сопоставьте обработчик с Recalculate Style, Layout, Paint или другой связанной записью. Проверьте call stack и список затронутых узлов.
  6. Сформулируйте одну гипотезу: чтение после записи, изменение геометрического свойства, слишком большой DOM или повторная обработка.
  7. Измените только одного владельца: обработчик, CSS-правило или расписание обновления. Сохраните прежний вариант для отката.
  8. Повторите тот же сценарий с теми же данными и настройками. Сверьте длительность, DOM после действия и отсутствие нового симптома.
\n

Если нужен числовой вывод, фиксируйте условия измерения. Браузер, версия, ограничение CPU, размер окна просмотра, число элементов, шрифты и данные влияют на результат. Без этих условий запись показывает наблюдение, но не универсальный бюджет для всех устройств.

\n

Когда оптимизировать нечего

\n

Первый ложный след — операция без изменения (no-op). Код выполняется, но записывает то же значение. Новый DOM-result не появляется, поэтому один клик не доказывает дорогую отрисовку. Проверьте двойной обработчик, повторный рендер и реальное отличие состояния до и после.

\n

Второй ложный след — некорректное обновление (invalid update). Селектор не нашёл элемент, класс не совпал или значение рассчитано из устаревшего состояния. Такой путь нельзя считать быстрым кадром с нулевой стоимостью. Сначала исправьте контракт и повторите запись.

\n

Третий случай — работа есть, но она не является проблемой. Layout может быть необходим, если меняется высота сообщения или появляется новый блок. Вопрос не в том, можно ли убрать событие любой ценой, а в том, укладывается ли взаимодействие в требования и сохраняется ли результат.

\n

Границы технической модели

\n

Стоимость зависит от размера и структуры DOM, сложности правил, изменённой области, шрифтов, изображений, анимаций, устройства и конкурирующей работы. Даже одинаковое CSS-свойство может вести себя по-разному в другом контексте. Производительность нельзя приписать слову «CSS» или одной строке JavaScript без записи.

\n

Сборка слоёв также не означает «бесплатно». Она использует ресурсы устройства. Принудительное создание композитных слоёв может увеличить расход памяти и стоимость растеризации. Поэтому перенос свойства в transform — гипотеза для измерения, а не универсальный рецепт.

\n

HTML Standard описывает ожидаемое отображение документа, но не устанавливает для разработчика фиксированный внутренний pipeline с названиями trace-событий. Это важная граница источников: документация объясняет термины и инструменты, а ответ для своего проекта даёт только собственная воспроизводимая запись.

\n

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

\n

Расследование можно закрыть, когда есть один воспроизводимый сценарий, ссылка на обработчик, DOM до и после, выделенный диапазон записи и подтверждённая или опровергнутая гипотеза. После правки карточка сохраняет ожидаемый результат, а выбранная работа уменьшилась или стала объяснимой без переноса симптома.

\n

Если измерение не показало существенной разницы, это тоже результат. Верните лишнюю сложность, оставьте функционально корректный код и запишите, при каких данных и на каком устройстве проверка проводилась. Так следующая оптимизация начнётся с факта, а не с ярлыка «layout тормозит».

\n

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

" }