Пользователь видит пустой экран, а команда получает только одну цифру «страница грузится 2,4 секунды». Цена ошибки — оптимизировать bundle, когда 1,6 секунды ушло до первого байта HTML, или ускорить сервер, когда браузер ждёт блокирующий CSS. Эти причины требуют разных владельцев и разных изменений.
Разделим путь на три наблюдения: время до первого байта, получение HTML и блокировку от ресурсов. Учебный локальный сервер намеренно задаёт задержку, поэтому результат легко повторить. В браузере дополнительно прочитаем Navigation Timing и Resource Timing, но не будем выдавать одну метрику за пользовательский опыт.
Критический путь начинается до JavaScript
Браузер не может построить документ, пока не получил HTML. После этого parser встречает CSS, обычные script и изображения, которые меняют порядок работы. TTFB (time to first byte) отвечает только за ожидание первого байта ответа. Он не включает весь HTML и тем более не описывает время до полезного пикселя.
| Участок | Метрика | Что проверяем |
|---|---|---|
| До ответа | responseStart − requestStart | сервер, сеть, proxy |
| HTML | responseEnd − responseStart | размер и передача документа |
| Ресурс | responseEnd − startTime | CSS, script, font, image |
| Главный поток | long task или task duration | парсинг и выполнение JS |
Локальный сервер с двумя режимами
Вход примера — query-параметр mode. Для slow сервер ждёт 300 миллисекунд перед заголовками, для fast отвечает сразу. Клиент измеряет время до получения тела. Ожидаемый результат: медленный режим имеет больший TTFB, а не «плохой JavaScript». Запуск безопасен: сервер слушает только loopback.
import { performance } from 'node:perf_hooks';
import { createServer as createHttpServer } from 'node:http';
const server = createHttpServer((request, response) => {
const delay = new URL(request.url, 'http://local').searchParams.get('mode') === 'slow' ? 300 : 0;
setTimeout(() => {
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
response.end('<main>ready</main>');
}, delay);
});
server.listen({ host: '127.0.0.1', port: 0 }, async () => {
const address = server.address();
const port = address.port;
for (const mode of ['fast', 'slow']) {
const started = performance.now();
const response = await fetch('http://127.0.0.1:' + port + '/?mode=' + mode);
await response.text();
console.log(mode, Math.round(performance.now() - started), response.status);
}
server.close();
});Ожидаемый вывод — две строки со статусом 200, причём slow примерно на 300 миллисекунд больше. Точное число зависит от машины, поэтому сравниваем режимы в одном запуске. Так появляется проверяемый симптом: различие находится на серверном ожидании. Для browser-показателей нужен браузерный прогон с тем же HTML и набором ресурсов.
Что читать в Navigation Timing
Объект PerformanceNavigationTiming содержит временные точки навигации. Для диагностического сообщения достаточно вывести requestStart, responseStart, responseEnd и domContentLoadedEventEnd. Вычитание даёт длительность участка, но абсолютное число нужно сопоставлять с условиями: cache, сеть, устройство и режим браузера меняют картину.
Не называйте domContentLoaded временем готовности экрана. Событие означает завершение разбора DOM и отложенных скриптов, но изображения и отрисовка могут продолжаться. FCP (first contentful paint) и LCP (largest contentful paint) относятся к визуальному результату, поэтому TTFB и DOMContentLoaded нельзя использовать как их замену. Для пользовательского симптома нужен отдельный критерий визуальной готовности.
Блокирующие ресурсы и компромисс
Обычный CSS блокирует построение стилей, а синхронный script может остановить parser. Перенести всё в async недостаточно: порядок выполнения изменится, а код, который ожидает DOM или глобальную библиотеку, может сломаться. defer сохраняет порядок отложенных скриптов, но не убирает размер передачи.
Поле renderBlockingStatus полезно читать только при поддержке конкретного браузера. Отсутствующее значение означает «нет данных», а не «ресурс точно не блокирует». Поэтому сверяйте запись с HTML-атрибутами и наблюдаемым изменением первого экрана.
| Наблюдение | Действие | Цена |
|---|---|---|
| высокий TTFB | профилировать серверный handler | нагрузка на backend и зависимости |
| большой HTML | сжать или уменьшить ответ | сложнее шаблон и кэширование |
| ранний блокирующий CSS | разделить critical и остальное | риск рассинхрона стилей |
| долгий script | разбить работу или отложить | сложнее порядок и состояние |
Порядок проверки
- Записать пользовательский симптом и выбрать одну страницу с фиксированным URL.
- Сравнить TTFB и время получения HTML на холодном и тёплом кэше.
- Прочитать navigation entry и выписать длительности без округления до одной общей цифры.
- Посмотреть resource entries и найти CSS, script или font, которые начинают работу раньше нужного.
- Изменить один участок: сервер, размер HTML или порядок загрузки ресурса.
- Повторить тот же прогон и сравнить не только среднее, но и медиану или p95.
Ограничения и следующий шаг
Node-замер не знает о layout, paint, LCP и работе главного потока браузера. Navigation Timing — временная модель навигации, а не готовый ответ о причине. Сетевые записи могут быть скрыты политикой приватности или кросс-доменными ограничениями. Поэтому локальный пример проверяет только серверный участок.
Следующим шагом подключите браузерный прогон к тестовой странице и сохраните четыре ряда: navigation, resources, long tasks и визуальную метрику. Меняйте одну причину за раз. Если TTFB стабилен, а экран задерживает CSS или JS, не переносите работу обратно на сервер.
Проверяемые источники
- 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 для сетевого слоя страницы. Граница применимости: Не описывает серверную архитектуру, пользовательский опыт целиком или причинность конкретной задержки.