{ "index": 301, "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

Сначала определите полезный экран

\n

Назовите результат до открытия DevTools. В учебном примере полезный экран содержит название товара, цену и hero рядом с кнопкой заказа. Spinner и пустой skeleton не считаются результатом: они показывают состояние приложения, но не дают пользователю нужной информации. Зафиксируйте DOM-признаки: видимый [data-product-card], непустой [data-product-title], цену и готовое изображение.

\n

Зафиксируйте один URL, viewport, вариант сборки и режим кэша. Повторите Reload и сохраните Network, screenshot и запись Performance. Лабораторные числа ниже — учебный пример. Они не описывают реальный сайт, устройство или production-выборку.

\n

Механизм: экран ждёт зависимые границы

\n

Браузер получает документ, строит DOM, находит стили и скрипты, выполняет JavaScript, рассчитывает геометрию, декодирует изображения и рисует кадр. Часть работы идёт параллельно. Но полезный элемент появляется только после своих зависимостей. Если URL hero появляется после запуска приложения, сжатие готового файла не исправит позднее обнаружение. Если запрос заканчивается рано, но main thread занят синхронным bootstrap, сеть не является первой причиной. Если изображение готово, но класс is-loading скрывает карточку, ищите условие рендера.

\n

Разделите путь на владельцев: документ и сеть, CSS, JavaScript на main thread, DOM и layout, изображение от запроса до paint. Не суммируйте полосы автоматически. Они могут перекрываться. Ищите разрыв между фактом «ресурс готов» и фактом «полезный элемент виден».

\n
ПолосаДанные учебного профиляМожно утверждатьНельзя утверждать
Документ и сеть180 мс, 18 КБОтвет документа выделен отдельной границейЧто сервер любого сайта отвечает за 180 мс
JavaScript165 мс, 96 КБЕсть отдельная работа parse и executeЧто любой такой bundle блокирует ровно 165 мс
CSS90 мс, 24 КБСтили имеют собственный этап готовностиЧто stylesheet всегда блокирует весь интервал
Hero45 мс, 72 КБУ изображения есть путь request, decode и paintЧто responseEnd равен моменту видимости
\n

Таблица задаёт язык, но не даёт диагноза. Фраза «hero поздний» должна означать конкретный факт: запрос стартовал после app.js, decode закончился после нужного кадра или изображение готово, но DOM его скрывает.

\n

Наблюдаемый пример: отделяем ресурс от рендера

\n

Проверка выполняется после Reload в локальном учебном профиле. Она помогает увидеть состояние, но не заменяет trace. Поздняя ручная проверка не доказывает, что состояние было таким во время первого paint.

\n
const 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');
\n

img.complete показывает состояние загрузки, но не доказывает, что пользователь увидел изображение. naturalWidth помогает отличить готовое изображение от элемента без декодированных данных. css.sheet не сообщает стоимость layout. User Timing ставит именованную границу, которую нужно сопоставить с trace.

\n

Если heroSrc пуст, ищите источник URL: HTML, состояние приложения, CSS или API. Если URL появился после bootstrap, причина — позднее обнаружение. Если URL был в HTML и запрос начался рано, смотрите main thread и paint. Если изображение готово, а cardHidden равен true, проверяйте переход состояния и отрицательный путь.

\n

Симптомы и минимальные проверки

\n
СимптомПричинаПроверкаДействие
Hero начинается после app.jsURL создаёт приложениеСравнить initiator и момент появления URLПередать критичный URL в HTML или изменить контракт данных; preload применять только после подтверждения
Hero готов, но экран ждёт длинный scriptingBootstrap выполняет некритичную работуОткрыть Bottom-up и Call Tree до screenshotОтложить второстепенный виджет, разбить синхронную работу, оставить fallback
CSS завершается поздноСтиль найден поздно или конкурирует за сетьПроверить link, @import и traceУбрать import-цепочку после проверки визуального результата
Изображение готово, но виден skeletonDOM или класс состояния скрывает карточкуСопоставить атрибуты, переход состояния и screenshotРазделить готовность данных и рекомендаций; не скрывать цену и заказ
Сеть быстрая, кадр позднийDecode, layout или paint ждут main threadСопоставить responseEnd, decode, layout и кадрСократить лишние чтения и записи layout; проверить размер изображения
\n

Как читать запись без догадок

\n

Начинайте с 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
Диагностика первого экрана: сеть, JavaScript, CSS, decode и paint
Диагностика начинается с полезного screenshot. Каждая ветвь получает свою проверку. Иллюстрация показывает учебную модель и не является trace конкретного продукта.
\n

Учебный фикс с отрицательным путём

\n

Предположим, trace подтвердил: рекомендации синхронно строятся до карточки, хотя цена и заказ от них не зависят. Учебный вариант переносит рекомендации после показа основной карточки. Он не заявляет производственный результат и не задаёт универсальный API.

\n
showProductCard({ title, price, hero });\nperformance.mark('product-card-visible');\nloadRecommendationsLater().then(renderRecommendations).catch(() => { renderRecommendationsFallback(); });
\n

После изменения проверяют не только исчезновение scripting. Карточка должна показать цену, кнопку и изображение без рекомендаций. Ошибка второстепенного запроса не должна скрыть основной товар. Если перенос меняет порядок аналитики, focus, доступность или layout, это новый контракт. Его проверяют на медленной сети и при отказе запроса.

\n

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

\n
  1. Опишите полезный первый экран и проверяемые DOM-признаки.
  2. Зафиксируйте URL, viewport, сборку, кэш и повторяемый Reload.
  3. Сохраните Network, screenshot и Performance trace до изменения.
  4. Проверьте момент обнаружения CSS, JavaScript и hero, затем main thread, decode, layout и paint.
  5. Назовите один разрыв: позднее обнаружение, scripting, стиль, состояние DOM или изображение.
  6. Проведите один узкий обратимый эксперимент с ожидаемым сдвигом.
  7. Повторите сценарий и проверьте полезный экран, fallback, ошибку и визуальную стабильность.
  8. Запишите лабораторный вывод с условиями. Полевые числа добавляйте только после отдельного сбора данных.
\n

Ограничения и критерий готовности

\n

Учебные значения не являются измерением production. Один trace не описывает все устройства, сети и состояния кэша. Размер бандла не равен времени блокировки, responseEnd не равен paint, а пользовательская метрика не появляется от одного performance.mark. Если причина не доказана, честный результат — список исключённых гипотез и следующий точный вопрос.

\n

Правка готова, когда один сценарий повторяется до и после изменения, полезный экран проходит DOM- и screenshot-критерий, подтверждена конкретная причина, а отрицательный путь не скрывает основную функцию. В отчёте есть условия, trace, изменённая граница и предел вывода. Формулировка «в лабораторном Reload карточка стала видимой раньше после переноса второстепенной работы» проверяема. Формулировка «всем пользователям стало быстрее» требует другой выборки.

\n

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

\n" }