DarkRiDDeR14 мин

Медленная первая загрузка: отделяем TTFB от блокирующих ресурсов

FrontendПроизводительность

Пользователь видит пустой экран, а команда получает только одну цифру «страница грузится 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
HTMLresponseEnd − responseStartразмер и передача документа
РесурсresponseEnd − startTimeCSS, script, font, image
Главный потокlong task или task durationпарсинг и выполнение JS
Критический путь страницы от запроса до главного содержимого: серверная задержка, HTML, CSS и JavaScript.
Схема разделяет участки времени. Рядом с каждой задержкой нужен собственный замер, иначе исправление выбирается по догадке.

Локальный сервер с двумя режимами

Вход примера — 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разбить работу или отложитьсложнее порядок и состояние

Порядок проверки

  1. Записать пользовательский симптом и выбрать одну страницу с фиксированным URL.
  2. Сравнить TTFB и время получения HTML на холодном и тёплом кэше.
  3. Прочитать navigation entry и выписать длительности без округления до одной общей цифры.
  4. Посмотреть resource entries и найти CSS, script или font, которые начинают работу раньше нужного.
  5. Изменить один участок: сервер, размер HTML или порядок загрузки ресурса.
  6. Повторить тот же прогон и сравнить не только среднее, но и медиану или 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 для сетевого слоя страницы. Граница применимости: Не описывает серверную архитектуру, пользовательский опыт целиком или причинность конкретной задержки.