{ "index": 302, "slug": "editorial-2019-08-mechanism-frontend-performance", "title": "Почему быстрый HTML не гарантирует быстрый экран", "excerpt": "Первый байт, запрос ресурса и видимый экран — разные границы. Разбираем критический путь первой загрузки, находим владельца задержки и проверяем исправление без выдуманных production-результатов.", "contentHtml": "

Сервер отвечает быстро: время до первого байта выглядит небольшим, HTML весит немного, а в Network нет огромных файлов. Но пользователь видит пустой фон, skeleton или неактивную кнопку. Иногда hero-картинка уже завершила загрузку, а на экране её всё ещё нет. Цена ошибки — недели случайных оптимизаций: сжимают изображения, меняют кеш и режут bundle, хотя задержка находится в другой границе.

\n

Тезис простой: быстрый ответ origin не равен быстрому полезному экрану. Браузер проходит цепочку зависимостей. Он получает и разбирает HTML, обнаруживает CSS и скрипты, строит DOM, выполняет код, считает layout, декодирует изображение и только потом рисует результат. Запросы могут идти параллельно. Зависимости между ними — нет. Критический путь — это не рейтинг самых больших файлов, а путь ресурсов и работ, без которых выбранный экран не может стать полезным.

\n

Механизм задержки

\n

HTML приходит потоком. Парсер может обнаружить <link>, <script> и <img> до конца документа. Поэтому место URL влияет на время старта запроса. Если адрес hero появляется только после выполнения приложения, браузер не мог начать загрузку раньше. Если внешний CSS скрыт за динамическим импортом, разметка есть, но правила для расчёта и отображения приходят поздно.

\n

У JavaScript есть две разные задержки. Первая — ожидание обнаружения, передачи и декодирования файла. Вторая — работа главного потока: parse, compile, execute, создание DOM и последующие style/layout. Маленький по gzip скрипт может надолго занять main thread. Большой файл может не удерживать первый экран, если его загрузили после первичного рендера. Размер помогает найти гипотезу, но не доказывает причину.

\n

CSS тоже не является украшением после HTML. Он задаёт размеры, видимость и раскладку элементов. Класс is-loading, который снимается только после bootstrap, превращает готовую разметку в пустой экран. Изображение имеет ещё несколько границ после responseEnd: decode, доступный main thread, layout и paint. Поэтому завершение запроса не означает момент видимости.

\n

Минимальный пример

\n

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

\n
<link rel=\"stylesheet\" href=\"/assets/app.css\">\n\n<main class=\"product-page\">\n  <h1>Название товара</h1>\n  <p class=\"price\">1 990 ₽</p>\n  <img\n    src=\"/assets/hero-960.webp\"\n    width=\"960\"\n    height=\"640\"\n    alt=\"Товар на нейтральном фоне\"\n  >\n  <button type=\"button\">Купить</button>\n</main>\n\n<script type=\"module\" src=\"/assets/app.js\"></script>
\n

Здесь браузер видит stylesheet и изображение без запуска приложения. Размеры картинки заранее известны, поэтому layout не обязан ждать её пикселей, чтобы зарезервировать место. Это не повод добавлять preload ко всем изображениям. Предзагрузка оправдана только для доказанно критического ресурса. Иначе она конкурирует с HTML, CSS и скриптом. Hero нельзя механически помечать loading=\"lazy\": lazy loading намеренно откладывает запрос.

\n

Симптом → причина → проверка → действие

\n
СимптомПричинаПроверкаДействие
HTML готов, hero стартует после app.jsURL изображения создаёт JavaScriptСравнить startTime hero и момент обнаружения app.js в waterfallВывести критический URL в HTML или доказать, что hero не относится к первому экрану
Ресурс пришёл, экран пуст до конца bootstrapКласс загрузки или каркас снимается кодомОткрыть DOM и trace main thread, найти условие скрытияОтрисовать безопасный каркас раньше, второстепенную инициализацию отложить
app.js пришёл быстро, но paint позднийДлинный scripting, лишний DOM или повторный layoutНайти длинную задачу и функцию-владельца в Performance traceУбрать работу с критического пути, разделить чтение и запись DOM, повторить профиль
CSS завершён после разметкиПозднее обнаружение, import-цепочка или конкуренция запросовСопоставить startTime и responseEnd stylesheet с первым полезным кадромСократить цепочку и конкуренцию; не переносить весь CSS inline без проверки
responseEnd картинки ранний, изображение видно поздноDecode, занятый main thread, размер контейнера или paintСверить screenshot, trace, размеры элемента и текущий источник картинкиУменьшить подходящий вариант изображения или освободить поток после доказательства причины
LCP улучшился в лаборатории, интерфейс не стал полезнееВыбранный элемент не отражает задачу пользователяНазвать полезную интеракцию и проверить её отдельно от одной метрикиОставить LCP диагностикой, а готовность определить через видимый контент и действие
\n

Как читать измерения

\n

PerformanceNavigationTiming описывает навигацию документа. PerformanceResourceTiming показывает отдельные ресурсы. Эти API отвечают на вопрос «когда произошло событие», но не строят полный граф причин. Высокий responseEnd изображения не доказывает, что оно удерживало экран. Длинная запись ресурса не доказывает дорогой JavaScript. Запись нужно сопоставлять с DOM, waterfall и дорожкой main thread.

\n
performance.mark('catalog: render-start');\nrenderCatalog(shellData);\nperformance.mark('catalog: render-end');\nperformance.measure(\n  'catalog: initial-render',\n  'catalog: render-start',\n  'catalog: render-end',\n);\n\nconsole.table(\n  performance.getEntriesByName('catalog: initial-render')\n    .map(({ duration }) => ({ duration: Math.round(duration) })),\n);
\n

Это учебный пример User Timing. Он измеряет только участок, который обрамляют две метки. Он не измеряет сеть, не заменяет trace и не превращает одну локальную запись в production-вывод. На реальном проекте имя операции должно соответствовать фактическому участку, а сбор таких меток должен учитывать приватность и объём данных.

\n
\"Критическая
Существующий asset показывает зависимые границы критического пути. Параллельная загрузка сокращает ожидание, но не отменяет зависимость между обнаружением, готовностью стилей, свободным main thread и paint.
\n

Порядок расследования

\n
  1. Назвать полезный первый экран. Записать конкретный результат: например, заголовок, цену и доступную кнопку, а не просто исчезнувший spinner.
  2. Выбрать один удерживаемый элемент или интеракцию и пройти от него назад к HTML, CSS, JavaScript, изображению, шрифту и данным.
  3. Проверить исходный документ. Установить, когда браузер впервые увидел каждый критический URL и не создаётся ли он только приложением.
  4. Открыть waterfall и отметить startTime, responseEnd и конкуренцию. Отдельно отметить CSS, скрипт и ресурс элемента.
  5. Открыть Performance trace. Найти scripting, style, layout и paint между готовностью ресурса и появлением полезного кадра.
  6. Сделать одно обратимое изменение на самой ранней разорванной границе. Затем повторить тот же URL, viewport, сеть и состояние кеша.
  7. Если результат не изменился, вернуть гипотезу в список и проверить следующую границу. Не оставлять оптимизацию только потому, что она выглядит правдоподобно.
\n

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

\n

Одинаковый код даёт разные трассы на разных браузерах, устройствах, сетях и состояниях кеша. Лабораторный запуск показывает механизм, но не описывает полевую аудиторию. Один тёплый запуск не задаёт бюджет релиза. LCP полезен для видимости крупного элемента, но не сообщает, стала ли готова нужная пользователю операция. Navigation Timing и Resource Timing не содержат полного объяснения работы рендера.

\n

Иногда критический ресурс действительно нельзя вынести в HTML: его адрес зависит от ответа сервера, прав или варианта эксперимента. Тогда не надо подменять ограничение фиктивным preload. Зафиксируйте зависимость, измерьте её отдельно и определите допустимый fallback. Если изображение не является частью первого экрана, поздний запрос не является дефектом. Если после изменения waterfall не сдвинулся и main thread остался тем же, фикс не доказан.

\n

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

\n

Расследование завершено, когда названы полезный экран и граница-владелец задержки, приложен профиль с теми же условиями, а изменение сдвинуло именно эту границу. Повторный запуск должен показать тот же или лучший момент появления выбранного результата без регрессии доступности и функциональности. Для production-решения отдельно нужны полевые данные и согласованный бюджет. Без такого доказательства статья о «быстром HTML» остаётся догадкой.

\n

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

\n" }