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. Эти работы могут идти параллельно. Их нельзя бездумно сложить в одну сумму. Профиль нужен не для красивого числа, а для следующего проверяемого вопроса.

\n

Сначала определяем полезный экран

\n

«Страница загрузилась» — слабое условие. Для диагностики нужно назвать результат глазами пользователя: например, «на экране видны заголовок товара, цена и кнопка заказа». Такой результат имеет границу. Он может наступить до load, если второстепенные изображения ещё загружаются, и после DOMContentLoaded, если приложение только начинает строить DOM.

\n

Navigation Timing описывает путь текущего документа: ответ, построение DOM, обработчики DOMContentLoaded и load. Resource Timing описывает отдельные ресурсы. Ни один из этих интерфейсов сам по себе не говорит, что конкретный блок уже виден и готов к действию. Для этого нужны screenshot в trace, DOM и участок main thread рядом с моментом появления блока.

\n

Современный термин LCP удобен как словарь для крупного элемента в viewport, но он не заменяет старый trace и не даёт права придумывать значение метрики. Если замера нет, нужно писать «проверяем момент появления hero» и сохранять условия запуска. Лабораторный профиль отвечает на вопрос о выбранном сценарии. Он не описывает распределение опыта всех пользователей.

\n

Разводим задержку по механизмам

\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

Таблица задаёт порядок расследования. Она не обещает, что одна причина окажется единственной. Важна связь между наблюдением и проверкой. Если действие не меняет именно участок задержки, оно не подтверждает гипотезу.

\n

Фиксируем условия до Reload

\n

Запишите URL, действие пользователя, viewport, состояние кэша, Service Worker, ограничение CPU и сети, версию сборки и определение полезного экрана. Не смешивайте в одном сравнении холодный и тёплый кэш. Не меняйте одновременно URL, сборку и throttling. Иначе новый профиль нельзя будет честно сравнить со старым.

\n

Одного локального запуска достаточно для поиска участка работы, но недостаточно для утверждения о production. Расширение viewport, реальные устройства, фоновые вкладки и сеть меняют результат. После локального эксперимента нужен отдельный полевой источник, если команда принимает решение о приоритете по пользовательскому эффекту. Не выдавайте один trace за статистику.

\n
const 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. Переиспользованное соединение и политика доступа также объясняют нулевые отдельные значения. Ноль не доказывает отсутствие сети.

\n

В общий лог нельзя бездумно отправлять полный URL: query-параметры могут содержать идентификаторы и пользовательские данные. Для отчёта оставьте путь, тип инициатора, округлённые значения, хэш сборки и условия. Сохраните screenshot и ссылку на участок trace. Результат должен позволить другому инженеру проверить тот же вопрос, а не только поверить выводу.

\n

Сеть отвечает только за свой участок

\n

У сетевой задержки есть обнаружение, очередь, соединение, запрос и передача. Размер файла влияет на передачу, но не объясняет поздний старт. Если hero появляется только после boot-кода, сжатие картинки не устранит задержку обнаружения. Если документ ждёт redirect, правка JavaScript не сократит этот переход. Сначала найдите URL на waterfall и сравните его старт с моментом чтения HTML.

\n

Критический ресурс должен быть виден там, где браузер может его обнаружить. Для hero это может быть обычный img в начальной разметке. Для стилей — прямой link. preload применяйте только к ресурсу, который доказанно нужен первому экрану. Лишний preload создаёт конкурента для документа или CSS. loading=\"lazy\" у первого значимого изображения также нельзя ставить по привычке: атрибут намеренно откладывает запрос.

\n

JavaScript задерживает экран двумя способами

\n

Первый способ — позднее обнаружение или получение файла. Второй — работа после получения: parse, compile, execute, создание DOM и повторные проходы style/layout. В Network эти случаи могут выглядеть одинаково. В Performance trace они различаются. Длинный scripting сразу после app.js указывает на работу главного потока. Поздний старт app.js указывает на обнаружение, очередь или сеть.

\n

Учебный пример: приложение получает данные каталога, строит 500 строк и сразу измеряет каждый узел, меняя класс после каждого чтения. Такой код может быть компактным, но вызывает чередование чтения и записи и несколько layout-проходов. Без trace нельзя утверждать, что именно этот фрагмент виноват. Проверка — найти функцию в main thread, отложить строки за пределы первого экрана или сгруппировать операции, а затем повторить тот же сценарий.

\n
performance.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); 
\n

performance.mark и performance.measure добавляют ориентиры для собственного кода. Они не ускоряют работу и не заменяют trace. Не ставьте отметку после всех запросов, если хотите понять первый экран. Отмечайте узкий участок и проверяйте, что он действительно относится к выбранному действию.

\n

CSS и изображение заканчиваются после передачи байтов

\n

CSS участвует в визуальной готовности. Браузеру нужны правила, чтобы рассчитать размеры и положение элементов. Внешняя таблица может завершиться поздно, а большой DOM — растянуть style и layout уже после её прихода. Поэтому фраза «HTML есть» не означает «экран готов». Ищите в trace окончание CSS, style/layout и первый screenshot.

\n

У изображения есть размер ответа, декодирование, место в layout и paint. Большой desktop-файл на маленьком viewport создаёт лишнюю работу даже при быстрой сети. Но менять формат или добавлять preload стоит после связи URL с конкретным визуальным элементом. Картинка, которая не нужна до первого действия, не должна конкурировать с CSS и скриптом только потому, что она заметна в исходнике.

\n
\"Четыре
Профиль показывает самостоятельных владельцев времени. Дорожки пересекаются, поэтому их длительности не образуют готовую сумму.
\n

Иллюстрация — схема механизма, а не запись реального сайта. Она помогает не потерять decode и paint после responseEnd. Для реального вывода нужен trace с тем же URL и условиями.

\n

Порядок действий

\n
  1. Назовите полезный экран и действие, которое должно стать доступным.
  2. Зафиксируйте URL, сборку, viewport, кэш, Service Worker и ограничения CPU и сети.
  3. Снимите trace с Network, main thread и screenshot. Не меняйте код до первого профиля.
  4. Отметьте момент ответа HTML, старт критических ресурсов, длинные участки scripting, style/layout, decode и paint.
  5. Выберите одного владельца задержки. Если доказательств несколько, начните с участка, который блокирует полезный экран.
  6. Сформулируйте одно обратимое изменение. Для preload, code split, lazy loading и нового формата заранее назовите возможный отрицательный эффект.
  7. Повторите тот же сценарий после изменения и сравните только заранее выбранный критерий.
  8. Оставьте краткий отчёт: условия, симптом, причина как гипотеза, проверка, действие и результат.
\n

Отрицательный путь и ограничения

\n

Иногда ответ HTML быстрый, критические ресурсы стартуют рано, а полезный экран всё равно не появляется. Тогда не нужно повторно сжимать сеть. Проверьте, не скрывает ли приложение разметку до завершения инициализации, не падает ли обработчик, не ждёт ли UI данные, которые не нужны для каркаса, и не занял ли главный поток сторонний код. Быстрый сервер не отменяет ошибки рендера.

\n

Нельзя объявлять универсальный бюджет по одному локальному запуску. Нельзя складывать параллельные полосы trace как последовательные этапы. Нельзя считать load эквивалентом видимости, transferSize — стоимостью JavaScript, а responseEnd — моментом paint. Данные другого origin могут быть неполными. Современные метрики зависят от браузера, viewport и фактического элемента. Эти ограничения не отменяют диагностику; они задают честные границы вывода.

\n

Проверяемый критерий готовности

\n

Изменение готово к следующему этапу, если на том же URL и при тех же зафиксированных условиях повторный профиль показывает более раннее появление выбранного полезного экрана, а участок названного владельца уменьшился или исчез. При этом не появился новый блокирующий участок: например, scripting не просто сменился на поздний запрос, а hero не стал видимым ценой пустого каркаса. Если критерий не выполнен, верните изменение или сформулируйте новую гипотезу. «Стало быстрее» без привязанного участка и условия ничего не доказывает.

\n

Так первая загрузка превращается из жалобы в короткий эксперимент. Вы не угадываете виновника по размеру файла. Вы фиксируете симптом, находите границу механизма, меняете один критический путь и проверяете тот же экран повторно.

\n

Проверяемые источники

" + "contentHtml": "

В учебном сценарии на дежурстве инженер открывает страницу товара: серверный ответ приходит быстро, но пользователь видит белый экран или неподвижную карточку. В Network нет огромного файла, а кнопка появляется после долгой паузы. Сначала команда уменьшает бандл, добавляет кэш или меняет формат картинок. Следующий запуск выглядит почти так же. Ошибка в диагнозе: слово «медленно» скрывает несколько разных задержек. Цена ошибки — релиз без доказанного эффекта, лишняя сложность в критическом пути и потерянное время пользователя до первого полезного действия.

\n

Тезис простой: первую загрузку нужно измерять по владельцам работы, а не по одному событию load и не по размеру gzip-файла. В одном воспроизводимом сценарии отдельно проверяют документ и сеть, обнаружение ресурсов, CSS, JavaScript на главном потоке, декодирование изображения и paint. Эти работы могут идти параллельно. Их нельзя бездумно сложить в одну сумму. Профиль нужен не для красивого числа, а для следующего проверяемого вопроса.

\n

Сначала определяем полезный экран

\n

«Страница загрузилась» — слабое условие. Для диагностики нужно назвать результат глазами пользователя: например, «на экране видны заголовок товара, цена и кнопка заказа». Такой результат имеет границу. Он может наступить до load, если второстепенные изображения ещё загружаются, и после DOMContentLoaded, если приложение только начинает строить DOM.

\n

Navigation Timing описывает путь текущего документа: ответ, построение DOM, обработчики DOMContentLoaded и load. Resource Timing описывает отдельные ресурсы. Ни один из этих интерфейсов сам по себе не говорит, что конкретный блок уже виден и готов к действию. Для этого нужны screenshot в trace, DOM и участок main thread рядом с моментом появления блока.

\n

Современный термин LCP удобен как словарь для крупного элемента в viewport, но он не заменяет старый trace и не даёт права придумывать значение метрики. Если замера нет, нужно писать «проверяем момент появления hero» и сохранять условия запуска. Лабораторный профиль отвечает на вопрос о выбранном сценарии. Он не описывает распределение опыта всех пользователей.

\n

Разводим задержку по механизмам

\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

Таблица задаёт порядок расследования. Она не обещает, что одна причина окажется единственной. Важна связь между наблюдением и проверкой. Если действие не меняет именно участок задержки, оно не подтверждает гипотезу.

\n

Фиксируем условия до Reload

\n

Запишите URL, действие пользователя, viewport, состояние кэша, Service Worker, ограничение CPU и сети, версию сборки и определение полезного экрана. Не смешивайте в одном сравнении холодный и тёплый кэш. Не меняйте одновременно URL, сборку и throttling. Иначе новый профиль нельзя будет честно сравнить со старым.

\n

Одного локального запуска достаточно для поиска участка работы, но недостаточно для утверждения о production. Расширение viewport, реальные устройства, фоновые вкладки и сеть меняют результат. После локального эксперимента нужен отдельный полевой источник, если команда принимает решение о приоритете по пользовательскому эффекту. Не выдавайте один trace за статистику.

\n
performance.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. Переиспользованное соединение и политика доступа также объясняют нулевые отдельные значения. Ноль не доказывает отсутствие сети.

\n

В общий лог нельзя бездумно отправлять полный URL: query-параметры могут содержать идентификаторы и пользовательские данные. Для отчёта оставьте путь, тип инициатора, округлённые значения, хэш сборки и условия. Сохраните screenshot и ссылку на участок trace. Результат должен позволить другому инженеру проверить тот же вопрос, а не только поверить выводу.

\n

Сеть отвечает только за свой участок

\n

У сетевой задержки есть обнаружение, очередь, соединение, запрос и передача. Размер файла влияет на передачу, но не объясняет поздний старт. Если hero появляется только после boot-кода, сжатие картинки не устранит задержку обнаружения. Если документ ждёт redirect, правка JavaScript не сократит этот переход. Сначала найдите URL на waterfall и сравните его старт с моментом чтения HTML.

\n

Критический ресурс должен быть виден там, где браузер может его обнаружить. Для hero это может быть обычный img в начальной разметке. Для стилей — прямой link. preload применяйте только к ресурсу, который доказанно нужен первому экрану. Лишний preload создаёт конкурента для документа или CSS. loading=\"lazy\" у первого значимого изображения также нельзя ставить по привычке: атрибут намеренно откладывает запрос.

\n

JavaScript задерживает экран двумя способами

\n

Первый способ — позднее обнаружение или получение файла. Второй — работа после получения: parse, compile, execute, создание DOM и повторные проходы style/layout. В Network эти случаи могут выглядеть одинаково. В Performance trace они различаются. Длинный scripting сразу после app.js указывает на работу главного потока. Поздний старт app.js указывает на обнаружение, очередь или сеть.

\n

Учебный пример: приложение получает данные каталога, строит 500 строк и сразу измеряет каждый узел, меняя класс после каждого чтения. Такой код может быть компактным, но вызывает чередование чтения и записи и несколько layout-проходов. Без trace нельзя утверждать, что именно этот фрагмент виноват. Проверка — найти функцию в main thread, отложить строки за пределы первого экрана или сгруппировать операции, а затем повторить тот же сценарий.

\n
const 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. Не ставьте отметку после всех запросов, если хотите понять первый экран. Отмечайте узкий участок и проверяйте, что он действительно относится к выбранному действию.

\n

CSS и изображение заканчиваются после передачи байтов

\n

CSS участвует в визуальной готовности. Браузеру нужны правила, чтобы рассчитать размеры и положение элементов. Внешняя таблица может завершиться поздно, а большой DOM — растянуть style и layout уже после её прихода. Поэтому фраза «HTML есть» не означает «экран готов». Ищите в trace окончание CSS, style/layout и первый screenshot.

\n

У изображения есть размер ответа, декодирование, место в layout и paint. Большой desktop-файл на маленьком viewport создаёт лишнюю работу даже при быстрой сети. Но менять формат или добавлять preload стоит после связи URL с конкретным визуальным элементом. Картинка, которая не нужна до первого действия, не должна конкурировать с CSS и скриптом только потому, что она заметна в исходнике.

\n
\"Четыре
Профиль показывает самостоятельных владельцев времени. Дорожки пересекаются, поэтому их длительности не образуют готовую сумму.
\n

Иллюстрация — схема механизма, а не запись реального сайта. Она помогает не потерять decode и paint после responseEnd. Для реального вывода нужен trace с тем же URL и условиями.

\n

Возвращаемся к странице товара

\n

На странице из начала разбора не выбирайте исправление по названию участка. Если trace покажет поздний старт URL, меняйте обнаружение ресурса; если URL стартовал рано, но scripting блокирует экран, меняйте порядок работы; если байты уже пришли, проверяйте decode, style, layout и paint. После одного изменения ожидаемый результат — более ранний полезный экран без нового блокирующего участка. Это критерий повторного профиля, а не утверждение о заранее известном виновнике.

\n

Порядок действий

\n
  1. Назовите полезный экран и действие, которое должно стать доступным.
  2. Зафиксируйте URL, сборку, viewport, кэш, Service Worker и ограничения CPU и сети.
  3. Снимите trace с Network, main thread и screenshot. Не меняйте код до первого профиля.
  4. Отметьте момент ответа HTML, старт критических ресурсов, длинные участки scripting, style/layout, decode и paint.
  5. Выберите одного владельца задержки. Если доказательств несколько, начните с участка, который блокирует полезный экран.
  6. Сформулируйте одно обратимое изменение. Для preload, code split, lazy loading и нового формата заранее назовите возможный отрицательный эффект.
  7. Повторите тот же сценарий после изменения и сравните только заранее выбранный критерий.
  8. Оставьте краткий отчёт: условия, симптом, причина как гипотеза, проверка, действие и результат.
\n

Отрицательный путь и ограничения

\n

Иногда ответ HTML быстрый, критические ресурсы стартуют рано, а полезный экран всё равно не появляется. Тогда не нужно повторно сжимать сеть. Проверьте, не скрывает ли приложение разметку до завершения инициализации, не падает ли обработчик, не ждёт ли UI данные, которые не нужны для каркаса, и не занял ли главный поток сторонний код. Быстрый сервер не отменяет ошибки рендера.

\n

Нельзя объявлять универсальный бюджет по одному локальному запуску. Нельзя складывать параллельные полосы trace как последовательные этапы. Нельзя считать load эквивалентом видимости, transferSize — стоимостью JavaScript, а responseEnd — моментом paint. Данные другого origin могут быть неполными. Современные метрики зависят от браузера, viewport и фактического элемента. Эти ограничения не отменяют диагностику; они задают честные границы вывода.

\n

Проверяемый критерий готовности

\n

Изменение готово к следующему этапу, если на том же URL и при тех же зафиксированных условиях повторный профиль показывает более раннее появление выбранного полезного экрана, а участок названного владельца уменьшился или исчез. При этом не появился новый блокирующий участок: например, scripting не просто сменился на поздний запрос, а hero не стал видимым ценой пустого каркаса. Если критерий не выполнен, верните изменение или сформулируйте новую гипотезу. «Стало быстрее» без привязанного участка и условия ничего не доказывает.

\n

Так первая загрузка превращается из жалобы в короткий эксперимент. Вы не угадываете виновника по размеру файла. Вы фиксируете симптом, находите границу механизма, меняете один критический путь и проверяете тот же экран повторно.

\n

Проверяемые источники

" }