Проблема начинается с фразы «страница тяжёлая». В ней нет владельца задержки: сервер мог ответить быстро, но браузер ждёт таблицу стилей; картинка уже пришла, но её не дают нарисовать длинные скрипты; файл JavaScript маленький в gzip, зато его разбор занимает главный поток. Пока все эти случаи называют одним числом «load», команда сжимает не тот ресурс и получает тот же пустой первый экран. Цена ошибки — ещё один релиз без понятного результата и пользователи, которые уходят до полезного содержимого.
Для первой загрузки я не начинаю с набора оптимизаций. Сначала выбираю один сценарий и делаю профиль, в котором четыре владельца времени видны отдельно: документ и сеть, JavaScript на главном потоке, CSS до готового стиля, изображение до декодирования и отрисовки. Это не обещание, что четыре полосы складываются в одну честную сумму. Их работа может пересекаться. Цель профиля проще: назвать следующий проверяемый вопрос, а не угадать виновника по размеру бандла.
Что именно считаем медленной первой загрузкой
У экрана есть несколько разных моментов: браузер получил начало HTML, увидел структуру, получил стили, нарисовал первый полезный контент и смог обработать действие. Между ними нет одной универсальной границы. DOMContentLoaded говорит о завершении разбора документа и defer-скриптов, а не о том, что главный блок уже нарисован. Событие load может ждать второстепенные картинки, которые не помогают пользователю начать работу. Поэтому запись «load за две секунды» не отвечает, почему кнопка или заголовок появились поздно.
В августе 2019 для расследования достаточно открыть DevTools и увидеть водопад, main thread и скриншоты загрузки. При переиздании можно соотнести результат с более поздним словарём LCP: он описывает момент, когда крупное содержание в viewport отрисовано. Но не стоит подменять этим словарём старую проверку и тем более писать вымышленное значение метрики. Если конкретный trace не записан, в заметке остаётся гипотеза и маршрут её проверки, а не число с точностью до миллисекунды.
| Наблюдение | Возможный владелец | Чего оно не доказывает | Первый запрос к данным |
|---|---|---|---|
| HTML быстро пришёл, экран пустой | CSS, JavaScript или скрытый критический ресурс | Что origin медленный | Сверить responseStart документа с первым screenshot и полосой main thread |
| Водопад длинный | Один критический запрос, очередь приоритетов или несколько независимых ресурсов | Что самый большой файл всегда виноват | Найти ресурс, без которого не появляется полезный блок |
| app.js небольшой после сжатия | Parse, compile и execute JavaScript | Что код дёшев на слабом устройстве | Записать trace и посмотреть scripting на main thread |
| Hero уже скачан | Decode, style, layout или занятый main thread | Что изображение стало видимым | Связать URL ресурса со screenshot и событием paint |
Профиль не требует сразу добавлять RUM или менять CDN. В первом проходе достаточно одной локальной страницы, одного пути и одной версии сборки. Он полезен именно потому, что ограничен: позже другой инженер может повторить условия и увидеть, какая полоса изменилась. Если смешать мобильный эмулятор, тёплый кэш, авторизованную сессию и три разных URL, сравнение превратится в набор впечатлений.
Фиксируем условия до нажатия Reload
Сначала записываю URL, действие пользователя и что считается полезным экраном. Не «страница открылась», а, например, «заголовок товара, цена и кнопка заказа видимы без прокрутки». Затем фиксирую, очищается ли кэш, есть ли Service Worker, какой viewport и какое ограничение CPU или сети выбрано. Эти параметры не делают лабораторный запуск похожим на каждого реального пользователя, но делают его повторяемым для сравнения двух веток.
Нельзя делать вывод о production только по одной локальной записи. Лабораторный профиль отвечает на вопрос «какой путь браузер прошёл в этих условиях». Полевая телеметрия отвечает на другой вопрос: «какой распределённый опыт получили пользователи». Для исправления конкретного регресса сначала нужен владелец из профиля; для приоритета работы нужна отдельная выборка. Смешивать эти доказательства — значит выдать диагностический опыт за статистику.
function collectLoadingEntries() {
const navigation = performance.getEntriesByType("navigation")[0];
const resources = performance.getEntriesByType("resource").map((entry) => ({
name: new URL(entry.name).pathname,
type: entry.initiatorType,
duration: Math.round(entry.duration),
transferSize: entry.transferSize,
encodedBodySize: entry.encodedBodySize,
}));
return {
navigation: navigation && {
responseStart: Math.round(navigation.responseStart),
domInteractive: Math.round(navigation.domInteractive),
domContentLoadedEnd: Math.round(navigation.domContentLoadedEventEnd),
},
resources,
};
}
console.table(collectLoadingEntries().resources);
Этот код не измеряет CSSOM или время выполнения скрипта: он берёт только записи navigation и resource. Это намеренное ограничение. Из него можно увидеть тип ресурса, длительность и размеры там, где браузер имеет право раскрыть их. Для ресурсов с другого origin подробные поля могут быть нулевыми без Timing-Allow-Origin. Нулевое DNS или connect время также не доказывает, что сети не было: соединение могло быть переиспользовано или значение ограничено политикой доступа.
Сохраняйте результат с версией сборки и условиями, но не отправляйте в общий лог полный URL с пользовательскими параметрами. Для локальной диагностики обычно хватает пути, типа инициатора и округлённых времён. Если нужен отчёт для команды, приложите screenshot с моментом появления полезного блока и короткое пояснение: какая гипотеза проверялась, что действительно измерено и что пока неизвестно.
Разводим сеть, JavaScript, CSS и изображение
У документа и ресурса есть свой водопад: redirect, DNS, connect, запрос и ответ. Это сетевой слой, но даже его нельзя сократить до transferSize. Два одинаковых файла получают разную задержку из-за origin, очереди, приоритета, повторного соединения или кэша. Если критическая картинка обнаруживается только после выполнения скрипта, её поздний старт выглядит сетевой проблемой, хотя сначала надо проверить путь обнаружения в HTML и CSS.
JavaScript проверяю не размером файла, а временем на main thread после его прихода. В trace ищу длинные фрагменты scripting и связываю их с конкретным ресурсом или функцией через source map, если она доступна. CSS проверяю отдельно: таблица стилей может блокировать расчёт стиля и первый рендер, а большой DOM может удлинить style и layout. У изображения два шага: загрузка байтов и декодирование с отрисовкой. Сжатие файла полезно только если оно попало в доказанный критический участок.
Контролируемый fixture проверяет классификацию, а не скорость сайта
Чтобы не спорить о том, как отчёт группирует данные, в этом модуле есть детерминированный fixture. В нём четыре записи: response документа, работа JavaScript, построение CSSOM и decode hero-изображения. Скрипт складывает байты и длительности по владельцу и проверяет, что в результате действительно четыре независимые группы. Fixture не открывает браузер, не делает HTTP-запрос и не измеряет этот сайт. Его доказательство узкое: классификатор не потерял CSS внутри JavaScript и не выдал картинку за сеть.
// Из каталога web/:
// node scripts/upgrade-2019-08.mjs --run-fixture
{
"owners": {
"network": { "milliseconds": 180, "bytes": 18000 },
"javascript": { "milliseconds": 165, "bytes": 96000 },
"css": { "milliseconds": 90, "bytes": 24000 },
"image": { "milliseconds": 45, "bytes": 72000 }
},
"totalBytes": 210000
}
Числа fixture не являются бюджетом и не являются результатом trace. Они выбраны так, чтобы тест ловил ошибку группировки. Например, если код отнесёт decode картинки к JavaScript, у результата исчезнет владелец image, и fixture упадёт. Для реальной страницы после этого всё равно нужен отдельный Reload в DevTools: только он покажет, перекрывались ли операции, какой URL был критическим и где браузер действительно потратил время.
Собираем короткий отчёт, пригодный для следующего запуска
Полезный отчёт помещается в несколько строк. Первая строка — условия: путь, кэш, viewport, throttle, хэш сборки. Вторая — наблюдение: «HTML получил ответ до первого screenshot, а полезный блок появился после scripting». Третья — конкретный владелец и ссылка на дорожку: «main thread: модуль checkout.js, участок parse плюс execute». Четвёртая — одна гипотеза изменения и критерий проверки. Если отчёт не называет владельца, он не помогает выбрать работу.
Не добавляйте в него слово «ускорили», пока не повторили профиль при тех же условиях. Например, перенос второстепенного виджета за событие пользователя может уменьшить scripting до полезного блока, но одновременно увеличить network idle. Это хороший обмен, если экран стал полезен раньше; это плохой аргумент, если измерен только размер одного чанка. Важно заранее зафиксировать, какой момент загрузки должен сдвинуться и какой вторичный эффект допустим.
| Фрагмент отчёта | Плохая запись | Проверяемая запись |
|---|---|---|
| Симптом | Долго грузится | Карточка появляется после длинного scripting, хотя ответ HTML уже получен |
| Причина | Много JavaScript | В trace участок main thread привязан к модулю фильтров; это гипотеза до source-map проверки |
| Действие | Оптимизировать бандл | Не загружать модуль подсказок до первого взаимодействия и оставить проверку fallback |
| Критерий | Стало лучше | Повторить тот же Reload и сравнить момент полезного блока и длительность участка scripting |
Меняем один критический путь за раз
Когда владелец назван, действие становится обычной инженерной работой. Для сети это может быть устранение лишнего redirect, раннее обнаружение ресурса или перенос критического файла на подходящий origin. Для JavaScript — разделение entry, удаление неиспользуемой ветки, откладывание виджета или уменьшение синхронной инициализации. Для CSS — критичный минимум, порядок подключения и уменьшение селекторов или DOM там, где trace показал style и layout. Для изображения — правильный размер, формат, ранний URL и отказ от lazy loading именно у первого значимого изображения.
Ни одна из этих мер не универсальна. preload помогает только ресурсу, который действительно нужен первому экрану; лишние preload конкурируют за сеть. Code splitting помогает, если код перестал быть частью начального пути; если модуль сразу нужен для рендера, дополнительный запрос может ухудшить ситуацию. Оптимизация картинки не поможет, если она уже скачана и ждёт занятый main thread. Поэтому после каждого изменения возвращаемся к тому же профилю, а не переносим удачную технику на все ресурсы подряд.
- Сформулировать полезный первый экран и зафиксировать URL, кэш, viewport, throttle и хэш сборки.
- Записать один Reload в DevTools с Network, Performance и screenshot, не называя результат production-метрикой.
- Разметить в отчёте документ, критический ресурс, участки scripting, style или layout и момент появления полезного блока.
- Выбрать одного владельца и одно изменение, которое должно сдвинуть конкретную полосу, а не весь мир сразу.
- Повторить тот же сценарий, сравнить только заявленный критерий и записать побочный эффект.
- Лишь после устойчивого лабораторного результата решать, нужна ли полевая метрика или выпускная проверка на реальных устройствах.
Итог: профиль превращает жалобу в маршрут
Первая загрузка не становится понятной от одного Lighthouse-числа, веса JavaScript или события load. Нужна короткая карта: что браузер получил, что обнаружил поздно, что заняло главный поток и что не успело появиться на экране. Такая карта не требует большой платформы наблюдаемости, но запрещает прятать разные причины под слово «тяжёлая».
В этом упражнении нет заявленного trace конкретного сайта и нет обещания универсального порога. Есть воспроизводимый fixture для классификации и маршрут для настоящего профиля в браузере. Следующая статья разберёт, почему документ, CSS, JavaScript и изображение образуют зависимую критическую цепочку даже тогда, когда отдельные запросы выглядят быстрыми.
Проверяемые источники
- MDN: Navigation Timing — объект navigation entry и границы загрузки документа, DOM и обработчиков события загрузки
- MDN: PerformanceResourceTiming — состав времён и размеров отдельных ресурсов, включая transferSize, encodedBodySize и ограничения кросс-доменных записей
- MDN: Performance data — типы записей Performance API и смысл developer marks и measures
- Chrome DevTools: Inspect network activity — разделение запросов по типам, фильтры и проверка водопада без догадки по одному общему времени загрузки
- Chrome DevTools: Performance features reference — как trace показывает работу loading, scripting, rendering и painting, а также custom marks
- web.dev: Web Vitals — современная рамка пользовательских метрик; в переиздании она помогает не смешивать скорость отображения с одним сетевым числом
- web.dev: Optimize Largest Contentful Paint — разделяет TTFB, задержку старта критического ресурса, его загрузку и задержку отрисовки; это позднейшая терминология для проверки гипотезы, а не выданная за отчёт 2019 года