diff --git a/editorial/agent-rewrites/302.json b/editorial/agent-rewrites/302.json index f327e6e..a4a50a2 100644 --- a/editorial/agent-rewrites/302.json +++ b/editorial/agent-rewrites/302.json @@ -3,5 +3,5 @@ "slug": "editorial-2019-08-mechanism-frontend-performance", "title": "Почему быстрый HTML не гарантирует быстрый экран", "excerpt": "Первый байт, запрос ресурса и видимый экран — разные границы. Разбираем критический путь первой загрузки, находим владельца задержки и проверяем исправление без выдуманных production-результатов.", - "contentHtml": "
Сервер отвечает быстро: время до первого байта выглядит небольшим, HTML весит немного, а в Network нет огромных файлов. Но пользователь видит пустой фон, skeleton или неактивную кнопку. Иногда hero-картинка уже завершила загрузку, а на экране её всё ещё нет. Цена ошибки — недели случайных оптимизаций: сжимают изображения, меняют кеш и режут bundle, хотя задержка находится в другой границе.
\nТезис простой: быстрый ответ origin не равен быстрому полезному экрану. Браузер проходит цепочку зависимостей. Он получает и разбирает HTML, обнаруживает CSS и скрипты, строит DOM, выполняет код, считает layout, декодирует изображение и только потом рисует результат. Запросы могут идти параллельно. Зависимости между ними — нет. Критический путь — это не рейтинг самых больших файлов, а путь ресурсов и работ, без которых выбранный экран не может стать полезным.
\nHTML приходит потоком. Парсер может обнаружить <link>, <script> и <img> до конца документа. Поэтому место URL влияет на время старта запроса. Если адрес hero появляется только после выполнения приложения, браузер не мог начать загрузку раньше. Если внешний CSS скрыт за динамическим импортом, разметка есть, но правила для расчёта и отображения приходят поздно.
У JavaScript есть две разные задержки. Первая — ожидание обнаружения, передачи и декодирования файла. Вторая — работа главного потока: parse, compile, execute, создание DOM и последующие style/layout. Маленький по gzip скрипт может надолго занять main thread. Большой файл может не удерживать первый экран, если его загрузили после первичного рендера. Размер помогает найти гипотезу, но не доказывает причину.
\nCSS тоже не является украшением после HTML. Он задаёт размеры, видимость и раскладку элементов. Класс is-loading, который снимается только после bootstrap, превращает готовую разметку в пустой экран. Изображение имеет ещё несколько границ после responseEnd: decode, доступный main thread, layout и paint. Поэтому завершение запроса не означает момент видимости.
Ниже учебный фрагмент. Он показывает, как сделать критические зависимости наблюдаемыми в начальном документе. Он не обещает одинакового результата на разных устройствах и не заменяет профиль реальной страницы.
\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 намеренно откладывает запрос.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| HTML готов, hero стартует после app.js | URL изображения создаёт 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 диагностикой, а готовность определить через видимый контент и действие |
PerformanceNavigationTiming описывает навигацию документа. PerformanceResourceTiming показывает отдельные ресурсы. Эти API отвечают на вопрос «когда произошло событие», но не строят полный граф причин. Высокий responseEnd изображения не доказывает, что оно удерживало экран. Длинная запись ресурса не доказывает дорогой JavaScript. Запись нужно сопоставлять с DOM, waterfall и дорожкой main thread.
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Одинаковый код даёт разные трассы на разных браузерах, устройствах, сетях и состояниях кеша. Лабораторный запуск показывает механизм, но не описывает полевую аудиторию. Один тёплый запуск не задаёт бюджет релиза. LCP полезен для видимости крупного элемента, но не сообщает, стала ли готова нужная пользователю операция. Navigation Timing и Resource Timing не содержат полного объяснения работы рендера.
\nИногда критический ресурс действительно нельзя вынести в HTML: его адрес зависит от ответа сервера, прав или варианта эксперимента. Тогда не надо подменять ограничение фиктивным preload. Зафиксируйте зависимость, измерьте её отдельно и определите допустимый fallback. Если изображение не является частью первого экрана, поздний запрос не является дефектом. Если после изменения waterfall не сдвинулся и main thread остался тем же, фикс не доказан.
\nРасследование завершено, когда названы полезный экран и граница-владелец задержки, приложен профиль с теми же условиями, а изменение сдвинуло именно эту границу. Повторный запуск должен показать тот же или лучший момент появления выбранного результата без регрессии доступности и функциональности. Для production-решения отдельно нужны полевые данные и согласованный бюджет. Без такого доказательства статья о «быстром HTML» остаётся догадкой.
\nСервер отвечает быстро: время до первого байта выглядит небольшим, HTML весит немного, а в Network нет огромных файлов. Но пользователь видит пустой фон, skeleton-экран (каркас) или неактивную кнопку. Иногда hero-картинка уже завершила загрузку, а на экране её всё ещё нет. Цена ошибки — недели случайных оптимизаций: сжимают изображения, меняют кеш и режут bundle, хотя задержка находится в другой границе.
\nТезис простой: быстрый ответ сервера не равен быстрому полезному экрану. Браузер проходит цепочку зависимостей. Он получает и разбирает HTML, обнаруживает CSS и скрипты, строит DOM и выполняет код. Для элемента, который зависит от изображения, к этой цепочке добавляются загрузка, декодирование, layout и paint. Запросы могут идти параллельно. Зависимости между ними — нет. Критический путь — это не рейтинг самых больших файлов, а путь ресурсов и работ, без которых выбранный экран не может стать полезным.
\nHTML приходит потоком. Парсер может обнаружить <link>, <script> и <img> до конца документа. Поэтому место URL влияет на время старта запроса. Если адрес hero появляется только после выполнения приложения, браузер не мог начать загрузку раньше. Если внешний CSS скрыт за динамическим импортом, разметка есть, но правила для расчёта и отображения приходят поздно.
У JavaScript есть две разные задержки. Первая — ожидание обнаружения, передачи и декодирования файла. Вторая — работа главного потока: parse, compile, execute, создание DOM и последующие style/layout. Маленький по gzip скрипт может надолго занять main thread. Большой файл может не удерживать первый экран, если его загрузили после первичного рендера. Размер помогает найти гипотезу, но не доказывает причину.
\nCSS тоже не является украшением после HTML. Он задаёт размеры, видимость и раскладку элементов. Класс is-loading, который снимается только после bootstrap, превращает готовую разметку в пустой экран. Изображение имеет ещё несколько границ после responseEnd: decode, доступный main thread, layout и paint. Поэтому завершение запроса не означает момент видимости.
Ниже учебный фрагмент. Он показывает, как сделать критические зависимости наблюдаемыми в начальном документе. Он не обещает одинакового результата на разных устройствах и не заменяет профиль реальной страницы.
\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 намеренно откладывает запрос.
| Симптом | Причина | Проверка | Действие |
|---|---|---|---|
| HTML готов, hero стартует после app.js | URL изображения создаёт 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 | Сверить снимок экрана, trace, размеры элемента и текущий источник изображения | Уменьшить подходящий вариант изображения или освободить поток после доказательства причины |
| LCP улучшился в лаборатории, интерфейс не стал полезнее | Выбранный элемент не отражает задачу пользователя | Назвать полезную интеракцию и проверить её отдельно от одной метрики | Оставить LCP диагностикой, а готовность определить через видимый контент и действие |
PerformanceNavigationTiming описывает навигацию документа. PerformanceResourceTiming показывает отдельные ресурсы. Эти API отвечают на вопрос «когда произошло событие», но не строят полный граф причин. Для cross-origin-ресурса часть полей может быть недоступна без заголовка Timing-Allow-Origin; нулевое значение в таком случае не равно быстрой загрузке. Высокий responseEnd изображения не доказывает, что оно удерживало экран. Длинная запись ресурса не доказывает дорогой JavaScript. Запись нужно сопоставлять с DOM, waterfall и дорожкой main thread.
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Одинаковый код даёт разные трассы на разных браузерах, устройствах, сетях и состояниях кеша. Лабораторный запуск показывает механизм, но не описывает полевую аудиторию. Один тёплый запуск не задаёт бюджет релиза. LCP полезен для видимости крупного элемента, но не сообщает, стала ли готова нужная пользователю операция. Navigation Timing и Resource Timing не содержат полного объяснения работы рендера.
\nИногда критический ресурс действительно нельзя вынести в HTML: его адрес зависит от ответа сервера, прав или варианта эксперимента. Тогда не надо подменять ограничение фиктивным preload. Зафиксируйте зависимость, измерьте её отдельно и определите допустимый fallback. Если изображение не является частью первого экрана, поздний запрос не является дефектом. Если после изменения waterfall не сдвинулся и main thread остался тем же, фикс не доказан.
\nРасследование завершено, когда названы полезный экран и граница-владелец задержки, приложен профиль с теми же условиями, а изменение сдвинуло именно эту границу. Повторный запуск должен показать тот же или лучший момент появления выбранного результата без регрессии доступности и функциональности. Для production-решения отдельно нужны полевые данные и согласованный бюджет. Без такого доказательства статья о «быстром HTML» остаётся догадкой.
\n