8 lines
16 KiB
JSON
8 lines
16 KiB
JSON
{
|
||
"index": 209,
|
||
"slug": "editorial-2022-03-mechanism-browser-rendering",
|
||
"title": "Почему браузер не успевает отрисовать изменение интерфейса",
|
||
"excerpt": "Разбор рывка после изменения DOM: как отделить работу JavaScript от style, layout, paint и composite, проверить одну запись и не исправить производительность ценой сломанного состояния интерфейса.",
|
||
"contentHtml": "<p>Кнопка меняет класс карточки, но экран отвечает с рывком. В Performance panel виден длинный участок работы. Команда называет виновником то JavaScript, то CSS и сразу меняет код. Это опасный диагноз. Если убрать обработчик, интерфейс может стать плавнее только потому, что карточка перестанет переходить в состояние «готово». Цена ошибки — потерянный пользовательский результат, лишний релиз и новая задержка, которую теперь труднее связать с причиной.</p>\n<p>Тезис статьи простой: изменение DOM — это вход, а видимое состояние — выход. Между ними браузер выполняет несколько видов работы. Их удобно разделить на вопросы: кто сделал mutation, какие стили стали применимы, нужна ли новая геометрия, какую область надо закрасить и как собрать результат на экране. Этот порядок помогает сузить гипотезу. Он не является обещанием одинакового внутреннего конвейера во всех браузерах.</p>\n<h2>Что происходит после изменения DOM</h2>\n<p>Рассмотрим учебный, но реалистичный сценарий. После отправки формы обработчик меняет класс карточки с <code>card is-pending</code> на <code>card is-ready</code>. Новый класс меняет цвет, высоту и подпись. Сначала нужно доказать сам факт изменения. Если <code>className</code> не изменился или выбран не тот элемент, искать дорогой paint бессмысленно.</p>\n<pre><code>const card = document.querySelector('.card');\n\nfunction markReady() {\n card.className = 'card is-ready';\n}\n\nbutton.addEventListener('click', markReady);</code></pre>\n<p>После mutation браузер пересчитывает применимые стили. Если свойства влияют на геометрию, он может пересчитать положение и размеры элементов. Затем он подготавливает пиксели для изменившейся области. В некоторых случаях отдельные слои можно собрать без полной перекраски. В DevTools эти этапы могут отображаться разными событиями, объединяться или отсутствовать в выбранном диапазоне. Поэтому названия <code>style</code>, <code>layout</code>, <code>paint</code> и <code>composite</code> ниже — рабочие labels для расследования, а не API-контракт.</p>\n<p>JavaScript часто запускает цепочку, но не равен всей цепочке. Обратная ошибка тоже встречается: строку CSS считают причиной только потому, что после неё виден layout. Нужна связь с действием пользователя, конкретным DOM-result и выбранным диапазоном записи.</p>\n<h2>Один пример: как чтение геометрии усиливает работу</h2>\n<p>Проблема становится заметнее, когда код сначала меняет стиль, а затем немедленно читает геометрию. Браузер ещё может откладывать расчёт. Чтение <code>offsetHeight</code> требует актуального значения, поэтому движок вынужден завершить нужную часть расчёта прямо внутри обработчика.</p>\n<pre><code>function resizeCard() {\n card.style.width = '320px';\n const height = card.offsetHeight;\n card.style.height = `${height}px`;\n}</code></pre>\n<p>Этот пример учебный. Он не доказывает, что каждый вызов приведёт к forced layout в каждом движке. Он показывает условие для проверки: запись стиля и чтение геометрии идут рядом, а между ними нет границы, на которой можно увидеть фактическую работу. В реальном расследовании надо открыть запись выбранного браузера и проверить, какой обработчик вызвал layout и какие узлы попали в расчёт.</p>\n<p>Без чтения записи нельзя объявлять свойство «дорогим» навсегда. Иногда браузер обновит только часть дерева. Иногда layout уже был нужен по другой причине. Иногда изменение попадёт в отдельный слой и не потребует полной перекраски. Поэтому заменять всё на <code>transform</code> или удалять visual state без проверки результата — такой же плохой путь, как игнорировать layout.</p>\n<h2>Как читать причинную цепочку</h2>\n<table><caption>Симптом → причина → проверка → действие</caption><thead><tr><th>Симптом</th><th>Возможная причина</th><th>Проверка</th><th>Действие</th></tr></thead><tbody><tr><td>После клика меняется не тот элемент</td><td>Неверный target или selector</td><td>Сравнить DOM before/after в Elements и код обработчика</td><td>Исправить контракт изменения; не оптимизировать trace</td></tr><tr><td>Долгий участок появляется после записи класса</td><td>Новый стиль требует расчёта геометрии или paint</td><td>Выделить одну interaction и посмотреть связанные Layout/Paint events</td><td>Проверить одно свойство или правило и повторить запись</td></tr><tr><td>Layout находится внутри обработчика</td><td>Чтение геометрии после записи стиля</td><td>Перейти из события к строке кода, которая читает размер или позицию</td><td>Разнести запись и чтение, либо проверить другой способ обновления</td></tr><tr><td>График стал короче, но UI не меняется</td><td>Оптимизация убрала функциональный result</td><td>Сверить ожидаемый className, текст и размеры after-state</td><td>Откатить изменение и выбрать более узкую гипотезу</td></tr><tr><td>В разных браузерах видны разные события</td><td>Разные движки и версии по-разному показывают внутреннюю работу</td><td>Зафиксировать браузер, версию, сценарий и диапазон записи</td><td>Сравнивать результат и пользовательский симптом, а не одинаковые labels</td></tr></tbody></table>\n<h2>Иллюстрация одной проверки</h2>\n<figure><img src=\"/assets/editorial/2022/browser-rendering-pipeline-2022.svg\" alt=\"Схема пути от изменения className через style, layout, paint и composite к видимому кадру\"><figcaption>Схема разделяет вопросы расследования. Она не утверждает, что выбранный браузер всегда создаёт пять отдельных событий с такими именами.</figcaption></figure>\n<p>Начните слева с DOM-контракта: какой элемент меняется, какое свойство записывается, что было до изменения и какой результат ожидается. Затем связывайте каждый следующий вопрос с одной и той же записью. Если after-state не совпал с ожиданием, проблема ещё функциональная. Если результат совпал, ищите работу, которая возникла после конкретной mutation. Только после этого выбирайте правку.</p>\n<h2>Порядок расследования</h2>\n<ol><li>Опишите симптом одним предложением: действие, видимый сбой и цену для пользователя.</li><li>Зафиксируйте DOM before и after. Укажите target, свойство и обработчик.</li><li>Откройте Performance panel в согласованном браузере и запишите только один сценарий, без серии лишних кликов.</li><li>Найдите interaction и сузьте диапазон до действия. Не делайте вывод по всей временной шкале.</li><li>Сопоставьте событие с кодом. Проверьте, есть ли после mutation чтение геометрии, массовое изменение DOM или правило, меняющее размеры.</li><li>Сформулируйте одну гипотезу и измените только один owner: обработчик, правило CSS или способ обновления свойства.</li><li>Повторите тот же сценарий. Сравните не только длительность работы, но и DOM after, визуальный результат и отсутствие нового симптома.</li><li>Сохраните способ отката. Если гипотеза не подтверждается, верните прежний код и начните с другого участка цепочки.</li></ol>\n<h2>Отрицательный путь: когда оптимизировать нечего</h2>\n<p>Есть два честных случая остановки. Первый — no-op: код записывает то же значение, которое уже установлено. Нового DOM-result нет, поэтому нельзя строить историю «дорогого кадра» только по факту клика. Проверьте, не повторяется ли действие из-за двойного обработчика или лишнего рендера.</p>\n<p>Второй — invalid update. Обработчик выбирает отсутствующий элемент, передаёт не то свойство или рассчитывает значение из устаревшего состояния. Такой путь нельзя считать быстрым кадром с нулевой стоимостью. Сначала исправьте контракт и повторите запись. Иначе оптимизация скроет функциональную ошибку.</p>\n<h2>Ограничения модели</h2>\n<p>Учебная цепочка не отвечает на вопрос, какое CSS-свойство всегда дёшево. Стоимость зависит от дерева, размера изменённой области, движка, версии браузера, устройства, шрифтов, изображений, анимации и соседней работы. <code>composite</code> также не означает автоматически «бесплатно»: сборка слоёв использует ресурсы устройства и может стать узким местом.</p>\n<p>Нельзя переносить примерные числа из диаграмм или одного trace в production-бюджет. Нельзя объявлять проблему доказанной по цвету события в панели. Нельзя сравнивать две записи, если изменились браузер, throttling, состояние данных или сам DOM-result. Источники объясняют терминологию и инструмент. Они не заменяют запись именно вашего сценария.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Расследование готово, когда есть один воспроизводимый сценарий, ссылка на обработчик, DOM before/after, выделенный диапазон записи и одна подтверждённая или опровергнутая гипотеза. После правки тот же сценарий даёт ожидаемый after-state, а выбранная работа уменьшилась или стала понятнее без переноса симптома в другое место. Если это учебный пример, так и пометьте его. Не называйте условные значения миллисекундами и не выдавайте их за результат production-измерения.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://developer.chrome.com/docs/devtools/performance\" target=\"_blank\" rel=\"noopener\">Chrome DevTools: Analyze runtime performance</a> — официальный порядок записи, чтения flame chart и поиска forced layout.</li><li><a href=\"https://html.spec.whatwg.org/multipage/rendering.html\" target=\"_blank\" rel=\"noopener\">WHATWG HTML Standard: Rendering</a> — официальное описание rendering как поведения user agent, а не фиксированного набора trace-событий.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Critical_rendering_path\" target=\"_blank\" rel=\"noopener\">MDN: Critical rendering path</a> — справочная схема перехода от HTML/CSS/JavaScript к layout и pixels с оговорками о измерении.</li></ul>"
|
||
}
|