Files

8 lines
18 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 210,
"slug": "editorial-2022-03-practice-browser-rendering",
"title": "Медленный рендер: как связать DOM-изменение с причиной в кадре",
"excerpt": "Если интерфейс дёргается после одного действия, не называйте виновником весь браузер. Зафиксируйте DOM-изменение, отделите JavaScript от работы конвейера отрисовки и проверьте гипотезу одной записью Performance panel.",
"contentHtml": "<p>После нажатия «Сохранить» карточка должна сменить состояние, но экран на мгновение замирает. На быстром компьютере задержка почти незаметна, а на слабом ноутбуке превращается в рывок. Команда видит слово «медленный рендер» и начинает менять всё сразу: переносит обработчик, добавляет <code>debounce</code>, убирает CSS и обвиняет фреймворк. Цена такой ошибки — часы работы и изменённый интерфейс без доказательства, что причина исчезла.</p>\n<p>Разбор начинается с более узкого вопроса: какое действие сделал пользователь, какой DOM-результат должен появиться и какая работа попала в тот же участок записи? Сначала фиксируем код и состояние до/после, затем смотрим trace (запись работы браузера) в Performance panel. Слова <code>style</code>, <code>layout</code>, <code>paint</code> и <code>composite</code> помогают задать вопрос к записи, но не образуют универсальный порядок для всех браузеров.</p>\n<h2>Симптом и граница задачи</h2>\n<p>Возьмём кнопку <code>save-card</code>, которая после успешного ответа должна получить класс <code>card is-ready</code>. До клика её состояние — <code>card is-pending</code>. Сеть в этом примере не измеряется: считаем, что ответ уже получен и исследуем только обработчик, DOM-изменение и последующую работу браузера.</p>\n<p>Граница расследования состоит из одного клика, одного target (элемента-цели), одного свойства и двух значений. Формулировка «страница тормозит» ничего не связывает с кодом. Формулировка «после клика на <code>save-card</code> свойство <code>className</code> меняется с <code>card is-pending</code> на <code>card is-ready</code>» уже допускает повторную проверку.</p>\n<figure><img src='/assets/editorial/2022/browser-rendering-timeline-2022.svg' alt='Учебная схема: клик меняет className карточки, после чего показаны условные вопросы о style, layout, paint и composite; synthetic units отделены от реального trace' loading='lazy' /><figcaption>Схема задаёт порядок вопросов к одному DOM-изменению. Synthetic units — условные единицы рисунка, а не миллисекунды и не запись Performance panel.</figcaption></figure>\n<h2>Механизм: от JavaScript к обновлению экрана</h2>\n<p>JavaScript меняет состояние документа. Когда обработчик присваивает новый класс, браузеру может понадобиться пересчитать применимые стили и layout — геометрию элементов. Затем движок обновляет визуальное представление; конкретная реализация может объединить, отложить или пропустить часть работы. Поэтому приложение не получает контракт «после каждой записи будут ровно пять событий».</p>\n<p>Для разговора с коллегой удобна рабочая карта: mutation — изменение DOM; style — пересчёт применимых стилей; layout — вычисление геометрии; paint — подготовка визуальных пикселей; composite — сборка слоёв. Это не обязательные имена событий в trace. Если запись показывает другой набор или порядок, описываем то, что действительно наблюдаем, и сверяем его с версией браузера.</p>\n<p>Отдельный кандидат на причину — чтение геометрии сразу после записи. Например, после изменения класса код читает <code>offsetWidth</code>. Это свойство возвращает layout-ширину элемента, а в Chrome чтение после инвалидирования стилей может потребовать немедленного layout. Такая гипотеза проверяется trace, а не самим присутствием свойства в коде: если стили не изменились или элемент не участвует в нужной геометрии, вывод будет другим.</p>\n<h2>Минимальный воспроизводимый пример</h2>\n<p>Ниже приведён самодостаточный фрагмент для страницы, где кнопка является карточкой. Сначала поместите разметку в HTML, затем выполните JavaScript после появления кнопки в документе. Строка с <code>offsetWidth</code> оставлена намеренно: она позволяет проверить гипотезу о чтении геометрии после записи. В рабочем коде не добавляйте такое чтение без причины. После первого клика состояние станет <code>card is-ready</code>; для повторного прогона обновите страницу.</p>\n<pre><code>&lt;button id='save-card' class='card is-pending' type='button'&gt;Сохранить&lt;/button&gt;\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', () =&gt; {\n setCardState('card is-pending', 'card is-ready');\n});</code></pre>\n<p>Функциональный результат здесь проверяем отдельно: после клика в Elements должно быть <code>class='card is-ready'</code>. Метки User Timing показывают длительность собственного участка JavaScript и появляются в Performance panel. Они не измеряют весь layout или paint, поэтому запись должна включать сам клик и выбранный диапазон Main track.</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>В один вывод смешаны обработчик, layout и соседние события</td><td>Записать один клик и выделить его interaction</td><td>Сузить trace до действия и одного target</td></tr><tr><td>Длинный участок есть, но владелец неясен</td><td>Не зафиксированы DOM before и after</td><td>Проверить класс в Elements и место записи в коде</td><td>Сначала восстановить контракт, потом искать причину</td></tr><tr><td>После чтения геометрии появился Layout</td><td>Запись класса инвалидировала стили, а чтение потребовало размер</td><td>Сопоставить вызов <code>offsetWidth</code> с выбранным событием</td><td>Разнести запись и чтение либо убрать ненужное чтение; подтвердить повторной записью</td></tr><tr><td>После CSS-правки стало «быстрее»</td><td>Исчез обязательный визуальный результат</td><td>Сверить класс, геометрию и screenshot</td><td>Не принимать сокращённый trace без функциональной проверки</td></tr><tr><td>В записи нет новой работы</td><td>Значения from и to совпали либо выбран не тот элемент</td><td>Проверить no-op и чистое исходное состояние</td><td>Не искать layout там, где не было новой mutation</td></tr><tr><td>Стадии trace отличаются от схемы</td><td>Учебные labels приняли за API браузера</td><td>Сверить документацию версии и фактические события</td><td>Описать наблюдаемую работу без принудительного переименования</td></tr></tbody></table></div>\n<h2>Порядок проверки в Performance panel</h2>\n<ol><li>Опишите симптом как действие и видимый результат: «после клика на <code>save-card</code> интерфейс отвечает неровно, класс должен стать <code>card is-ready</code>».</li><li>Зафиксируйте target, свойство, DOM before и DOM after. Отдельно опишите ожидаемое состояние при ошибке и no-op, когда переход не должен происходить.</li><li>Подготовьте страницу: одинаковые данные, viewport, браузер, cache и настройки throttling. Уберите лишние клики и фоновые действия, но запишите условия запуска.</li><li>Откройте Performance panel, включите запись runtime и выполните один клик. Остановите запись сразу после появления ожидаемого DOM-результата.</li><li>Найдите interaction и узкий диапазон вокруг неё. В Main track проверьте обработчик, User Timing marks и связанные события, а не только самый длинный прямоугольник.</li><li>Выберите одну гипотезу. Для geometry read это конкретный вызов <code>offsetWidth</code>, а не общее утверждение «рендер дорогой».</li><li>Внесите одну обратимую правку: уберите чтение, разделите записи и чтения или измените один selector. Сохраните ожидаемый DOM-result и способ rollback.</li><li>Повторите тот же сценарий в той же среде. Сравните выбранные диапазоны, DOM-result и отрицательный путь. Если trace не подтверждает гипотезу, верните правку и выберите следующий вопрос.</li></ol>\n<h2>Если гипотеза не подтверждается</h2>\n<p>Если DOM после клика не соответствует контракту, остановитесь: вы исследуете не тот путь. Если метка JavaScript есть, а ожидаемого layout или paint в выбранном диапазоне нет, это не доказывает, что рендер «бесплатный». Работа могла быть объединена или перенесена. Если длинный участок лежит вне выбранной interaction, не приписывайте его кнопке без связи с кодом.</p>\n<p>Не отключайте проверку стилей, не удаляйте обязательный визуальный результат и не добавляйте таймер как доказательство. <code>requestAnimationFrame</code> может помочь привязать учебный шаг к очередной возможности отрисовки, но сам callback не объясняет причину задержки. <code>PerformanceObserver</code> наблюдает поддерживаемые performance entries; он не раскрывает весь внутренний pipeline и не заменяет trace DevTools.</p>\n<h2>Ограничения измерения</h2>\n<p>Запись относится к конкретной среде. Версия браузера и DevTools, устройство, viewport, throttling, cache, шрифты, расширения, размер DOM и фоновые задачи меняют длительность и состав работы. Chrome DevTools показывает собственное представление trace; Firefox и Safari могут группировать или называть события иначе. Один локальный прогон не описывает все устройства и не является полевым измерением пользовательского опыта.</p>\n<p>Метки на рисунке и числа в учебной модели не являются миллисекундами, CPU time, FPS, LCP, INP или production-данными. Не переносите результат «стало быстрее» из одного trace на весь продукт. Улучшение принимается только вместе с сохранённым функциональным результатом и повторной записью на нужном сценарии.</p>\n<h2>Критерий готовности</h2>\n<p>Диагностика готова, когда другой инженер может повторить один сценарий в указанной среде, увидеть DOM before и after, найти обработчик и открыть связанный диапазон trace. Для правки записаны одна гипотеза, ожидаемый эффект и rollback. Успешная ветка сохраняет <code>card is-ready</code>, ошибка и no-op проверяются отдельно. Если хотя бы одного артефакта нет, итогом остаётся новая гипотеза, а не заявление «рендер исправлен».</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href='https://html.spec.whatwg.org/multipage/webappapis.html#update-the-rendering' target='_blank' rel='noopener noreferrer'>WHATWG HTML Standard: update the rendering</a> — описывает rendering opportunity, пересчёт стилей и layout, а также обновление интерфейса без обещания фиксированного списка внутренних событий.</li><li><a href='https://developer.chrome.com/docs/devtools/performance/reference' target='_blank' rel='noopener noreferrer'>Chrome DevTools: Performance features reference</a> — описывает runtime-запись, Main track, навигацию по trace и пользовательские настройки записи.</li><li><a href='https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/User_timing' target='_blank' rel='noopener noreferrer'>MDN Web APIs: User Timing</a> — определяет именованные <code>PerformanceMark</code> и измерения <code>PerformanceMeasure</code>, которые используются в примере.</li><li><a href='https://developer.mozilla.org/en-US/docs/Web/API/HTMLElement/offsetWidth' target='_blank' rel='noopener noreferrer'>MDN Web APIs: HTMLElement.offsetWidth</a> — описывает возвращаемую layout-ширину и границы значения свойства.</li></ul>"
}