8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"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><button id='save-card' class='card is-pending' type='button'>Сохранить</button>\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', () => {\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>"
|
||
}
|