diff --git a/editorial/agent-rewrites/303.json b/editorial/agent-rewrites/303.json index baf997c..b326c83 100644 --- a/editorial/agent-rewrites/303.json +++ b/editorial/agent-rewrites/303.json @@ -3,5 +3,5 @@ "slug": "editorial-2019-08-practice-frontend-performance", "title": "Первая загрузка: как найти владельца задержки", "excerpt": "Пустой первый экран не объясняется словом «тяжёлая страница». Разбираем сеть, CSS, JavaScript и изображения по отдельным проверкам и выбираем одно изменение с воспроизводимым критерием.", - "contentHtml": "
Симптом знаком: сервер отвечает быстро, но пользователь видит белый экран или неподвижную карточку. В Network нет огромного файла, а кнопка появляется после долгой паузы. Команда уменьшает бандл, добавляет кэш или меняет формат картинок. Следующий запуск выглядит почти так же. Ошибка в диагнозе: слово «медленно» скрывает несколько разных задержек. Цена ошибки — релиз без доказанного эффекта, лишняя сложность в критическом пути и потерянное время пользователя до первого полезного действия.
\nТезис простой: первую загрузку нужно измерять по владельцам работы, а не по одному событию load и не по размеру gzip-файла. В одном воспроизводимом сценарии отдельно проверяют документ и сеть, обнаружение ресурсов, CSS, JavaScript на главном потоке, декодирование изображения и paint. Эти работы могут идти параллельно. Их нельзя бездумно сложить в одну сумму. Профиль нужен не для красивого числа, а для следующего проверяемого вопроса.
«Страница загрузилась» — слабое условие. Для диагностики нужно назвать результат глазами пользователя: например, «на экране видны заголовок товара, цена и кнопка заказа». Такой результат имеет границу. Он может наступить до load, если второстепенные изображения ещё загружаются, и после DOMContentLoaded, если приложение только начинает строить DOM.
Navigation Timing описывает путь текущего документа: ответ, построение DOM, обработчики DOMContentLoaded и load. Resource Timing описывает отдельные ресурсы. Ни один из этих интерфейсов сам по себе не говорит, что конкретный блок уже виден и готов к действию. Для этого нужны screenshot в trace, DOM и участок main thread рядом с моментом появления блока.
Современный термин LCP удобен как словарь для крупного элемента в viewport, но он не заменяет старый trace и не даёт права придумывать значение метрики. Если замера нет, нужно писать «проверяем момент появления hero» и сохранять условия запуска. Лабораторный профиль отвечает на вопрос о выбранном сценарии. Он не описывает распределение опыта всех пользователей.
\nДокумент может прийти рано, но скрыть критический URL за JavaScript. CSS может загрузиться, но затем длинный layout задержит отрисовку. JavaScript может быть маленьким в передаче, но занять главный поток на parse, compile и execute. Изображение может завершить передачу, но ждать decode, стилей или свободного main thread. Одинаковый симптом требует разных действий.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| HTML быстро пришёл, экран пустой | Критический CSS, DOM или изображение обнаруживается поздно | Сравнить responseStart документа, первый screenshot и старт ресурсов | Вывести минимальный каркас и критический URL в начальный HTML либо найти блокирующую работу |
| Длинный scripting после app.js | Синхронная инициализация, большой список или сторонний код | Открыть main-thread trace и привязать участок к функции | Отложить второстепенный путь, сократить работу до первого экрана и повторить профиль |
| Hero скачан, но не виден | Decode, style, layout или paint ждут занятый главный поток | Сопоставить URL с screenshot, decode и paint | Проверить реальные пиксели и момент отрисовки; менять формат только при подтверждённой цене decode |
| Водопад долго не затихает | После первого экрана идут независимые второстепенные ресурсы | Отметить, какие запросы нужны выбранному экрану | Не оптимизировать idle как замену полезности; отложить запросы, которые не помогают действию |
Таблица задаёт порядок расследования. Она не обещает, что одна причина окажется единственной. Важна связь между наблюдением и проверкой. Если действие не меняет именно участок задержки, оно не подтверждает гипотезу.
\nЗапишите URL, действие пользователя, viewport, состояние кэша, Service Worker, ограничение CPU и сети, версию сборки и определение полезного экрана. Не смешивайте в одном сравнении холодный и тёплый кэш. Не меняйте одновременно URL, сборку и throttling. Иначе новый профиль нельзя будет честно сравнить со старым.
\nОдного локального запуска достаточно для поиска участка работы, но недостаточно для утверждения о production. Расширение viewport, реальные устройства, фоновые вкладки и сеть меняют результат. После локального эксперимента нужен отдельный полевой источник, если команда принимает решение о приоритете по пользовательскому эффекту. Не выдавайте один trace за статистику.
\nconst navigation = performance.getEntriesByType('navigation')[0];\nconst resources = performance.getEntriesByType('resource').map((entry) => ({\n path: new URL(entry.name).pathname,\n initiator: entry.initiatorType,\n duration: Math.round(entry.duration),\n transferSize: entry.transferSize,\n encodedBodySize: entry.encodedBodySize,\n}));\n\nconsole.table({\n responseStart: Math.round(navigation?.responseStart ?? 0),\n domInteractive: Math.round(navigation?.domInteractive ?? 0),\n domContentLoaded: Math.round(navigation?.domContentLoadedEventEnd ?? 0),\n});\nconsole.table(resources);\nКод — учебный пример для текущего документа. Он не измеряет время выполнения JavaScript, CSSOM, layout или paint. Он помогает увидеть границы Navigation Timing и ресурсы, доступные браузеру. Поля размера и сетевых этапов могут быть ограничены для другого origin без Timing-Allow-Origin. Переиспользованное соединение и политика доступа также объясняют нулевые отдельные значения. Ноль не доказывает отсутствие сети.
В общий лог нельзя бездумно отправлять полный URL: query-параметры могут содержать идентификаторы и пользовательские данные. Для отчёта оставьте путь, тип инициатора, округлённые значения, хэш сборки и условия. Сохраните screenshot и ссылку на участок trace. Результат должен позволить другому инженеру проверить тот же вопрос, а не только поверить выводу.
\nУ сетевой задержки есть обнаружение, очередь, соединение, запрос и передача. Размер файла влияет на передачу, но не объясняет поздний старт. Если hero появляется только после boot-кода, сжатие картинки не устранит задержку обнаружения. Если документ ждёт redirect, правка JavaScript не сократит этот переход. Сначала найдите URL на waterfall и сравните его старт с моментом чтения HTML.
\nКритический ресурс должен быть виден там, где браузер может его обнаружить. Для hero это может быть обычный img в начальной разметке. Для стилей — прямой link. preload применяйте только к ресурсу, который доказанно нужен первому экрану. Лишний preload создаёт конкурента для документа или CSS. loading=\"lazy\" у первого значимого изображения также нельзя ставить по привычке: атрибут намеренно откладывает запрос.
Первый способ — позднее обнаружение или получение файла. Второй — работа после получения: parse, compile, execute, создание DOM и повторные проходы style/layout. В Network эти случаи могут выглядеть одинаково. В Performance trace они различаются. Длинный scripting сразу после app.js указывает на работу главного потока. Поздний старт app.js указывает на обнаружение, очередь или сеть.
\nУчебный пример: приложение получает данные каталога, строит 500 строк и сразу измеряет каждый узел, меняя класс после каждого чтения. Такой код может быть компактным, но вызывает чередование чтения и записи и несколько layout-проходов. Без trace нельзя утверждать, что именно этот фрагмент виноват. Проверка — найти функцию в main thread, отложить строки за пределы первого экрана или сгруппировать операции, а затем повторить тот же сценарий.
\nperformance.mark('catalog:render-start');\nrenderCatalog(initialItems);\nperformance.mark('catalog:render-end');\nperformance.measure(\n 'catalog:initial-render',\n 'catalog:render-start',\n 'catalog:render-end',\n); \nperformance.mark и performance.measure добавляют ориентиры для собственного кода. Они не ускоряют работу и не заменяют trace. Не ставьте отметку после всех запросов, если хотите понять первый экран. Отмечайте узкий участок и проверяйте, что он действительно относится к выбранному действию.
CSS участвует в визуальной готовности. Браузеру нужны правила, чтобы рассчитать размеры и положение элементов. Внешняя таблица может завершиться поздно, а большой DOM — растянуть style и layout уже после её прихода. Поэтому фраза «HTML есть» не означает «экран готов». Ищите в trace окончание CSS, style/layout и первый screenshot.
\nУ изображения есть размер ответа, декодирование, место в layout и paint. Большой desktop-файл на маленьком viewport создаёт лишнюю работу даже при быстрой сети. Но менять формат или добавлять preload стоит после связи URL с конкретным визуальным элементом. Картинка, которая не нужна до первого действия, не должна конкурировать с CSS и скриптом только потому, что она заметна в исходнике.
\nИллюстрация — схема механизма, а не запись реального сайта. Она помогает не потерять decode и paint после responseEnd. Для реального вывода нужен trace с тем же URL и условиями.
Иногда ответ HTML быстрый, критические ресурсы стартуют рано, а полезный экран всё равно не появляется. Тогда не нужно повторно сжимать сеть. Проверьте, не скрывает ли приложение разметку до завершения инициализации, не падает ли обработчик, не ждёт ли UI данные, которые не нужны для каркаса, и не занял ли главный поток сторонний код. Быстрый сервер не отменяет ошибки рендера.
\nНельзя объявлять универсальный бюджет по одному локальному запуску. Нельзя складывать параллельные полосы trace как последовательные этапы. Нельзя считать load эквивалентом видимости, transferSize — стоимостью JavaScript, а responseEnd — моментом paint. Данные другого origin могут быть неполными. Современные метрики зависят от браузера, viewport и фактического элемента. Эти ограничения не отменяют диагностику; они задают честные границы вывода.
Изменение готово к следующему этапу, если на том же URL и при тех же зафиксированных условиях повторный профиль показывает более раннее появление выбранного полезного экрана, а участок названного владельца уменьшился или исчез. При этом не появился новый блокирующий участок: например, scripting не просто сменился на поздний запрос, а hero не стал видимым ценой пустого каркаса. Если критерий не выполнен, верните изменение или сформулируйте новую гипотезу. «Стало быстрее» без привязанного участка и условия ничего не доказывает.
\nТак первая загрузка превращается из жалобы в короткий эксперимент. Вы не угадываете виновника по размеру файла. Вы фиксируете симптом, находите границу механизма, меняете один критический путь и проверяете тот же экран повторно.
\nВ учебном сценарии на дежурстве инженер открывает страницу товара: серверный ответ приходит быстро, но пользователь видит белый экран или неподвижную карточку. В Network нет огромного файла, а кнопка появляется после долгой паузы. Сначала команда уменьшает бандл, добавляет кэш или меняет формат картинок. Следующий запуск выглядит почти так же. Ошибка в диагнозе: слово «медленно» скрывает несколько разных задержек. Цена ошибки — релиз без доказанного эффекта, лишняя сложность в критическом пути и потерянное время пользователя до первого полезного действия.
\nТезис простой: первую загрузку нужно измерять по владельцам работы, а не по одному событию load и не по размеру gzip-файла. В одном воспроизводимом сценарии отдельно проверяют документ и сеть, обнаружение ресурсов, CSS, JavaScript на главном потоке, декодирование изображения и paint. Эти работы могут идти параллельно. Их нельзя бездумно сложить в одну сумму. Профиль нужен не для красивого числа, а для следующего проверяемого вопроса.
«Страница загрузилась» — слабое условие. Для диагностики нужно назвать результат глазами пользователя: например, «на экране видны заголовок товара, цена и кнопка заказа». Такой результат имеет границу. Он может наступить до load, если второстепенные изображения ещё загружаются, и после DOMContentLoaded, если приложение только начинает строить DOM.
Navigation Timing описывает путь текущего документа: ответ, построение DOM, обработчики DOMContentLoaded и load. Resource Timing описывает отдельные ресурсы. Ни один из этих интерфейсов сам по себе не говорит, что конкретный блок уже виден и готов к действию. Для этого нужны screenshot в trace, DOM и участок main thread рядом с моментом появления блока.
Современный термин LCP удобен как словарь для крупного элемента в viewport, но он не заменяет старый trace и не даёт права придумывать значение метрики. Если замера нет, нужно писать «проверяем момент появления hero» и сохранять условия запуска. Лабораторный профиль отвечает на вопрос о выбранном сценарии. Он не описывает распределение опыта всех пользователей.
\nДокумент может прийти рано, но скрыть критический URL за JavaScript. CSS может загрузиться, но затем длинный layout задержит отрисовку. JavaScript может быть маленьким в передаче, но занять главный поток на parse, compile и execute. Изображение может завершить передачу, но ждать decode, стилей или свободного main thread. Одинаковый симптом требует разных действий.
\n| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| HTML быстро пришёл, экран пустой | Критический CSS, DOM или изображение обнаруживается поздно | Сравнить responseStart документа, первый screenshot и старт ресурсов | Вывести минимальный каркас и критический URL в начальный HTML либо найти блокирующую работу |
| Длинный scripting после app.js | Синхронная инициализация, большой список или сторонний код | Открыть main-thread trace и привязать участок к функции | Отложить второстепенный путь, сократить работу до первого экрана и повторить профиль |
| Hero скачан, но не виден | Decode, style, layout или paint ждут занятый главный поток | Сопоставить URL с screenshot, decode и paint | Проверить реальные пиксели и момент отрисовки; менять формат только при подтверждённой цене decode |
| Водопад долго не затихает | После первого экрана идут независимые второстепенные ресурсы | Отметить, какие запросы нужны выбранному экрану | Не оптимизировать idle как замену полезности; отложить запросы, которые не помогают действию |
Таблица задаёт порядок расследования. Она не обещает, что одна причина окажется единственной. Важна связь между наблюдением и проверкой. Если действие не меняет именно участок задержки, оно не подтверждает гипотезу.
\nЗапишите URL, действие пользователя, viewport, состояние кэша, Service Worker, ограничение CPU и сети, версию сборки и определение полезного экрана. Не смешивайте в одном сравнении холодный и тёплый кэш. Не меняйте одновременно URL, сборку и throttling. Иначе новый профиль нельзя будет честно сравнить со старым.
\nОдного локального запуска достаточно для поиска участка работы, но недостаточно для утверждения о production. Расширение viewport, реальные устройства, фоновые вкладки и сеть меняют результат. После локального эксперимента нужен отдельный полевой источник, если команда принимает решение о приоритете по пользовательскому эффекту. Не выдавайте один trace за статистику.
\nperformance.setResourceTimingBufferSize?.(1000);\nconst navigation = performance.getEntriesByType('navigation')[0];\nconst resources = performance.getEntriesByType('resource').map((entry) => ({\n path: new URL(entry.name).pathname,\n initiator: entry.initiatorType,\n duration: Math.round(entry.duration),\n transferSize: entry.transferSize,\n encodedBodySize: entry.encodedBodySize,\n}));\n\nconsole.table({\n responseStart: Math.round(navigation?.responseStart ?? 0),\n domInteractive: Math.round(navigation?.domInteractive ?? 0),\n domContentLoaded: Math.round(navigation?.domContentLoadedEventEnd ?? 0),\n});\nconsole.table(resources);\nКод — учебный пример для текущего документа. Выполните его до Reload, если хотите увеличить буфер ресурсных записей: это не восстановит записи, которые уже были вытеснены. Фрагмент не измеряет время выполнения JavaScript, CSSOM, layout или paint. Он помогает увидеть границы Navigation Timing и ресурсы, доступные браузеру. Поля размера и сетевых этапов могут быть ограничены для другого origin без Timing-Allow-Origin. Переиспользованное соединение и политика доступа также объясняют нулевые отдельные значения. Ноль не доказывает отсутствие сети.
В общий лог нельзя бездумно отправлять полный URL: query-параметры могут содержать идентификаторы и пользовательские данные. Для отчёта оставьте путь, тип инициатора, округлённые значения, хэш сборки и условия. Сохраните screenshot и ссылку на участок trace. Результат должен позволить другому инженеру проверить тот же вопрос, а не только поверить выводу.
\nУ сетевой задержки есть обнаружение, очередь, соединение, запрос и передача. Размер файла влияет на передачу, но не объясняет поздний старт. Если hero появляется только после boot-кода, сжатие картинки не устранит задержку обнаружения. Если документ ждёт redirect, правка JavaScript не сократит этот переход. Сначала найдите URL на waterfall и сравните его старт с моментом чтения HTML.
\nКритический ресурс должен быть виден там, где браузер может его обнаружить. Для hero это может быть обычный img в начальной разметке. Для стилей — прямой link. preload применяйте только к ресурсу, который доказанно нужен первому экрану. Лишний preload создаёт конкурента для документа или CSS. loading=\"lazy\" у первого значимого изображения также нельзя ставить по привычке: атрибут намеренно откладывает запрос.
Первый способ — позднее обнаружение или получение файла. Второй — работа после получения: parse, compile, execute, создание DOM и повторные проходы style/layout. В Network эти случаи могут выглядеть одинаково. В Performance trace они различаются. Длинный scripting сразу после app.js указывает на работу главного потока. Поздний старт app.js указывает на обнаружение, очередь или сеть.
\nУчебный пример: приложение получает данные каталога, строит 500 строк и сразу измеряет каждый узел, меняя класс после каждого чтения. Такой код может быть компактным, но вызывает чередование чтения и записи и несколько layout-проходов. Без trace нельзя утверждать, что именно этот фрагмент виноват. Проверка — найти функцию в main thread, отложить строки за пределы первого экрана или сгруппировать операции, а затем повторить тот же сценарий.
\nconst catalogRoot = document.querySelector('#catalog');\nconst initialItems = Array.from({ length: 500 }, (_, id) => ({ id }));\nconst fragment = document.createDocumentFragment();\n\nperformance.mark('catalog:render-start');\nfor (const item of initialItems) {\n const row = document.createElement('div');\n row.textContent = `Товар ${item.id}`;\n fragment.append(row);\n}\ncatalogRoot?.replaceChildren(fragment);\nperformance.mark('catalog:render-end');\nperformance.measure(\n 'catalog:initial-render',\n 'catalog:render-start',\n 'catalog:render-end',\n);\nconsole.table(performance.getEntriesByName('catalog:initial-render').map(({ duration }) => ({\n durationMs: Math.round(duration),\n})));\nПеред запуском добавьте в страницу элемент <div id=\"catalog\"></div> или оставьте его отсутствие, чтобы измерить только построение фрагмента. performance.mark и performance.measure добавляют ориентиры для собственного кода. Они не ускоряют работу и не заменяют trace. Не ставьте отметку после всех запросов, если хотите понять первый экран. Отмечайте узкий участок и проверяйте, что он действительно относится к выбранному действию.
CSS участвует в визуальной готовности. Браузеру нужны правила, чтобы рассчитать размеры и положение элементов. Внешняя таблица может завершиться поздно, а большой DOM — растянуть style и layout уже после её прихода. Поэтому фраза «HTML есть» не означает «экран готов». Ищите в trace окончание CSS, style/layout и первый screenshot.
\nУ изображения есть размер ответа, декодирование, место в layout и paint. Большой desktop-файл на маленьком viewport создаёт лишнюю работу даже при быстрой сети. Но менять формат или добавлять preload стоит после связи URL с конкретным визуальным элементом. Картинка, которая не нужна до первого действия, не должна конкурировать с CSS и скриптом только потому, что она заметна в исходнике.
\nИллюстрация — схема механизма, а не запись реального сайта. Она помогает не потерять decode и paint после responseEnd. Для реального вывода нужен trace с тем же URL и условиями.
На странице из начала разбора не выбирайте исправление по названию участка. Если trace покажет поздний старт URL, меняйте обнаружение ресурса; если URL стартовал рано, но scripting блокирует экран, меняйте порядок работы; если байты уже пришли, проверяйте decode, style, layout и paint. После одного изменения ожидаемый результат — более ранний полезный экран без нового блокирующего участка. Это критерий повторного профиля, а не утверждение о заранее известном виновнике.
\nИногда ответ HTML быстрый, критические ресурсы стартуют рано, а полезный экран всё равно не появляется. Тогда не нужно повторно сжимать сеть. Проверьте, не скрывает ли приложение разметку до завершения инициализации, не падает ли обработчик, не ждёт ли UI данные, которые не нужны для каркаса, и не занял ли главный поток сторонний код. Быстрый сервер не отменяет ошибки рендера.
\nНельзя объявлять универсальный бюджет по одному локальному запуску. Нельзя складывать параллельные полосы trace как последовательные этапы. Нельзя считать load эквивалентом видимости, transferSize — стоимостью JavaScript, а responseEnd — моментом paint. Данные другого origin могут быть неполными. Современные метрики зависят от браузера, viewport и фактического элемента. Эти ограничения не отменяют диагностику; они задают честные границы вывода.
Изменение готово к следующему этапу, если на том же URL и при тех же зафиксированных условиях повторный профиль показывает более раннее появление выбранного полезного экрана, а участок названного владельца уменьшился или исчез. При этом не появился новый блокирующий участок: например, scripting не просто сменился на поздний запрос, а hero не стал видимым ценой пустого каркаса. Если критерий не выполнен, верните изменение или сформулируйте новую гипотезу. «Стало быстрее» без привязанного участка и условия ничего не доказывает.
\nТак первая загрузка превращается из жалобы в короткий эксперимент. Вы не угадываете виновника по размеру файла. Вы фиксируете симптом, находите границу механизма, меняете один критический путь и проверяете тот же экран повторно.
\nresponseStart, DOM-события и load.responseStart, responseEnd, transferSize и ограничения Timing-Allow-Origin.