8 lines
20 KiB
JSON
8 lines
20 KiB
JSON
{
|
||
"index": 209,
|
||
"slug": "editorial-2022-03-mechanism-browser-rendering",
|
||
"title": "Как браузер рисует изменение интерфейса: модель и границы",
|
||
"excerpt": "Разбираем путь от изменения DOM до видимого кадра: как найти forced layout в записи, отделить производительность от функционального результата и выбрать проверяемое изменение.",
|
||
"contentHtml": "<p>Кнопка меняет класс карточки, но интерфейс отвечает с рывком. В Performance panel виден длинный участок работы, и команда сразу обвиняет JavaScript или CSS. Такой диагноз может убрать не задержку, а само состояние «готово». Пользователь увидит более короткую запись, но не получит результат действия.</p>\n<p>Для расследования нужна причинная цепочка: кто изменил DOM, какие стили стали применимы, потребовалась ли новая геометрия, какая область перерисовалась и как браузер собрал кадр. Это инженерная модель, а не обещание одинакового набора событий во всех движках. Её задача — связать симптом с одной записью и одной проверяемой гипотезой.</p>\n<h2>Что именно меняется после DOM-операции</h2>\n<p>Рассмотрим учебный сценарий. После отправки формы обработчик заменяет состояние карточки с <code>is-pending</code> на <code>is-ready</code>. Вместе с классом меняются цвет и подпись. Если обработчик выбрал не тот элемент или класс не изменился, искать дорогую отрисовку рано: сначала сломан функциональный контракт.</p>\n<p>После записи браузер должен определить применимые стили. Если изменение влияет на размеры или положение, движку может понадобиться layout — расчёт геометрии. Затем он подготавливает пиксели изменившейся области и собирает итоговые слои для вывода. В Chrome DevTools эти этапы видны через события и связанные записи, но конкретное отображение зависит от браузера, версии, устройства и настроек захвата.</p>\n<p>Поэтому слова <code>style</code>, <code>layout</code>, <code>paint</code> и <code>composite</code> полезны как названия вопросов. Они не доказывают сами по себе, что один этап вызвал другой. Доказательство появляется, когда запись связывает обработчик, изменение DOM и наблюдаемый результат.</p>\n<h2>Минимальный пример с проверяемым результатом</h2>\n<p>Предположим, на странице есть кнопка <code>[data-ready-button]</code>, карточка <code>[data-card]</code> и подпись <code>.card__status</code>. Обработчик ниже меняет только состояние карточки и текст. Проверяемый результат — класс <code>is-ready</code> и слово «Готово» после одного клика.</p>\n<pre><code>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});</code></pre>\n<p>До записи зафиксируйте <code>class</code> карточки и текст подписи. После записи проверьте оба значения в Elements или через консоль. Если <code>classList.replace</code> вернул <code>false</code>, старого класса не было, и причина находится в состоянии или селекторе, а не в стоимости paint.</p>\n<p>В таком сценарии JavaScript запускает изменение, но не равен всей работе браузера. Он также не доказывает, что любое изменение класса приведёт к полному layout. Объём работы определяется стилями, деревом элементов и текущим состоянием страницы.</p>\n<h2>Почему чтение геометрии может стать синхронным</h2>\n<p>Браузер может отложить часть расчётов до удобной точки. Но чтение геометрии после записи стиля требует актуального значения. Если новое значение ещё не рассчитано, движку приходится завершить нужные style и layout прямо во время текущей задачи. В DevTools это часто описывают как forced synchronous layout или forced reflow.</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>Здесь сначала записывается ширина, а затем читается <code>offsetHeight</code>. Если ширина меняет перенос строк, высота зависит от этой записи. Так чтение становится границей, на которой браузер обязан получить новую геометрию. Сам фрагмент не гарантирует одинаковую стоимость в каждом движке: её нужно увидеть в конкретной записи.</p>\n<p>Безопасная оптимизация начинается с вопроса о результате. Если нужна высота до изменения, прочитайте её до записи и затем выполните записи вместе:</p>\n<pre><code>function keepCurrentHeight() {\n const currentHeight = card.offsetHeight;\n\n card.style.width = '320px';\n card.style.height = `${currentHeight}px`;\n}</code></pre>\n<p>Этот вариант подходит только для контракта «сохранить текущую высоту». Он не заменяет измерение новой высоты, если ширина должна изменить переносы и размер карточки. В последнем случае измерение после записи — часть задачи, а не автоматический дефект. Профиль нужен, чтобы понять, стала ли эта граница проблемой для реального взаимодействия.</p>\n<h2>Как читать одну запись, а не весь график</h2>\n<div class='table-scroll'><table><caption>Симптом, гипотеза и следующая проверка</caption><thead><tr><th scope='col'>Наблюдение</th><th scope='col'>Рабочая гипотеза</th><th scope='col'>Что проверить</th><th scope='col'>Следующее действие</th></tr></thead><tbody><tr><td>После клика нет ожидаемого класса или текста</td><td>Неверный элемент, селектор или исходное состояние</td><td>Сравнить DOM до и после; проверить результат обработчика</td><td>Исправить контракт состояния и повторить сценарий</td></tr><tr><td>После записи класса появляется долгий Layout</td><td>Стиль изменил геометрию или затронул много узлов</td><td>Выделить одну запись взаимодействия и посмотреть связанные Layout и Recalculate Style</td><td>Проверить одно правило или свойство, затем повторить запись</td></tr><tr><td>Layout находится внутри обработчика</td><td>После записи читается размер или позиция</td><td>Перейти из события к строке с <code>offsetHeight</code>, <code>getBoundingClientRect</code> или похожим чтением</td><td>Разнести чтения и записи, если это сохраняет требуемый результат</td></tr><tr><td>График стал короче, но карточка не готова</td><td>Оптимизация убрала функциональную операцию</td><td>Сверить класс, текст, доступность и визуальное состояние</td><td>Вернуть результат и сузить гипотезу до конкретной работы</td></tr><tr><td>В двух браузерах разные события</td><td>Различаются движок, версия или способ инструментирования</td><td>Зафиксировать браузер, версию, устройство и настройки записи</td><td>Сравнивать пользовательский результат и стоимость, а не одинаковые названия событий</td></tr></tbody></table></div>\n<h2>Иллюстрация модели</h2>\n<figure><img src='/assets/editorial/2022/browser-rendering-pipeline-2022.svg' alt='Пять вопросов при расследовании изменения интерфейса: JavaScript mutation, style, layout, paint и composite'><figcaption>Схема показывает порядок вопросов от mutation до сборки кадра. Пунктирная рамка подчёркивает: это учебная модель, а не обязательные имена событий браузера.</figcaption></figure>\n<p>Начинайте слева: какой элемент изменился, какое значение было до записи и какой результат должен увидеть пользователь. Затем переходите к стилям и геометрии в том же диапазоне записи. Если работа находится после конкретной mutation, это ещё гипотеза. Нужно проверить, что она повторяется в том же сценарии и исчезает или уменьшается после одного контролируемого изменения.</p>\n<p>Отдельно смотрите на paint и composite. Небольшой paint не делает решение автоматически хорошим: изменение может нарушить состояние или вызвать последующую работу. И наоборот, наличие Layout не означает, что нужно срочно заменить всё на <code>transform</code>. Сначала определите, какую геометрию требует интерфейс и нельзя ли уменьшить область или частоту изменения.</p>\n<h2>Порядок воспроизводимого расследования</h2>\n<ol><li>Опишите действие, видимый симптом и цену ошибки: например, «после клика карточка должна стать готовой, но отклик задерживается».</li><li>Зафиксируйте DOM до и после: элемент, класс, текст, размеры и обработчик.</li><li>Откройте Performance panel в одном согласованном браузере. Запишите один чистый сценарий без лишних кликов и фоновых действий.</li><li>Найдите запись взаимодействия и ограничьте диапазон этой задачей. Не делайте вывод по всей временной шкале.</li><li>Сопоставьте обработчик с Recalculate Style, Layout, Paint или другой связанной записью. Проверьте call stack и список затронутых узлов.</li><li>Сформулируйте одну гипотезу: чтение после записи, изменение геометрического свойства, слишком большой DOM или повторная обработка.</li><li>Измените только одного владельца: обработчик, CSS-правило или расписание обновления. Сохраните прежний вариант для отката.</li><li>Повторите тот же сценарий с теми же данными и настройками. Сверьте длительность, DOM после действия и отсутствие нового симптома.</li></ol>\n<p>Если нужен числовой вывод, фиксируйте условия измерения. Браузер, версия, ограничение CPU, размер окна просмотра, число элементов, шрифты и данные влияют на результат. Без этих условий запись показывает наблюдение, но не универсальный бюджет для всех устройств.</p>\n<h2>Когда оптимизировать нечего</h2>\n<p>Первый ложный след — операция без изменения (no-op). Код выполняется, но записывает то же значение. Новый DOM-result не появляется, поэтому один клик не доказывает дорогую отрисовку. Проверьте двойной обработчик, повторный рендер и реальное отличие состояния до и после.</p>\n<p>Второй ложный след — некорректное обновление (invalid update). Селектор не нашёл элемент, класс не совпал или значение рассчитано из устаревшего состояния. Такой путь нельзя считать быстрым кадром с нулевой стоимостью. Сначала исправьте контракт и повторите запись.</p>\n<p>Третий случай — работа есть, но она не является проблемой. Layout может быть необходим, если меняется высота сообщения или появляется новый блок. Вопрос не в том, можно ли убрать событие любой ценой, а в том, укладывается ли взаимодействие в требования и сохраняется ли результат.</p>\n<h2>Границы технической модели</h2>\n<p>Стоимость зависит от размера и структуры DOM, сложности правил, изменённой области, шрифтов, изображений, анимаций, устройства и конкурирующей работы. Даже одинаковое CSS-свойство может вести себя по-разному в другом контексте. Производительность нельзя приписать слову «CSS» или одной строке JavaScript без записи.</p>\n<p>Сборка слоёв также не означает «бесплатно». Она использует ресурсы устройства. Принудительное создание композитных слоёв может увеличить расход памяти и стоимость растеризации. Поэтому перенос свойства в <code>transform</code> — гипотеза для измерения, а не универсальный рецепт.</p>\n<p>HTML Standard описывает ожидаемое отображение документа, но не устанавливает для разработчика фиксированный внутренний pipeline с названиями trace-событий. Это важная граница источников: документация объясняет термины и инструменты, а ответ для своего проекта даёт только собственная воспроизводимая запись.</p>\n<h2>Критерий готовности</h2>\n<p>Расследование можно закрыть, когда есть один воспроизводимый сценарий, ссылка на обработчик, DOM до и после, выделенный диапазон записи и подтверждённая или опровергнутая гипотеза. После правки карточка сохраняет ожидаемый результат, а выбранная работа уменьшилась или стала объяснимой без переноса симптома.</p>\n<p>Если измерение не показало существенной разницы, это тоже результат. Верните лишнюю сложность, оставьте функционально корректный код и запишите, при каких данных и на каком устройстве проверка проводилась. Так следующая оптимизация начнётся с факта, а не с ярлыка «layout тормозит».</p>\n<h2>Проверяемые источники</h2><ul><li><a href='https://developer.chrome.com/docs/devtools/performance/overview' target='_blank' rel='noopener'>Chrome DevTools: Performance panel</a> — официальная документация по записи профиля и анализу производительности.</li><li><a href='https://web.dev/articles/avoid-large-complex-layouts-and-layout-thrashing' target='_blank' rel='noopener'>web.dev: Avoid large, complex layouts and layout thrashing</a> — объяснение layout, чтения геометрии после записи и forced synchronous layout.</li><li><a href='https://html.spec.whatwg.org/multipage/rendering.html' target='_blank' rel='noopener'>WHATWG HTML Standard: Rendering</a> — нормативный контекст: стандарт задаёт ожидаемое отображение, а не обязательный набор trace-событий.</li></ul>"
|
||
}
|