В waterfall длинная полоса у шрифта выглядит как причина медленной страницы, но шрифт мог начать загрузку после того, как главный контент уже отрисовался. Цена ошибки — менять порядок ресурсов без понимания зависимости, получить вспышку нестилизованного текста и увеличить количество сетевых запросов.
Механика здесь простая: ресурс задерживает страницу только через конкретную зависимость — parser, CSSOM, layout, script или компонент, который ждёт ответ. Воспроизводимый пример покажет, как собрать записи ресурсов в браузере и отфильтровать их по времени старта и статусу блокировки.
Полоса времени не объясняет причинность
Waterfall отвечает на вопрос «когда ресурс был активен». Он не отвечает на вопрос «почему экран ждал». Для причинности нужны DOM-порядок, атрибуты rel и async/defer, тип ресурса и событие, которое использовало результат. Две полосы одинаковой длины могут иметь разную цену: одна относится к hero-изображению, другая — к аналитике после загрузки.
| Признак | Что он даёт | Следующая проверка |
|---|---|---|
| name | какой URL запрошен | секреты и query удалены? |
| initiatorType | кто начал запрос | parser, script, css или fetch |
| startTime | когда началась активность | какое событие было раньше |
| responseEnd | когда пришёл ответ | кто действительно ждал |
| renderBlockingStatus | признак блокировки | поддерживает ли его браузер |
Учебная страница с наблюдателем
Это локальный HTML-фрагмент. Вставьте его в страницу, запущенную любым статическим сервером, откройте DevTools и посмотрите консоль. Входом являются browser PerformanceEntry. Результат — компактный список ресурсов, которые начали работу до события DOMContentLoaded, с указанием инициатора. Если браузер не поддерживает поле, выводится unknown, а не придуманное значение.
const started = performance.getEntriesByType('resource').map((entry) => ({
name: new URL(entry.name).pathname,
initiator: entry.initiatorType,
start: Math.round(entry.startTime),
end: Math.round(entry.responseEnd),
blocking: entry.renderBlockingStatus ?? 'unknown',
}));
const navigation = performance.getEntriesByType('navigation')[0];
const domEnd = navigation?.domContentLoadedEventEnd ?? Infinity;
console.table(started.filter((entry) => entry.start < domEnd));Поля start и end — миллисекунды относительно начала навигации. Фильтр оставляет ресурсы, начавшие работу до DOMContentLoaded, но это ещё не список причин. Для каждой строки проверьте потребителя: script мог запросить JSON после DOM, а CSS мог повлиять на первый layout. Наблюдатель сокращает поиск, а не заменяет чтение кода.
Как CSS и script меняют порядок
CSS, подключённый в head, нужен браузеру для построения стилей и поэтому часто попадает в раннюю часть цепочки. Обычный script без атрибута может остановить parser. defer загружает скрипт параллельно и исполняет его после разбора документа в порядке появления. async отдаёт порядок загрузке и подходит только для независимого кода.
Любое изменение атрибута проверяется через зависимость. Если script обращается к DOM-узлу, defer обычно подходит. Если он ожидает библиотеку выше, async может дать race. Если стиль нужен только внутри раскрываемого блока, его можно разделить, но нужно проверить скачок layout и доступность. Критерий решения — не «полоса стала короче», а перестал ли нужный компонент ждать ресурс.
Размер и приоритет не одно и то же
Большой ресурс может загружаться низким приоритетом после первого экрана, а маленький CSS блокировать парсер. Проверяйте размер вместе с инициатором, типом, приоритетом и моментом потребления. В Resource Timing есть данные о времени и размерах, но доступ к кросс-доменным значениям зависит от заголовков и политики браузера.
| Изменение | Положительный эффект | Что сломать легко |
|---|---|---|
| defer для независимого script | parser не ждёт выполнение | код, рассчитывающий на ранний глобальный объект |
| preload для критичного ресурса | запрос начинается раньше | лишний запрос и неправильный тип |
| разделение CSS | меньше раннего CSS | FOUC, layout shift и порядок правил |
Порядок чтения waterfall
- Зафиксировать страницу, режим кэша, viewport и браузер, иначе строки несопоставимы.
- Отфильтровать resource entries по initiatorType и времени до DOMContentLoaded.
- Для каждой ранней строки найти тег, script или CSS-правило, которое её инициировало.
- Проверить, кто читает ответ и действительно ли этот потребитель влияет на первый экран.
- Изменить один атрибут или порядок загрузки и повторить запись.
- Сравнить визуальный результат, long tasks и сетевой waterfall; не принимать сокращение одной полосы как успех.
Ограничения и следующий шаг
PerformanceObserver и renderBlockingStatus зависят от поддержки браузера. Сетевые записи скрывают часть кросс-доменных данных, а локальная страница не отражает задержки мобильной сети. Визуальный результат зависит от шрифтов, viewport и устройства. Поэтому наблюдатель полезен как диагностический слой, но не как единственный критерий качества.
Следующий шаг — сохранить в тестовом отчёте таблицу «ресурс → инициатор → потребитель → действие». В ней должны быть хотя бы один оставленный ресурс и один ресурс, который вы сознательно отложили. Это помогает объяснить компромисс и не возвращать preload или async при следующем изменении шаблона.
Проверяемые источники
- W3C Navigation Timing Level 2 — Working Draft, 25 February 2026. Определяет навигационные временные метки и связь между этапами загрузки документа. Граница применимости: Рабочий черновик стандарта не сообщает значения метрик вашего браузера и не заменяет полевой сбор.
- W3C Performance Timeline — Candidate Recommendation Draft, 21 May 2025. Определяет PerformanceEntry, getEntries и PerformanceObserver как основу чтения метрик. Граница применимости: Спецификация не задаёт пороги качества и не гарантирует одинаковый набор entry во всех движках.
- W3C Resource Timing — Candidate Recommendation Draft, 20 April 2026. Определяет записи ресурсов, размеры и render-blocking status для сетевого слоя страницы. Граница применимости: Не описывает серверную архитектуру, пользовательский опыт целиком или причинность конкретной задержки.