diff --git a/editorial/agent-rewrites/301.json b/editorial/agent-rewrites/301.json index b4bceaa..cd4912d 100644 --- a/editorial/agent-rewrites/301.json +++ b/editorial/agent-rewrites/301.json @@ -3,5 +3,5 @@ "slug": "editorial-2019-08-field-frontend-performance", "title": "Пустой первый экран при быстром HTML: как найти задержку в цепочке загрузки", "excerpt": "Сервер отвечает быстро, но пользователь видит пустой каркас. Разбираем, как отделить позднее обнаружение ресурса, работу JavaScript, CSS и decode изображения и проверить исправление без выдуманных production-цифр.", - "contentHtml": "
Сервер отвечает за 180 мс, HTML уже пришёл, но пользователь всё ещё видит фон и пустой каркас. Карточка товара появляется позже: вместе с названием, ценой и изображением. Команда сжимает hero, уменьшает бандл или ставит preload, но экран почти не меняется. Цена ошибки — не только потерянные миллисекунды. Каждый случай запускает новую случайную правку, усложняет загрузочный путь и оставляет следующему разработчику неверную причину.
\nТезис простой: быстрый ответ HTML не равен быстрому полезному экрану. Нужно найти первую зависимость, которая удерживает именно этот экран. Ресурс может поздно обнаружиться, ждать CSS, ждать свободный main thread, пройти decode или быть скрыт классом загрузки. Эти причины похожи в браузере, но требуют разных проверок и действий.
\nНазовите результат до открытия DevTools. В учебном примере полезный экран содержит название товара, цену и hero рядом с кнопкой заказа. Spinner и пустой skeleton не считаются результатом: они показывают состояние приложения, но не дают пользователю нужной информации. Зафиксируйте DOM-признаки: видимый [data-product-card], непустой [data-product-title], цену и готовое изображение.
Зафиксируйте один URL, viewport, вариант сборки и режим кэша. Повторите Reload и сохраните Network, screenshot и запись Performance. Лабораторные числа ниже — учебный пример. Они не описывают реальный сайт, устройство или production-выборку.
\nБраузер получает документ, строит DOM, находит стили и скрипты, выполняет JavaScript, рассчитывает геометрию, декодирует изображения и рисует кадр. Часть работы идёт параллельно. Но полезный элемент появляется только после своих зависимостей. Если URL hero появляется после запуска приложения, сжатие готового файла не исправит позднее обнаружение. Если запрос заканчивается рано, но main thread занят синхронным bootstrap, сеть не является первой причиной. Если изображение готово, но класс is-loading скрывает карточку, ищите условие рендера.
Разделите путь на владельцев: документ и сеть, CSS, JavaScript на main thread, DOM и layout, изображение от запроса до paint. Не суммируйте полосы автоматически. Они могут перекрываться. Ищите разрыв между фактом «ресурс готов» и фактом «полезный элемент виден».
\n| Полоса | Данные учебного профиля | Можно утверждать | Нельзя утверждать |
|---|---|---|---|
| Документ и сеть | 180 мс, 18 КБ | Ответ документа выделен отдельной границей | Что сервер любого сайта отвечает за 180 мс |
| JavaScript | 165 мс, 96 КБ | Есть отдельная работа parse и execute | Что любой такой bundle блокирует ровно 165 мс |
| CSS | 90 мс, 24 КБ | Стили имеют собственный этап готовности | Что stylesheet всегда блокирует весь интервал |
| Hero | 45 мс, 72 КБ | У изображения есть путь request, decode и paint | Что responseEnd равен моменту видимости |
Таблица задаёт язык, но не даёт диагноза. Фраза «hero поздний» должна означать конкретный факт: запрос стартовал после app.js, decode закончился после нужного кадра или изображение готово, но DOM его скрывает.
Проверка выполняется после Reload в локальном учебном профиле. Она помогает увидеть состояние, но не заменяет trace. Поздняя ручная проверка не доказывает, что состояние было таким во время первого paint.
\nconst hero = document.querySelector('[data-product-hero]');\nconst card = document.querySelector('[data-product-card]');\nconst css = document.querySelector('link[href*=app.css]');\nconsole.table({ heroSrc: hero?.currentSrc || '', heroComplete: hero?.complete || false, heroNaturalWidth: hero?.naturalWidth || 0, stylesheetReady: Boolean(css?.sheet), cardHidden: card?.classList.contains('is-loading') || false });\nperformance.mark('product-card-visible');\nimg.complete показывает состояние загрузки, но не доказывает, что пользователь увидел изображение. naturalWidth помогает отличить готовое изображение от элемента без декодированных данных. css.sheet не сообщает стоимость layout. User Timing ставит именованную границу, которую нужно сопоставить с trace.
Если heroSrc пуст, ищите источник URL: HTML, состояние приложения, CSS или API. Если URL появился после bootstrap, причина — позднее обнаружение. Если URL был в HTML и запрос начался рано, смотрите main thread и paint. Если изображение готово, а cardHidden равен true, проверяйте переход состояния и отрицательный путь.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Hero начинается после app.js | URL создаёт приложение | Сравнить initiator и момент появления URL | Передать критичный URL в HTML или изменить контракт данных; preload применять только после подтверждения |
| Hero готов, но экран ждёт длинный scripting | Bootstrap выполняет некритичную работу | Открыть Bottom-up и Call Tree до screenshot | Отложить второстепенный виджет, разбить синхронную работу, оставить fallback |
| CSS завершается поздно | Стиль найден поздно или конкурирует за сеть | Проверить link, @import и trace | Убрать import-цепочку после проверки визуального результата |
| Изображение готово, но виден skeleton | DOM или класс состояния скрывает карточку | Сопоставить атрибуты, переход состояния и screenshot | Разделить готовность данных и рекомендаций; не скрывать цену и заказ |
| Сеть быстрая, кадр поздний | Decode, layout или paint ждут main thread | Сопоставить responseEnd, decode, layout и кадр | Сократить лишние чтения и записи layout; проверить размер изображения |
Начинайте с screenshot и DOM, а не с самого большого файла. От полезного элемента идите назад: какой URL, стиль, состояние и код нужны, чтобы он стал видимым. Затем идите от документа вперёд: когда стартовали CSS, app.js и hero. Так вес ресурса не подменяет его место в критическом пути.
\nВ Chrome DevTools откройте Performance и запишите один Reload. В Network проверьте initiator hero и порядок запросов. В main thread найдите длинные задачи до screenshot. При наличии source map сопоставьте функцию с исходным модулем. При отсутствии source map не придумывайте имя компонента: запишите скомпилированный ресурс и отдельный вопрос на восстановление соответствия.
\nСделайте один обратимый эксперимент. Например, отключите второстепенный виджет локальным флагом и повторите тот же Reload. Если screenshot сдвинулся, измерьте исчезнувшую работу и проверьте карточку без виджета. Если экран не изменился, верните эксперимент и исключите гипотезу. Не удаляйте половину bootstrap и не называйте результат оптимизацией без контрольного сравнения.
\nПредположим, trace подтвердил: рекомендации синхронно строятся до карточки, хотя цена и заказ от них не зависят. Учебный вариант переносит рекомендации после показа основной карточки. Он не заявляет производственный результат и не задаёт универсальный API.
\nshowProductCard({ title, price, hero });\nperformance.mark('product-card-visible');\nloadRecommendationsLater().then(renderRecommendations).catch(() => { renderRecommendationsFallback(); });\nПосле изменения проверяют не только исчезновение scripting. Карточка должна показать цену, кнопку и изображение без рекомендаций. Ошибка второстепенного запроса не должна скрыть основной товар. Если перенос меняет порядок аналитики, focus, доступность или layout, это новый контракт. Его проверяют на медленной сети и при отказе запроса.
\nУчебные значения не являются измерением production. Один trace не описывает все устройства, сети и состояния кэша. Размер бандла не равен времени блокировки, responseEnd не равен paint, а пользовательская метрика не появляется от одного performance.mark. Если причина не доказана, честный результат — список исключённых гипотез и следующий точный вопрос.
Правка готова, когда один сценарий повторяется до и после изменения, полезный экран проходит DOM- и screenshot-критерий, подтверждена конкретная причина, а отрицательный путь не скрывает основную функцию. В отчёте есть условия, trace, изменённая граница и предел вывода. Формулировка «в лабораторном Reload карточка стала видимой раньше после переноса второстепенной работы» проверяема. Формулировка «всем пользователям стало быстрее» требует другой выборки.
\nСервер отвечает за 180 мс, HTML уже пришёл, но пользователь всё ещё видит фон и пустой каркас. Карточка товара появляется позже: вместе с названием, ценой и изображением. Команда сжимает hero, уменьшает бандл или ставит preload, но экран почти не меняется. Цена ошибки — не только потерянные миллисекунды. Каждый случай запускает новую случайную правку, усложняет загрузочный путь и оставляет следующему разработчику неверную причину.
\nТезис простой: быстрый ответ HTML не равен быстрому полезному экрану. Нужно найти первую зависимость, которая удерживает именно этот экран. Ресурс может поздно обнаружиться, ждать CSS, ждать свободный main thread, пройти decode или быть скрыт классом загрузки. Эти причины похожи в браузере, но требуют разных проверок и действий.
\nНазовите результат до открытия DevTools. В учебном примере полезный экран содержит название товара, цену и hero рядом с кнопкой заказа. Spinner и пустой skeleton не считаются результатом: они показывают состояние приложения, но не дают пользователю нужной информации. Зафиксируйте DOM-признаки: видимый [data-product-card], непустой [data-product-title], цену и готовое изображение.
Зафиксируйте один URL, viewport, вариант сборки и режим кэша. Повторите Reload и сохраните Network, screenshot и запись Performance. Лабораторные числа ниже — учебный пример. Они не описывают реальный сайт, устройство или production-выборку.
\nБраузер получает документ, строит DOM, находит стили и скрипты, выполняет JavaScript, рассчитывает геометрию, декодирует изображения и рисует кадр. Часть работы идёт параллельно. Но полезный элемент появляется только после своих зависимостей. Если URL hero появляется после запуска приложения, сжатие готового файла не исправит позднее обнаружение. Если запрос заканчивается рано, но main thread занят синхронным bootstrap, сеть не является первой причиной. Если изображение готово, но класс is-loading скрывает карточку, ищите условие рендера.
Разделите путь на владельцев: документ и сеть, CSS, JavaScript на main thread, DOM и layout, изображение от запроса до paint. Не суммируйте полосы автоматически. Они могут перекрываться. Ищите разрыв между фактом «ресурс готов» и фактом «полезный элемент виден».
\n| Полоса | Данные учебного профиля | Можно утверждать | Нельзя утверждать |
|---|---|---|---|
| Документ и сеть | 180 мс, 18 КБ | Ответ документа выделен отдельной границей | Что сервер любого сайта отвечает за 180 мс |
| JavaScript | 165 мс, 96 КБ | Есть отдельная работа parse и execute | Что любой такой bundle блокирует ровно 165 мс |
| CSS | 90 мс, 24 КБ | Стили имеют собственный этап готовности | Что stylesheet всегда блокирует весь интервал |
| Hero | 45 мс, 72 КБ | У изображения есть путь request, decode и paint | Что responseEnd равен моменту видимости |
Таблица задаёт язык, но не даёт диагноза. Фраза «hero поздний» должна означать конкретный факт: запрос стартовал после app.js, decode закончился после нужного кадра или изображение готово, но DOM его скрывает.
Проверка выполняется после Reload в локальном учебном профиле. Она помогает увидеть состояние, но не заменяет trace. Поздняя ручная проверка не доказывает, что состояние было таким во время первого paint.
\nconst hero = document.querySelector('[data-product-hero]');\nconst card = document.querySelector('[data-product-card]');\nconst title = document.querySelector('[data-product-title]');\nconst price = document.querySelector('[data-product-price]');\nconst css = document.querySelector('link[href*=app.css]');\nconst rect = card?.getBoundingClientRect();\nconst inViewport = rect && rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && rect.top < innerHeight && rect.left < innerWidth;\nconsole.table({ heroSrc: hero?.currentSrc || '', heroComplete: hero?.complete || false, heroNaturalWidth: hero?.naturalWidth || 0, stylesheetReady: Boolean(css?.sheet), cardHidden: card?.classList.contains('is-loading') || false, inViewport: Boolean(inViewport) });\nif (hero?.complete && hero.naturalWidth > 0 && title?.textContent?.trim() && price?.textContent?.trim() && !card?.classList.contains('is-loading') && inViewport) {\n performance.mark('product-card-visible');\n}\nimg.complete показывает, что загрузка завершилась, в том числе с ошибкой, поэтому его проверяют вместе с naturalWidth. Прямоугольник подтверждает геометрическую видимость в момент ручной проверки, но не заменяет screenshot и trace первого paint. css.sheet показывает наличие объекта таблицы стилей, но не её стоимость для layout. User Timing ставит именованную границу только после явно заданного условия; её нужно сопоставить с trace.
Если heroSrc пуст, ищите источник URL: HTML, состояние приложения, CSS или API. Если URL появился после bootstrap, причина — позднее обнаружение. Если URL был в HTML и запрос начался рано, смотрите main thread и paint. Если изображение готово, а cardHidden равен true, проверяйте переход состояния и отрицательный путь.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| Hero начинается после app.js | URL создаёт приложение | Сравнить initiator и момент появления URL | Передать критичный URL в HTML или изменить контракт данных; preload применять только после подтверждения |
| Hero готов, но экран ждёт длинный scripting | Bootstrap выполняет некритичную работу | Открыть Bottom-up и Call Tree до screenshot | Отложить второстепенный виджет, разбить синхронную работу, оставить fallback |
| CSS завершается поздно | Стиль найден поздно или конкурирует за сеть | Проверить link, @import и trace | Убрать import-цепочку после проверки визуального результата |
| Изображение готово, но виден skeleton | DOM или класс состояния скрывает карточку | Сопоставить атрибуты, переход состояния и screenshot | Разделить готовность данных и рекомендаций; не скрывать цену и заказ |
| Сеть быстрая, кадр поздний | Decode, layout или paint ждут main thread | Сопоставить responseEnd, decode, layout и кадр | Сократить лишние чтения и записи layout; проверить размер изображения |
Начинайте с screenshot и DOM, а не с самого большого файла. От полезного элемента идите назад: какой URL, стиль, состояние и код нужны, чтобы он стал видимым. Затем идите от документа вперёд: когда стартовали CSS, app.js и hero. Так вес ресурса не подменяет его место в критическом пути.
\nВ Chrome DevTools откройте Performance и запишите один Reload. В Network проверьте initiator hero и порядок запросов. В main thread найдите длинные задачи до screenshot. При наличии source map сопоставьте функцию с исходным модулем. При отсутствии source map не придумывайте имя компонента: запишите скомпилированный ресурс и отдельный вопрос на восстановление соответствия.
\nСделайте один обратимый эксперимент. Например, отключите второстепенный виджет локальным флагом и повторите тот же Reload. Если screenshot сдвинулся, измерьте исчезнувшую работу и проверьте карточку без виджета. Если экран не изменился, верните эксперимент и исключите гипотезу. Не удаляйте половину bootstrap и не называйте результат оптимизацией без контрольного сравнения.
\nПредположим, trace подтвердил: рекомендации синхронно строятся до карточки, хотя цена и заказ от них не зависят. Учебный вариант переносит рекомендации после показа основной карточки. Он не заявляет производственный результат и не задаёт универсальный API.
\nshowProductCard({ title, price, hero });\nrequestAnimationFrame(() => performance.mark('product-card-frame'));\nloadRecommendationsLater().then(renderRecommendations).catch(() => { renderRecommendationsFallback(); });\nПосле изменения проверяют не только исчезновение scripting. Карточка должна показать цену, кнопку и изображение без рекомендаций. Ошибка второстепенного запроса не должна скрыть основной товар. Если перенос меняет порядок аналитики, focus, доступность или layout, это новый контракт. Его проверяют на медленной сети и при отказе запроса.
\nУчебные значения не являются измерением production. Один trace не описывает все устройства, сети и состояния кэша. Размер бандла не равен времени блокировки, responseEnd не равен paint, а пользовательская метрика не появляется от одного performance.mark. Если причина не доказана, честный результат — список исключённых гипотез и следующий точный вопрос.
Правка готова, когда один сценарий повторяется до и после изменения, полезный экран проходит DOM- и screenshot-критерий, подтверждена конкретная причина, а отрицательный путь не скрывает основную функцию. В отчёте есть условия, trace, изменённая граница и предел вывода. Формулировка «в лабораторном Reload карточка стала видимой раньше после переноса второстепенной работы» проверяема. Формулировка «всем пользователям стало быстрее» требует другой выборки.
\n