8 lines
22 KiB
JSON
8 lines
22 KiB
JSON
{
|
||
"index": 208,
|
||
"slug": "editorial-2022-03-field-browser-rendering",
|
||
"title": "Тяжёлый кадр в браузере: как найти причину и проверить обратимую правку",
|
||
"excerpt": "Если интерфейс дёргается, широкий участок Performance trace ещё не объясняет причину. Разбираем один пользовательский шаг через DOM-результат, владельца изменения, узкий диапазон записи и rollback.",
|
||
"contentHtml": "<p>Пользователь нажимает «Сохранить», карточка должна перейти из <code>card is-pending</code> в <code>card is-ready</code>, но интерфейс на мгновение замирает. В Performance panel виден широкий участок main thread с JavaScript, style, layout и paint. Если назвать весь участок «медленным рендером», команда может удалить нужную мутацию, перенести работу в другой callback или оптимизировать не тот экран. Цена ошибки — потерянный визуальный результат, новый дефект вместо ускорения и повторное расследование после релиза.</p>\n<p>Тезис простой: тяжёлый кадр нужно разбирать от наблюдаемого результата к конкретному владельцу изменения. Сначала зафиксируйте, что изменилось в DOM. Затем свяжите одно действие пользователя с узким диапазоном trace. После этого проверьте одну гипотезу маленькой обратимой правкой. Только новая запись, в которой сохранился DOM-результат, позволяет обсуждать эффект правки.</p>\n<h2>Механизм: результат важнее ярлыка в trace</h2>\n<p>Браузер не получает от приложения готовую команду «сделай layout». Код меняет DOM, CSS-классы, inline-стили, текст или геометрию. User agent сам обновляет представление документа и планирует нужные этапы. В trace эти этапы показываются как события выбранной версии браузера. Поэтому слова <code>style</code>, <code>layout</code>, <code>paint</code> и <code>composite</code> помогают сформулировать вопрос, но сами по себе не указывают владельца.</p>\n<p>Владелец находится в коде и в контракте результата. Для примера контракт выглядит так: пользователь выполняет один submit; target карточки остаётся тем же; <code>className</code> меняется с <code>card is-pending</code> на <code>card is-ready</code>; при ошибке класс не меняется; rollback возвращает исходное состояние. Если trace стал короче, но класс больше не меняется, оптимизация не прошла функциональную проверку.</p>\n<pre><code>const before = card.className;\n\nfunction onSave(card, response) {\n if (!response.ok) {\n return { result: 'error', className: before };\n }\n\n const nextClassName = 'card is-ready';\n card.className = nextClassName;\n return { result: 'ready', className: nextClassName };\n}\n\nfunction rollback(card, before) {\n card.className = before;\n}</code></pre>\n<p>Код учебный. Он не измеряет длительность, не вызывает настоящий network request и не доказывает, что браузер нарисовал карточку в конкретном кадре. Его задача — показать границу наблюдения: до оптимизации известны <code>before</code> и <code>after</code>, а у изменения есть явная отмена. В рабочем компоненте тот же контракт нужно связать с реальным обработчиком, состоянием ошибки и снимком DOM.</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>В запись попали соседние timers, сеть или расширение</td><td>Повторить один submit на чистом экране и выделить его interaction</td><td>Отделить диапазон действия от соседних событий</td></tr><tr><td>Trace широкий, причина неясна</td><td>Ищут общий ярлык вместо связи с DOM-результатом</td><td>Сравнить target, property, before и after</td><td>Вернуться к mutation и назвать одного владельца</td></tr><tr><td>После правки график спокойнее, класс не меняется</td><td>Удалили функциональную мутацию вместе с лишней работой</td><td>Проверить DOM after тем же сценарием</td><td>Откатить правку и разделить визуальный результат и оптимизацию</td></tr><tr><td>Появился layout, но действие не меняло геометрию</td><td>Проверяется не тот range или другой код прочитал geometry</td><td>Найти вызов чтения размеров и его стек</td><td>Проверить один geometry read, а не весь layout</td></tr><tr><td>Повторная запись даёт другой trace</td><td>Изменились CPU, viewport, cache, данные или версия браузера</td><td>Зафиксировать среду и повторить сценарий несколько раз</td><td>Сравнивать только согласованные условия и сохранять запись</td></tr><tr><td>«Улучшение» нельзя отменить</td><td>Не определены старое состояние и путь rollback</td><td>Вернуть before отдельным запуском и проверить DOM</td><td>Сделать изменение локальным, переключаемым или отдельным commit</td></tr></tbody></table></div>\n<h2>Пример: одна interaction и один DOM-result</h2>\n<p>Представим форму сохранения карточки. До действия в DOM есть <code><article class=\"card is-pending\"></code>. Пользователь нажимает кнопку. Обработчик отправляет данные и после успешного ответа добавляет <code>is-ready</code>. В учебном сценарии тяжёлую работу связывают с чтением геометрии сразу после записи стиля. Это гипотеза, а не утверждение о любом браузере: она должна быть подтверждена стеком вызовов и новой записью.</p>\n<pre><code>function updateCard(card) {\n card.classList.add('is-ready');\n const height = card.getBoundingClientRect().height;\n logLayoutDependentValue(height);\n}</code></pre>\n<p>Такой порядок может потребовать дополнительной работы по обновлению стилей и layout, но по одному фрагменту нельзя заключить, что именно он вызвал задержку. Возможно, размер уже был нужен другому коду. Возможно, запись попала в другой пользовательский сценарий. Проверка должна ответить на узкий вопрос: связан ли этот обработчик и этот read с выбранным участком записи, и сохраняется ли <code>is-ready</code> после изменения?</p>\n<p>Отрицательный путь обязателен. Если сервер вернул ошибку, карточка должна остаться <code>is-pending</code>, сообщение об ошибке должно остаться доступным пользователю, а trace нельзя объявлять успешным только потому, что в нём стало меньше событий. Если выбранный элемент уже имел <code>is-ready</code>, действие может оказаться no-op. Тогда новая запись не проверяет переход состояния: сначала нужно вернуть исходный state и повторить сценарий.</p>\n<figure><img src=\"/assets/editorial/2022/browser-rendering-diagnosis-rollback-2022.svg\" alt=\"Схема диагностики тяжёлого кадра: DOM-результат, interaction, вопросы к trace, обратимая правка и rollback\" loading=\"lazy\" /><figcaption>Учебная схема связывает DOM before/after с одной interaction и одной гипотезой. Ветка no-op останавливает разбор, а rollback возвращает исходный результат. Схема не является снимком DevTools trace и не содержит production-метрик.</figcaption></figure>\n<h2>Как читать Performance panel</h2>\n<p>Начинайте запись на уже подготовленной странице. Выполните одно действие и остановите запись сразу после появления ожидаемого результата. Не включайте в один профиль загрузку, несколько кликов и длинный период ожидания. Чистое воспроизведение не делает измерение идеальным, но уменьшает число несвязанных событий.</p>\n<p>После записи сначала найдите пользовательский шаг, а не самый длинный прямоугольник. Выделите interaction и проверьте, какие вызовы находятся под ней на main thread. Затем сопоставьте их с обработчиком. Скриншот кадра помогает увидеть, что видел пользователь, но не доказывает причину. Summary показывает разбиение работы, а flame chart — порядок событий и стек вызовов. Ни один из этих видов не заменяет сравнение DOM before/after.</p>\n<p>Если подозрение связано со стилями или геометрией, ограничьте вопрос. «Почему layout длинный?» слишком широко. «Какой вызов прочитал геометрию после записи класса в этом обработчике?» уже можно проверить. Если выбранный range не содержит такого обработчика, остановитесь: trace и код не связаны. Если mutation не даёт ожидаемый DOM-result, остановитесь ещё раньше.</p>\n<h2>Обратимая правка</h2>\n<p>У первой правки должны быть четыре поля: один owner, ожидаемый DOM after, способ проверить trace и путь rollback. Например, временно вынести один geometry read из обработчика, локально выключить визуальный эффект через feature toggle или изменить один selector. Не меняйте одновременно рендер списка, кеш, анимацию и порядок сетевых запросов. Иначе новая запись не скажет, какое изменение повлияло на результат.</p>\n<p>После правки повторите тот же сценарий в той же среде. Проверьте две вещи: карточка по-прежнему получает <code>is-ready</code>, а выбранный участок записи изменился так, как предполагала гипотеза. Если DOM сохранился, но trace не подтверждает гипотезу, это не провал. Это отрицательный результат: выберите другой owner или верните код. Если DOM сломался, rollback должен вернуть исходное поведение, а не просто удалить строку из diff.</p>\n<pre><code>const baseClassName = 'card is-pending';\n\nfunction applyForward(card) {\n card.className = 'card is-ready';\n}\n\nfunction applyRollback(card) {\n card.className = baseClassName;\n}\n\nfunction assertResult(card, expected) {\n if (card.className !== expected) {\n throw new Error(`unexpected DOM result: ${card.className}`);\n }\n}</code></pre>\n<p>Этот пример проверяет только наблюдаемое состояние. Он не измеряет FPS, не моделирует планировщик и не доказывает отсутствие layout. Production-решение требует реальной записи и подходящего набора пользовательских сценариев. Учебный код полезен как каркас вопроса, но не как evidence.</p>\n<h2>Порядок действий</h2>\n<ol><li><strong>Опишите симптом.</strong> Запишите действие, видимый сбой и цену ошибки. Например: «после submit карточка появляется с задержкой, а при ошибке состояние остаётся неясным».</li><li><strong>Зафиксируйте контракт.</strong> Сохраните target, property, DOM before, DOM after для успеха и DOM after для ошибки. Укажите, что считается no-op.</li><li><strong>Подготовьте среду.</strong> Зафиксируйте браузер, viewport, CPU throttling, данные, cache и наличие расширений. Снимите запись на чистом экране.</li><li><strong>Запишите одну interaction.</strong> Выполните один пользовательский шаг. Остановите запись сразу после visual result. Не смешивайте несколько гипотез.</li><li><strong>Свяжите trace с кодом.</strong> Найдите обработчик, mutation и выбранный range. Если связи нет, не называйте причину.</li><li><strong>Сформулируйте гипотезу.</strong> Назовите одного owner и один проверяемый вопрос: например, влияет ли конкретный geometry read на работу после mutation.</li><li><strong>Внесите одну обратимую правку.</strong> Измените небольшой участок, сохраните способ rollback и не меняйте контракт результата.</li><li><strong>Повторите запись.</strong> Сравните согласованные диапазоны, но не переносите synthetic units или локальные числа на production.</li><li><strong>Проверьте обе ветки.</strong> Успешный путь должен дать ожидаемый DOM after. Ошибка и no-op не должны маскироваться под успешный переход.</li><li><strong>Зафиксируйте вывод.</strong> Оставьте среду, шаг, before/after, ссылку на код, range, гипотезу, результат повторной записи и способ rollback.</li></ol>\n<h2>Ограничения</h2>\n<p>Разбор Performance panel относится к конкретной среде. Браузер, версия DevTools, устройство, throttling, размер viewport, расширения, cache, данные и состояние страницы меняют запись. Даже повтор одного сценария может дать другой порядок и длительность работы. Поэтому нельзя переносить вывод из Chrome на Firefox или Safari без отдельной проверки и нельзя считать локальную запись полевым измерением пользовательского опыта.</p>\n<p>Названия этапов в trace не образуют универсальный авторский pipeline. HTML Standard описывает поведение user agent, а DevTools показывает представление записи конкретного инструмента. <code>requestAnimationFrame</code> не является гарантией, что весь нужный rendering завершится до следующей строки. CSS-анимация, compositor, изображения, шрифты, layout containment, сеть и расширения могут изменить картину.</p>\n<p>Не объявляйте улучшением уменьшение одного участка без функциональной проверки. Работа может переместиться в другой callback, появиться при другом viewport или проявиться только на слабом устройстве. Если после правки исчезла карточка, сообщение или error state, более короткая timeline ничего не доказывает. Если rollback возвращает только класс, но не возвращает сетевой или серверный результат, это локальная отмена, а не полный rollback операции.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Исследование готово, когда другой инженер может повторить один сценарий в указанной среде, увидеть DOM before/after, открыть выбранный range и найти связь с конкретной mutation. Для правки записаны owner, ожидаемый результат, наблюдаемый эффект и rollback. Успешная ветка сохраняет функциональный DOM-result. Ошибка и no-op проходят отдельно и не выдаются за успешное изменение. Если этих артефактов нет, формулировка «ускорили рендер» остаётся предположением.</p>\n<p>Минимальный итог можно проверить четырьмя вопросами: что сделал пользователь; какой DOM-результат изменился; какой код-владелец связан с range; что произошло после обратимой правки. На каждый вопрос должна быть ссылка на повторяемый шаг или сохранённую запись. Числа из учебного примера, цвет блока в trace и субъективное ощущение плавности не заменяют эту проверку.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://developer.chrome.com/docs/devtools/performance\" target=\"_blank\" rel=\"noopener noreferrer\">Chrome for Developers: Analyze runtime performance</a> — официальная инструкция по записи runtime-профиля, чтению FPS/CPU и поиску узкого места в Performance panel.</li><li><a href=\"https://developer.chrome.com/docs/devtools/performance/reference\" target=\"_blank\" rel=\"noopener noreferrer\">Chrome for Developers: Performance features reference</a> — актуальное описание режимов записи и элементов Performance panel; интерфейс зависит от версии Chrome.</li><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 user agent, без обещания фиксированной внутренней последовательности для всех браузеров.</li></ul>"
|
||
}
|