Files
2026-09-03 23:23:44 +03:00

8 lines
16 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 302,
"slug": "editorial-2019-08-mechanism-frontend-performance",
"title": "Почему быстрый HTML не гарантирует быстрый экран",
"excerpt": "Первый байт, запрос ресурса и видимый экран — разные границы. Разбираем критический путь первой загрузки, находим владельца задержки и проверяем исправление без выдуманных production-результатов.",
"contentHtml": "<p>Сервер отвечает быстро: время до первого байта выглядит небольшим, HTML весит немного, а в Network нет огромных файлов. Но пользователь видит пустой фон, skeleton-экран (каркас) или неактивную кнопку. Иногда hero-картинка уже завершила загрузку, а на экране её всё ещё нет. Цена ошибки — недели случайных оптимизаций: сжимают изображения, меняют кеш и режут bundle, хотя задержка находится в другой границе.</p>\n<p>Тезис простой: быстрый ответ сервера не равен быстрому полезному экрану. Браузер проходит цепочку зависимостей. Он получает и разбирает HTML, обнаруживает CSS и скрипты, строит DOM и выполняет код. Для элемента, который зависит от изображения, к этой цепочке добавляются загрузка, декодирование, layout и paint. Запросы могут идти параллельно. Зависимости между ними — нет. Критический путь — это не рейтинг самых больших файлов, а путь ресурсов и работ, без которых выбранный экран не может стать полезным.</p>\n<h2>Механизм задержки</h2>\n<p>HTML приходит потоком. Парсер может обнаружить <code>&lt;link&gt;</code>, <code>&lt;script&gt;</code> и <code>&lt;img&gt;</code> до конца документа. Поэтому место URL влияет на время старта запроса. Если адрес hero появляется только после выполнения приложения, браузер не мог начать загрузку раньше. Если внешний CSS скрыт за динамическим импортом, разметка есть, но правила для расчёта и отображения приходят поздно.</p>\n<p>У JavaScript есть две разные задержки. Первая — ожидание обнаружения, передачи и декодирования файла. Вторая — работа главного потока: parse, compile, execute, создание DOM и последующие style/layout. Маленький по gzip скрипт может надолго занять main thread. Большой файл может не удерживать первый экран, если его загрузили после первичного рендера. Размер помогает найти гипотезу, но не доказывает причину.</p>\n<p>CSS тоже не является украшением после HTML. Он задаёт размеры, видимость и раскладку элементов. Класс <code>is-loading</code>, который снимается только после bootstrap, превращает готовую разметку в пустой экран. Изображение имеет ещё несколько границ после <code>responseEnd</code>: decode, доступный main thread, layout и paint. Поэтому завершение запроса не означает момент видимости.</p>\n<h2>Минимальный пример</h2>\n<p>Ниже учебный фрагмент. Он показывает, как сделать критические зависимости наблюдаемыми в начальном документе. Он не обещает одинакового результата на разных устройствах и не заменяет профиль реальной страницы.</p>\n<pre><code>&lt;link rel=\"stylesheet\" href=\"/assets/app.css\"&gt;\n\n&lt;main class=\"product-page\"&gt;\n &lt;h1&gt;Название товара&lt;/h1&gt;\n &lt;p class=\"price\"&gt;1 990 ₽&lt;/p&gt;\n &lt;img\n src=\"/assets/hero-960.webp\"\n width=\"960\"\n height=\"640\"\n alt=\"Товар на нейтральном фоне\"\n &gt;\n &lt;button type=\"button\"&gt;Купить&lt;/button&gt;\n&lt;/main&gt;\n\n&lt;script type=\"module\" src=\"/assets/app.js\"&gt;&lt;/script&gt;</code></pre>\n<p>Здесь браузер видит stylesheet и изображение без запуска приложения. Размеры картинки заранее известны, поэтому layout не обязан ждать её пикселей, чтобы зарезервировать место. Это не повод добавлять <code>preload</code> ко всем изображениям. Предзагрузка оправдана только для доказанно критического ресурса. Иначе она конкурирует с HTML, CSS и скриптом. Hero нельзя механически помечать <code>loading=\"lazy\"</code>: lazy loading намеренно откладывает запрос.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<table><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>HTML готов, hero стартует после app.js</td><td>URL изображения создаёт JavaScript</td><td>Сравнить startTime hero и момент обнаружения app.js в waterfall</td><td>Вывести критический URL в HTML или доказать, что hero не относится к первому экрану</td></tr><tr><td>Ресурс пришёл, экран пуст до конца bootstrap</td><td>Класс загрузки или каркас снимается кодом</td><td>Открыть DOM и trace main thread, найти условие скрытия</td><td>Отрисовать безопасный каркас раньше, второстепенную инициализацию отложить</td></tr><tr><td>app.js пришёл быстро, но paint поздний</td><td>Длинный scripting, лишний DOM или повторный layout</td><td>Найти длинную задачу и функцию-владельца в Performance trace</td><td>Убрать работу с критического пути, разделить чтение и запись DOM, повторить профиль</td></tr><tr><td>CSS завершён после разметки</td><td>Позднее обнаружение, import-цепочка или конкуренция запросов</td><td>Сопоставить startTime и responseEnd stylesheet с первым полезным кадром</td><td>Сократить цепочку и конкуренцию; не переносить весь CSS inline без проверки</td></tr><tr><td>responseEnd изображения ранний, изображение видно поздно</td><td>Decode, занятый main thread, размер контейнера или paint</td><td>Сверить снимок экрана, trace, размеры элемента и текущий источник изображения</td><td>Уменьшить подходящий вариант изображения или освободить поток после доказательства причины</td></tr><tr><td>LCP улучшился в лаборатории, интерфейс не стал полезнее</td><td>Выбранный элемент не отражает задачу пользователя</td><td>Назвать полезную интеракцию и проверить её отдельно от одной метрики</td><td>Оставить LCP диагностикой, а готовность определить через видимый контент и действие</td></tr></tbody></table>\n<h2>Как читать измерения</h2>\n<p><code>PerformanceNavigationTiming</code> описывает навигацию документа. <code>PerformanceResourceTiming</code> показывает отдельные ресурсы. Эти API отвечают на вопрос «когда произошло событие», но не строят полный граф причин. Для cross-origin-ресурса часть полей может быть недоступна без заголовка <code>Timing-Allow-Origin</code>; нулевое значение в таком случае не равно быстрой загрузке. Высокий <code>responseEnd</code> изображения не доказывает, что оно удерживало экран. Длинная запись ресурса не доказывает дорогой JavaScript. Запись нужно сопоставлять с DOM, waterfall и дорожкой main thread.</p>\n<pre><code>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 }) =&gt; ({ duration: Math.round(duration) })),\n);</code></pre>\n<p>Это учебный пример User Timing. Он измеряет только участок, который обрамляют две метки. Он не измеряет сеть, не заменяет trace и не превращает одну локальную запись в production-вывод. На реальном проекте имя операции должно соответствовать фактическому участку, а сбор таких меток должен учитывать приватность и объём данных.</p>\n<figure><img src=\"/assets/editorial/2019/frontend-critical-path-2019.svg\" alt=\"Критическая цепочка первой загрузки: HTML открывает CSS, JavaScript и hero-изображение, затем браузер выполняет код и рисует полезный экран\"><figcaption>Схема показывает зависимые границы критического пути. Параллельная загрузка сокращает ожидание, но не отменяет зависимость между обнаружением, готовностью стилей, свободным main thread и paint.</figcaption></figure>\n<h2>Порядок расследования</h2>\n<ol><li>Назвать полезный первый экран. Записать конкретный результат: например, заголовок, цену и доступную кнопку, а не просто исчезнувший spinner.</li><li>Выбрать один удерживаемый элемент или интеракцию и пройти от него назад к HTML, CSS, JavaScript, изображению, шрифту и данным.</li><li>Проверить исходный документ. Установить, когда браузер впервые увидел каждый критический URL и не создаётся ли он только приложением.</li><li>Открыть waterfall и отметить startTime, responseEnd и конкуренцию. Отдельно отметить CSS, скрипт и ресурс элемента.</li><li>Открыть Performance trace. Найти scripting, style, layout и paint между готовностью ресурса и появлением полезного кадра.</li><li>Сделать одно обратимое изменение на самой ранней разорванной границе. Затем повторить тот же URL, viewport, сеть и состояние кеша.</li><li>Если результат не изменился, вернуть гипотезу в список и проверить следующую границу. Не оставлять оптимизацию только потому, что она выглядит правдоподобно.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Одинаковый код даёт разные трассы на разных браузерах, устройствах, сетях и состояниях кеша. Лабораторный запуск показывает механизм, но не описывает полевую аудиторию. Один тёплый запуск не задаёт бюджет релиза. LCP полезен для видимости крупного элемента, но не сообщает, стала ли готова нужная пользователю операция. Navigation Timing и Resource Timing не содержат полного объяснения работы рендера.</p>\n<p>Иногда критический ресурс действительно нельзя вынести в HTML: его адрес зависит от ответа сервера, прав или варианта эксперимента. Тогда не надо подменять ограничение фиктивным preload. Зафиксируйте зависимость, измерьте её отдельно и определите допустимый fallback. Если изображение не является частью первого экрана, поздний запрос не является дефектом. Если после изменения waterfall не сдвинулся и main thread остался тем же, фикс не доказан.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Расследование завершено, когда названы полезный экран и граница-владелец задержки, приложен профиль с теми же условиями, а изменение сдвинуло именно эту границу. Повторный запуск должен показать тот же или лучший момент появления выбранного результата без регрессии доступности и функциональности. Для production-решения отдельно нужны полевые данные и согласованный бюджет. Без такого доказательства статья о «быстром HTML» остаётся догадкой.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://developer.chrome.com/docs/lighthouse/performance/lighthouse-largest-contentful-paint\" target=\"_blank\" rel=\"noopener noreferrer\">Chrome for Developers: Largest Contentful Paint</a></li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Performance_API/Resource_timing\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: Resource timing</a></li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/Performance/measure\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: Performance.measure</a></li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Attributes/rel/preload\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: rel=\"preload\"</a></li><li><a href=\"https://www.w3.org/TR/navigation-timing-2/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Navigation Timing Level 2</a></li></ul>"
}