Симптом: карточка товара открывается, серверный лог показывает короткий ответ HTML, но пользователь несколько секунд видит фон и каркас без товара. Первое поспешное решение — сжать hero-изображение. Оно может не изменить экран вообще, если изображение уже скачано и ждёт выполнения стартового JavaScript. Обратная ошибка тоже частая: вынести код в другой чанк, хотя картинка вообще не была обнаружена до запуска приложения. Цена такого поиска — серия случайных правок, которые нельзя объяснить следующему разработчику.
Ниже учебный разбор, а не trace реального сайта. Для контролируемого профиля мы задаём четыре независимые полосы: документ и сеть — 180 ms, JavaScript — 165 ms, CSS — 90 ms, hero после загрузки — 45 ms. Эти числа существуют только в fixture модуля и проверяют, что отчёт не смешивает владельцев. Они не складываются в «настоящие 480 ms», не описывают устройство пользователя и не дают права писать о production-результате. На их основе можно честно отрепетировать порядок расследования.
Фиксируем учебный сценарий и границу полезности
Полезным экраном в этом случае считаем три вещи: название товара, цену и hero-изображение рядом с кнопкой заказа. Spinner не считается результатом: он говорит только, что код начал работу. Это определение важно, потому что команда иначе может улучшить момент появления skeleton и объявить победу, хотя покупатель всё ещё не знает, что покупает. До любых изменений фиксируем тот же URL, тот же вариант страницы, состояние кэша и viewport.
В controlled fixture документ уже получил ответ, CSS имеет отдельный этап, JavaScript — отдельную работу, изображение — отдельные байты и decode. Так мы заранее не объявляем один слой главным. Если после настоящего Reload окажется, что hero вообще не на первом экране или содержимое текстовое, сценарий меняется: диагностика всегда начинается с фактического визуального критерия, а не с названия файла hero.webp.
| Полоса fixture | Данные fixture | Что можно утверждать | Что утверждать нельзя |
|---|---|---|---|
| Документ и сеть | 180 ms, 18 000 bytes | Классификатор выделил ответ документа отдельным владельцем | Что origin конкретного сайта отвечает за 180 ms |
| JavaScript | 165 ms, 96 000 bytes | Отдельно учтён parse и execute app.js в учебном профиле | Что любой bundle такого размера блокирует ровно 165 ms |
| CSS | 90 ms, 24 000 bytes | CSS не потерян среди сетевых или JS-данных | Что stylesheet в реальном браузере всегда блокирует весь этот интервал |
| Hero | 45 ms, 72 000 bytes | Загрузка и decode изображения имеют свой владелец | Что responseEnd равен моменту видимости изображения |
Такая таблица полезна именно своей скромностью. Она не говорит, какая полоса длиннее на устройстве пользователя, и не добавляет несуществующий waterfall. Она даёт контракт для инструмента и для автора статьи: в дальнейших фразах сеть означает сетевые записи, JavaScript — работу main thread, CSS — готовность стилей, изображение — путь до paint. Если фактический trace позже покажет перекрытие, модель не сломается: полосы всё равно остаются разными владельцами.
Первый вопрос: какой ресурс открывает полезный экран
В настоящем проекте я бы начал не с главного чанка, а с DOM и screenshot. Есть ли title и price в исходном HTML? Есть ли у hero прямой src или URL появляется в состоянии приложения? Не скрывает ли контейнер класс is-loading до завершения bootstrap? Эти вопросы часто дают результат быстрее, чем сортировка Network по размеру: если полезный блок создаётся только после renderProduct(), его картинка физически не могла стартовать раньше выполнения этого кода.
После этого проверяем waterfall в двух направлениях. От документа вперёд: когда открылись CSS, app.js и hero. От полезного элемента назад: какой URL, стиль и код нужны именно ему. Если hero начинает запрос после app.js, фиксируем не «медленную сеть», а позднее обнаружение. Если hero стартует рано, но screenshot меняется поздно, сеть перестаёт быть первой гипотезой: смотрим main thread, decode и скрывающую логику.
const hero = document.querySelector("[data-product-hero]");
const css = document.querySelector("link[href*=app.css]");
console.table({
heroSrc: hero && hero.currentSrc,
heroComplete: hero && hero.complete,
heroNaturalWidth: hero && hero.naturalWidth,
stylesheetLoaded: css && css.sheet !== null,
productHidden: document.querySelector(".product.is-loading") !== null,
});
Этот фрагмент — локальная проверка состояния после загрузки, не автоматический benchmark. img.complete не доказывает, что изображение уже показано пользователю, а link.sheet не отвечает на вопрос о стоимости layout. Зато он быстро отделяет ситуацию «URL отсутствует или изображение не готово» от ситуации «ресурс уже доступен, ищем работу рендера». Если запускать его поздно вручную, фиксируйте это в заметке: console-проверка после события не воспроизводит точный момент первого paint.
Вторая проверка: не держит ли экран стартовый JavaScript
Представим, что waterfall показывает ранний hero и завершённый CSS, но полезный screenshot всё равно появляется после блока scripting. Тогда цель — не «разбить всё на чанки», а найти работу, которая происходит до renderProduct. Это может быть инициализация фильтров, формирование рекомендаций, синхронный разбор большой конфигурации или сторонний виджет. Любая из этих функций имеет другой безопасный момент запуска и другой риск для поведения страницы.
В Chrome DevTools выбираем участок main thread между окончанием критического запроса и появлением нужного screenshot. Затем смотрим Bottom-up или Call Tree, если source map позволяет увидеть исходные функции. Без source map не подставляем название модуля из фантазии: пишем путь скомпилированного ресурса и оставляем задачу на сопоставление. Отсутствие точного имени — не повод вернуться к догадке по весу бандла.
| Факт после Reload | Следующая гипотеза | Минимальный эксперимент | Критерий |
|---|---|---|---|
| Hero и CSS стартовали рано, перед экраном длинный scripting | Bootstrap выполняет некритичную работу | Временно убрать второстепенный виджет из initial path | Сдвигается момент полезного screenshot и сокращается участок scripting |
| Hero стартовал после app.js | URL создаётся приложением | Показать URL в HTML или проверить preload только для hero | На waterfall запрос hero начинается раньше при тех же условиях |
| Hero завершился, но экран меняется после style/layout | DOM или классы создают поздний render | Сгруппировать записи DOM и убрать лишний ранний layout | Уменьшается work между ресурсом и paint |
| CSS заканчивается поздно | Стиль найден поздно или конкурирует за сеть | Проверить порядок link и import-цепочку | CSS приходит до нужного визуального этапа без новых ошибок стиля |
Важно оставить эксперимент узким и обратимым. Не удаляйте сразу половину приложения. Отключите один реально второстепенный блок под локальным флагом или в отдельной ветке и повторите сценарий. Если экрана это не коснулось, фикс возвращают и не рекламируют «оптимизацию». Если сдвиг есть, следующий шаг — безопасно изменить загрузочный контракт: отложить модуль, оставить заглушку, проверить ошибку загрузки и убедиться, что пользовательская функция не исчезла навсегда.
Третья проверка: CSS и изображение не завершаются в один момент
Даже в аккуратном водопаде нельзя считать responseEnd финишем визуальной работы. Браузер должен применить стиль, рассчитать геометрию, при необходимости декодировать изображение и отрисовать кадр. Если JavaScript несколько раз читает размеры и тут же меняет классы, он может создавать повторные layout между готовым hero и paint. В этом случае перекодировка картинки даст небольшой сетевой выигрыш, но проблема полезного экрана останется в main thread.
С другой стороны, картинка может быть действительно слишком поздней: URL находится в background-image внешнего CSS, viewport получает неподходящий большой вариант или первый элемент помечен lazy loading. Проверка должна назвать один из этих фактов. У URL в CSS нет автоматического права на preload: сначала убедитесь, что это тот элемент, который нужен без прокрутки. Когда это доказано, раннее объявление ресурса или изменение разметки становятся осмысленным действием, а не массовой настройкой.
Проверяем изменение тем же маршрутом, а не одним размером файла
Допустим, trace подтвердил, что блок рекомендаций синхронно строится до карточки. После переноса его за первую отрисовку нельзя останавливаться на уменьшении initial chunk. Повторяем Reload в тех же условиях и смотрим четыре вещи: появился ли полезный screenshot раньше, исчезла ли конкретная работа scripting, не стартовал ли hero позже из-за нового порядка, не сломалась ли карточка без рекомендаций. Только такой набор позволяет отличить улучшение критического пути от перемещения задержки в соседнюю полосу.
Если изменение касается CSS, дополнительно проверяем отсутствие скачка layout и доступность контента без внешнего файла. Если касается изображения — его фактические размеры, корректный alt и fallback. Если касается загрузки модуля — состояние ошибки и медленную сеть. Производительность первой загрузки не освобождает от корректности: пустой, но быстрый экран не считается результатом. В 2019 это особенно важно для постепенных улучшений поверх существующего интерфейса, а не для демонстрации красивого измерения.
performance.mark("product: shell-visible");
showProductShell();
loadRecommendationsLater().catch(() => {
// Карточка уже полезна; ошибка второстепенного блока не скрывает цену и заказ.
showRecommendationFallback();
});
performance.mark("product: recommendations-scheduled");
const navigation = performance.getEntriesByType("navigation")[0];
const shellVisible = performance.getEntriesByName("product: shell-visible")[0];
console.log("shell after navigation", Math.round(
shellVisible.startTime - navigation.startTime,
));
Название initial-shell здесь важно: измеряется момент, когда показали каркас продукта, а не вся страница и не подтверждённая пользовательская метрика. Для trace можно использовать performance.mark как ориентир рядом с Network и main thread. Но значение mark не становится доказательством, что контент полезен, пока это не подтверждено заранее определённым DOM и screenshot-критерием. Так команда не подменяет результат измерением удобной, но пустой стадии.
Маршрут выпуска без ложного production-отчёта
Перед выпуском инженер может написать честный итог: «В лабораторном Reload при таких-то условиях полезный блок появился раньше после переноса второстепенного модуля; отдельно проверены Network, main thread и fallback». Он не должен писать «все пользователи получили минус 300 ms», если полевая выборка не собиралась. Для поля нужны отдельные согласованные метрики, сегменты устройств и правила приватности. Лабораторный результат остаётся основанием для merge конкретной правки, а не заменой аналитики.
Если профиль не дал однозначной причины, это тоже нормальный итог. Сохраняем trace, условия и исключённые гипотезы: hero стартует рано, CSS готов до первого screenshot, но source map отсутствует для долгого scripting. Следующая задача тогда конкретна — восстановить карту исходников или изолировать функцию, а не повторять сжатие изображений. Неопределённость уменьшается фактами, а не более ярким заголовком оптимизации.
- Назвать полезный первый экран и записать его DOM-признаки до открытия DevTools.
- Снять один повторяемый Reload с фиксированными условиями, сохранив Network, screenshot и main-thread trace.
- Проверить, когда стартовали HTML, CSS, app.js и нужное изображение, не используя размер файла как приговор.
- Найти раннюю разорванную границу: позднее обнаружение, long scripting, style/layout или decode и paint.
- Сделать один обратимый эксперимент и проверить конкретный ожидаемый сдвиг, а также fallback и визуальную корректность.
- Сформулировать лабораторный вывод с его пределами; production-числа добавлять только после отдельного сбора полевых данных.
Итог: быстрый ответ — это только начало расследования
Учебный fixture показывает важный навык: даже когда каждая полоса имеет число, не надо строить из них фальшивую общую скорость. Первая загрузка становится полезной после зависимой работы HTML, сети, CSS, JavaScript, изображения и paint. У каждой части есть собственный инструмент проверки и собственный способ сломаться.
Поэтому хороший разбор начинается с видимого критерия, идёт назад по зависимостям и заканчивается одним воспроизводимым изменением. В нём есть точные ограничения: fixture не является browser trace, trace не является полевой статистикой, а маленький бандл не равен быстрому экрану. Такая дисциплина делает следующую оптимизацию короче, потому что она отвечает на конкретный вопрос, а не на жалобу «страница тяжёлая».
Проверяемые источники
- 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 года