{"index":21,"slug":"editorial-2027-06-practice-performance-capstone","title":"Медленная первая загрузка: как отделить TTFB от блокирующих ресурсов","excerpt":"Практический маршрут для случая, когда пользователь видит пустой экран: разделяем ожидание ответа, передачу HTML, CSS и работу JavaScript, а затем проверяем одну гипотезу повторным замером.","contentHtml":"

Пользователь открывает страницу и несколько секунд видит пустой или неполный первый экран. В отчёте команда замечает одну цифру: «загрузка заняла 2,4 секунды», — и начинает уменьшать JavaScript. Но эти 2,4 секунды могли включать ожидание ответа сервера, передачу HTML, построение стилей и длинную задачу на главном потоке. Цена неверной гипотезы — изменить слой, который не владеет задержкой, и получить новый риск без изменения симптома.

Разбор начинается с границы. TTFB (time to first byte) показывает время до первого байта ответа, но не время готовности экрана. После первого байта браузер ещё получает тело документа, строит DOM и CSSOM, загружает ресурсы и выполняет код. Ниже — маршрут, который связывает жалобу пользователя с одним участком критического пути и позволяет проверить исправление в тех же условиях.

Механизм: один экран, несколько причин

Браузер начинает навигацию с запроса. Сервер формирует ответ и отправляет первый байт. Только после этого браузер получает весь HTML и строит DOM. Когда parser встречает таблицу стилей, он ждёт CSSOM для расчёта стилей. Синхронный script может остановить parser. Отложенный код продолжает занимать главный поток даже после завершения сетевой загрузки.

Поэтому время нужно разложить. Участок до первого байта включает соединение, кеш, proxy и серверный обработчик. Участок от первого байта до конца ответа относится к размеру HTML и передаче. Ранний CSS влияет на построение стилей. Скрипт может задержать DOM, layout или обработку пользовательского ввода. Одна общая длительность скрывает владельца и действие.

Диагностика первой загрузки
СимптомПричинаПроверкаДействие
TTFB стабильно выше 300 мсСервер или зависимость задерживает начало ответаСравнить responseStart - startTime и журнал handlerПрофилировать серверный путь; не начинать с bundle
TTFB нормален, HTML приходит долгоБольшой ответ или медленная передачаСравнить responseEnd - responseStart и размер HTMLУменьшить ответ, проверить сжатие и кеш
HTML пришёл, первый экран ждёт CSSРанний stylesheet блокирует построение стилейСверить waterfall, initiator и видимый результатСократить критический CSS или отложить второстепенный
Сеть закончилась, экран не готовДолгая задача на главном потокеПосмотреть long tasks и длительность scriptРазбить работу или перенести некритичную часть
После async ломается интерфейсКод потерял порядок инициализацииПроверить зависимости скриптов и ошибки консолиВернуть порядок или использовать defer, если он подходит
Одно измерение лучше остальныхСработал кеш или изменились сеть и устройствоПовторить серию и сравнить p50/p95Не принимать единичный прогон за результат
Критический путь первой загрузки: запрос, TTFB, HTML, CSS и JavaScript
Схема разделяет ожидание ответа, получение HTML и работу ресурсов. Каждому участку нужен собственный замер и собственное действие.

Учебный пример: серверная задержка видна отдельно

Ниже — минимальный локальный сервер. Параметр mode=slow добавляет задержку перед отправкой заголовков. Пример намеренно проверяет только серверный участок. Он не моделирует мобильную сеть, кеш браузера, CDN, рендеринг или реальную нагрузку. Его вывод нельзя выдавать за production-результат.

import { createServer } from 'node:http'; import { performance } from 'node:perf_hooks'; const server = createServer((request, response) => { const url = new URL(request.url, 'http://127.0.0.1'); const delay = url.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 { port } = server.address(); 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 миллисекунд длиннее. Точное значение зависит от машины, поэтому сравнивайте режимы в одном запуске. Если различия нет, проверьте URL, единицы времени и то, что задержка стоит до writeHead, а не после отправки ответа.

Этот пример показывает причинность только для TTFB. Клиент ждёт тело целиком, поэтому его общее время включает передачу HTML. Чтобы измерить участки в браузере, откройте ту же страницу и прочитайте navigation entry. Не переносите число из Node в вывод о FCP или LCP: серверный пример не видит отрисовку.

Читаем Navigation Timing

В браузере найдите запись типа navigation. Поля requestStart, responseStart и responseEnd дают точки для разделения запроса, первого байта и конца ответа. Поле domContentLoadedEventEnd показывает завершение соответствующего события. Это диагностические временные точки, а не готовая оценка качества экрана.

const navigation = performance.getEntriesByType('navigation')[0]; if (navigation) { console.table({ ttfb: navigation.responseStart - navigation.requestStart, html: navigation.responseEnd - navigation.responseStart, domContentLoaded: navigation.domContentLoadedEventEnd - navigation.startTime }); }

Если responseStart - startTime велик, разделите соединение, кеш, proxy и серверную задержку по журналу и повторному замеру. Если TTFB мал, а responseEnd - responseStart велик, проверьте размер HTML, сжатие и сеть. Если оба участка малы, но пользователь всё ещё видит пустой экран, переходите к ресурсам и главному потоку. Такой отрицательный путь важен: отсутствие серверной проблемы не доказывает, что страница быстрая.

DOMContentLoaded нельзя называть временем готовности экрана. Событие связано с разбором документа и отложенными скриптами. Изображения, шрифты, layout, paint и работа JavaScript могут продолжаться. Для визуального симптома нужен отдельный наблюдаемый критерий. Если команда использует FCP или LCP, измеряйте его тем же браузерным сценарием и не подменяйте его TTFB.

Ресурсы и порядок выполнения

После navigation entry соберите записи ресурсов. Для каждой записи важны URL без секретных параметров, тип ресурса, время начала, конец ответа, initiator и размер. Сначала ищите ресурс, который действительно пересекается с критическим участком. Длинная полоса в waterfall сама по себе не доказывает блокировку: шрифт мог начать загрузку после отрисовки главного содержимого.

Обычный stylesheet влияет на построение стилей. Синхронный script может остановить parser. defer оставляет порядок отложенных скриптов и запускает их после разбора документа. async запускает скрипт по готовности и меняет порядок. Если второй файл использует глобальный объект первого, безоговорочная замена на async создаёт отрицательный путь: сеть стала быстрее, но приложение упало до инициализации.

Поле renderBlockingStatus полезно только при поддержке конкретного браузера и конкретной записи. Отсутствующее значение означает отсутствие данных, а не доказательство, что ресурс не блокирует. Сверяйте API с HTML и визуальным результатом. Если сведения не совпадают, оставьте это ограничение в отчёте и опирайтесь на поддерживаемые наблюдения.

Как выбрать следующее изменение
НаблюдениеПервое изменениеРиск
Высокий TTFB при разных клиентахПрофилировать handler и его зависимостиИзменение кеша скроет, но не устранит причину
Большой HTML при нормальном TTFBПроверить структуру ответа и сжатиеСложнее кеширование и отладка шаблона
CSS задерживает первый полезный контентОтделить критические стили от второстепенныхFOUC и рассинхрон стилей
Script создаёт длинную задачуРазбить вычисление или отложить егоИзменится порядок состояния и событий

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

  1. Зафиксировать симптом: URL, устройство, браузер, сеть, режим кеша и видимый момент задержки.
  2. Повторить страницу серией прогонов, а не одним открытием. Сохранить сырые navigation и resource entries.
  3. Разделить TTFB, передачу HTML и время после получения документа.
  4. Сопоставить каждый участок с серверным журналом, waterfall и главным потоком браузера.
  5. Выбрать одну гипотезу и изменить только её: handler, HTML, CSS, script или порядок ресурса.
  6. Повторить тот же сценарий и сравнить медиану и p95. Проверить, что визуальный симптом изменился вместе с измеряемым участком.
  7. Проверить отрицательный путь: после изменения async/defer открыть страницу с медленной сетью и убедиться, что зависимости не запускаются в неверном порядке.
  8. Зафиксировать ограничения и вернуть изменение, если улучшилась одна цифра, но ухудшился первый экран или интерактивность.

Ограничения

Navigation Timing описывает события навигации, но не знает архитектуру backend и не устанавливает пороги качества. Resource Timing может скрывать часть сведений из-за политики приватности и кросс-доменных ограничений. Браузеры различаются по поддержке отдельных полей. Поэтому один API не заменяет сетевой журнал, профиль главного потока и визуальный замер.

Локальный сервер с фиксированной задержкой проверяет ветвление диагностики, но не показывает распределение latency, холодный кеш, CDN, TLS, балансировщик и конкуренцию запросов. Числа из примера не являются обещанием для production. Полевой вывод требует зафиксированных условий и серии наблюдений на целевом устройстве.

Ускорение одного участка может ухудшить другой. Отложенный CSS уменьшает блокировку, но может вызвать вспышку нестилизованного контента. Разбиение JavaScript сокращает длинную задачу, но увеличивает количество границ состояния. Проверяйте не только сеть, но и визуальную стабильность, ошибки консоли и интерактивность.

Проверяемый критерий готовности

Диагностика готова, если для страницы есть серия повторяемых замеров, отдельные значения TTFB и передачи HTML, список ранних ресурсов с initiator и запись о главном потоке. Для выбранной гипотезы названо одно действие, а повторный прогон показывает изменение именно целевого участка. Первый экран не ухудшился, консоль не получила новую ошибку, а отрицательный путь проверен на медленной сети. Если команда может сказать только «страница стала быстрее», причина и критерий ещё не определены.

Проверяемые источники

"}