TTFB в 2026: снижение времени до первого байта до 200 мс с edge-кэшированием, ISR и HTTP/3

Практический разбор: как я снижаю TTFB до 200 мс в 2026 с edge-кэшированием, ISR, HTTP/3 и 103 Early Hints. С рабочими конфигами Next.js, nginx.

TTFB в 2026: гайд по снижению до 200 мс

Обновлено: 17 сентября 2026

TTFB (Time to First Byte), это время от отправки HTTP-запроса до получения первого байта ответа сервера. В 2026 году веб-производительность считается здоровой при TTFB ≤ 200 мс для 75-го перцентиля пользователей и «требует внимания» между 200 и 800 мс, если верить данным Chrome UX Report. Ниже я разбираю, как реально сокращаю TTFB на проде: edge-кэширование через CDN, инкрементальная статическая регенерация (ISR), HTTP/3, 103 Early Hints, стриминговый SSR и точечная настройка бэкенда, с рабочими конфигами, а не общими советами.

  • Целевое значение TTFB на 2026 год не превышает 200 мс на 75-м перцентиле; порог «нужна оптимизация» начинается с 800 мс (данные web.dev и CrUX).
  • TTFB напрямую влияет на LCP и косвенно на INP: любая миллисекунда до первого байта откладывает начало парсинга HTML и загрузку критических ресурсов.
  • Edge-кэширование с корректно настроенным Cache-Control: s-maxage и stale-while-revalidate обычно снижает TTFB c 400–800 мс до 20–60 мс.
  • ISR (Next.js, Astro, Nuxt) даёт статический TTFB для динамических страниц: генерация вне запроса, отдача из CDN.
  • HTTP/3 (QUIC) сокращает время рукопожатия на 100–300 мс на мобильных сетях за счёт объединения TLS и транспортного handshake.
  • 103 Early Hints позволяют начать preload критических ресурсов ещё до готовности основного ответа, фактически параллеля TTFB и загрузку ассетов.

Что такое TTFB и почему он важен в 2026

TTFB измеряется браузером как разница между началом навигации (navigationStart) и моментом, когда получен первый байт основного HTML-документа (responseStart). Внутри этого окна упаковано много этапов: DNS-резолвинг, TCP-рукопожатие, TLS-переговоры, отправка запроса, ожидание на сервере (server think time) и передача первого байта по сети. Каждый из этапов оптимизируется своими средствами.

Формально TTFB не входит в Core Web Vitals, но глубоко влияет на две ключевые метрики. LCP не может стартовать раньше, чем браузер получил HTML и распарсил критический CSS. INP страдает косвенно: медленный TTFB часто означает, что и последующие XHR/fetch на этом же бэкенде идут медленно, а обработчики событий блокируются. По данным официального руководства web.dev по TTFB, каждые сэкономленные 100 мс TTFB транслируются примерно в такие же 100 мс улучшения LCP на среднестатистической странице.

В моей практике TTFB, это первое, что я проверяю у клиентов с плохим LCP. Часто оказывается, что фронтендщики уже применили все возможные приёмы (lazy-loading, WebP, preload), а корень проблемы совсем в другом: SSR-сервер, отдающий HTML за 900 мс из-за N+1 запроса к БД. Честно, я на такой сценарий натыкался столько раз, что теперь начинаю аудит именно с бэкенда.

Как правильно измерить TTFB: lab и field-данные

Есть два принципиально разных источника данных о TTFB: лабораторные (WebPageTest, Lighthouse, Chrome DevTools) и полевые (RUM, CrUX) от реальных пользователей. Для принятия решений опирайтесь именно на field-данные, лабораторные измерения из офиса в Москве не покажут, что для пользователя в Иркутске TTFB стабильно > 1.2 c.

В браузере достаточно короткого сниппета через PerformanceNavigationTiming:

// Измеряем TTFB и отправляем в аналитику
new PerformanceObserver((list) => {
  for (const entry of list.getEntriesByType('navigation')) {
    const ttfb = entry.responseStart - entry.requestStart;
    // Учитываем redirect: если он был, добавляем redirect time
    const total = entry.responseStart - entry.startTime;
    console.log({ ttfb, total, redirectTime: entry.redirectEnd - entry.redirectStart });
    // Отправляем в аналитику (Beacon для надёжности при выгрузке страницы)
    navigator.sendBeacon('/rum', JSON.stringify({ metric: 'ttfb', value: total }));
  }
}).observe({ type: 'navigation', buffered: true });

Библиотека web-vitals сама считает TTFB и присылает его через колбэк onTTFB(). Я разбирал сбор всех Core Web Vitals метрик, включая TTFB, в руководстве по Real User Monitoring и CrUX. Там же описал, как разбивать данные по перцентилям (p50/p75/p95), а не смотреть среднее, которое почти всегда лжёт.

Какое значение TTFB считается хорошим?

Google в 2026 году держит те же пороги, что и в 2023: до 800 мс это «хорошо» (пропускной, минимум), до 1800 мс «требует улучшения», выше «плохо». Но эти цифры устарели: конкурентные сайты выходят на 100–200 мс, а Cloudflare и Vercel рапортуют, что 60% их клиентских проектов держат TTFB под 100 мс на статике из edge. Ниже таблица моих внутренних порогов, которые я использую в аудитах.

Класс страницыЦель p75 TTFBПорог тревоги p75Основной рычаг
Статический маркетинг< 100 мс> 250 мсEdge cache + Brotli
SSR продуктовые страницы< 200 мс> 500 мсISR / edge-функции
Персонализированный SSR< 400 мс> 800 мсСтриминг + БД-кэш
API-ответ (JSON)< 150 мс> 350 мсИндексы, connection pooling
Админка / dashboard< 500 мс> 1200 мсPrefetch + skeleton

Что вызывает высокий TTFB: разбор по компонентам

Разбираю каждый компонент TTFB на реальном примере: приложение на Next.js 15, размещённое на VPS в европейском датацентре, пользователь в Санкт-Петербурге.

1. DNS lookup: 20-120 мс

Первый визит требует резолвинга. Решение: короткий TTL с географическим DNS (Route53, Cloudflare DNS), preconnect с других страниц через <link rel="dns-prefetch"> и <link rel="preconnect">.

2. TCP handshake: 30-150 мс

Три пакета туда-обратно (SYN, SYN-ACK, ACK). Ничего не оптимизируем, если только не переключимся на HTTP/3 (см. ниже).

3. TLS handshake: 50-200 мс

TLS 1.3 сократил рукопожатие до одного round-trip, а 0-RTT для повторных подключений до нуля. Убедитесь, что сервер отдаёт TLS 1.3 (проверить: openssl s_client -connect example.com:443 -tls1_3) и включён OCSP stapling.

4. Server think time: 50-2000 мс

Самая большая переменная. Здесь живут SSR-рендер, запросы к БД, вызовы внешних API, шаблонизация. Именно эту часть режет ISR и кэширование.

5. Отдача первого байта: 5-50 мс

Практически константа при HTTP/2 и HTTP/3, страдает только при пиковой нагрузке или нехватке файловых дескрипторов на сервере.

# Смотрим разбивку TTFB через curl (полезно для CI)
curl -w "@-" -o /dev/null -s https://example.com/ <<'EOF'
    DNS lookup:      %{time_namelookup}s
    TCP connect:     %{time_connect}s
    TLS handshake:   %{time_appconnect}s
    Server time:     %{time_starttransfer}s
    Total:           %{time_total}s
    HTTP version:    %{http_version}
EOF

Edge-кэширование и CDN: главный рычаг для снижения TTFB

Как снизить TTFB быстрее всего? Раздавать HTML из ближайшего к пользователю POP’а. Средний RTT от Москвы до Frankfurt равен 30-45 мс; до POP-точки в самой Москве, всего 3-8 мс. Только это уже отсекает 60-80% сетевой части TTFB.

Ключевая механика, это правильные HTTP-заголовки. Cache-Control: s-maxage=60, stale-while-revalidate=3600 говорит CDN хранить ответ 60 секунд как «свежий», а следующий час отдавать как «протухший, но с фоновым обновлением». В браузере пользователь видит мгновенный ответ; на бэкенд запрос идёт максимум раз в 60 секунд с одного POP’а.

// Next.js 15 App Router: настраиваем edge-кэш через Response headers
// app/products/[id]/page.tsx
export const revalidate = 60; // ISR: перегенерация раз в 60 с

// Или через явные заголовки в route handler:
// app/api/catalog/route.ts
export async function GET() {
  const data = await fetchCatalog();
  return Response.json(data, {
    headers: {
      // s-maxage — для CDN, max-age для браузера
      'Cache-Control': 'public, s-maxage=60, stale-while-revalidate=3600',
      'CDN-Cache-Control': 'public, s-maxage=300', // Cloudflare/Fastly
      'Vary': 'Accept-Encoding, Accept-Language',
    },
  });
}

Отдельно про инвалидацию, моё любимое. Классический подход «purge всего кэша» болезнен: CDN холодный, TTFB прыгает до 800 мс, пока не прогреется. Правильно, теговая инвалидация: помечаем ответы Cache-Tag: product-{id}, catalog и точечно вычищаем при событии в БД. Cloudflare Enterprise и Fastly поддерживают это из коробки; в Next.js есть revalidateTag().

ISR и статическая регенерация: как получить TTFB как у статики

ISR (Incremental Static Regeneration), паттерн, который придумали в Vercel для Next.js, а сейчас его копируют Astro (on-demand), Nuxt (routeRules.isr), SvelteKit (prerender.entries) и даже Gatsby. Идея простая: страница генерируется вне запроса пользователя (либо при билде, либо в фоне после первого хита) и лежит на диске или в edge-кэше как статический HTML. TTFB такой страницы равен TTFB статики, обычно 20-50 мс.

// Next.js 15: App Router с ISR
// app/blog/[slug]/page.tsx
import { notFound } from 'next/navigation';

// Генерируем 100 самых популярных постов на билде,
// остальные — по запросу (fallback: 'blocking' поведение)
export async function generateStaticParams() {
  const posts = await fetch('https://cms.example.com/posts?limit=100').then(r => r.json());
  return posts.map((p: { slug: string }) => ({ slug: p.slug }));
}

// Перегенерация раз в 5 минут (300 с)
export const revalidate = 300;
// Разрешаем ISR для параметров, не сгенерированных на билде
export const dynamicParams = true;

export default async function BlogPost({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const post = await fetch(`https://cms.example.com/posts/${slug}`, {
    // Тегируем для точечной инвалидации через revalidateTag
    next: { tags: [`post-${slug}`] },
  }).then(r => r.ok ? r.json() : null);

  if (!post) notFound();
  return <article dangerouslySetInnerHTML={{ __html: post.html }} />;
}

При публикации нового поста в CMS хук вызывает revalidateTag('post-my-slug'), и следующий запрос перерисует страницу. Все промежуточные запросы получают старую версию мгновенно. Никаких водопадов и холодных стартов.

Astro 5.x реализует то же через experimental.i18n и edge middleware; я сравнивал подходы в разборе стратегий кэширования Service Worker. Там речь про клиентский слой, но stale-while-revalidate логика идентична на CDN.

HTTP/3 и QUIC: сокращаем рукопожатие

HTTP/3 работает поверх QUIC (UDP), а не TCP. Ключевое преимущество для TTFB: TLS 1.3 handshake встроен в транспортный уровень. Вместо TCP+TLS = 2 RTT получаем 1 RTT для первого подключения и 0-RTT для повторных. На мобильном 4G с RTT в 80 мс это экономит до 160 мс на первом запросе.

По данным Cloudflare (сентябрь 2026), 34% всего HTTPS-трафика в их сети уже идёт по HTTP/3. Поддержка в браузерах: Chrome 106+, Firefox 88+, Safari 14+. Проверить, что ваш сайт отдаёт HTTP/3:

curl -I --http3 https://example.com/
# Смотрим ответ:
# HTTP/3 200
# alt-svc: h3=":443"; ma=86400

Для включения HTTP/3 на nginx (нужен nginx 1.25+ с --with-http_v3_module):

server {
    listen 443 quic reuseport;
    listen 443 ssl;
    http2 on;

    ssl_certificate /etc/ssl/cert.pem;
    ssl_certificate_key /etc/ssl/key.pem;
    ssl_protocols TLSv1.3;

    # Анонс поддержки HTTP/3 клиенту
    add_header alt-svc 'h3=":443"; ma=86400';

    # 0-RTT (early data), с осторожностью, есть replay attack surface
    ssl_early_data on;
}

103 Early Hints: параллелим TTFB и загрузку ресурсов

103 Early Hints, редкий случай, когда HTTP RFC (см. RFC 8297 «Indicating Hints») решает практическую проблему. Сервер может отправить промежуточный ответ 103 с заголовками Link: preload ещё до готовности основного HTML. Браузер увидит его, начнёт качать CSS/шрифты, и когда придёт финальный 200-ответ, критические ресурсы уже в кэше.

// Express-подобный псевдокод для Node.js бэкенда с ранними подсказками
app.get('/product/:id', async (req, res) => {
  // Отправляем 103 ДО обращения к БД: секунду выигрываем "бесплатно"
  res.writeEarlyHints({
    link: [
      '</css/product.css>; rel=preload; as=style',
      '</fonts/inter.woff2>; rel=preload; as=font; crossorigin',
      '</js/product-hero.js>; rel=preload; as=script',
    ],
  });

  // Медленная часть: обращение к БД, микросервисам и т.д.
  const product = await slowQuery(req.params.id);
  res.send(renderProductPage(product));
});

Поддержка: Node.js 18.11+, Cloudflare, Fastly, Vercel, Fastify (нативно). На платформе Vercel Early Hints автоматически посылаются для preload-ассетов, объявленных в <head>. Детальный разбор комбинации 103 с fetchpriority и preconnect есть в моём гайде по приоритизации загрузки ресурсов.

Настройка бэкенда: база данных, компрессия, keep-alive

Даже с идеальным CDN бэкенд-стрелка стреляет в TTFB каждый раз, когда кэш инвалидируется. Три технических долга, которые я вижу чаще всего.

N+1 в SSR-рендере

SSR рендерит страницу, для каждого элемента списка бьёт в БД отдельным запросом. Профилируйте через ORM-логгер (Prisma log: ['query']) или Server-Timing заголовки. Решение: SELECT ... JOIN или dataloader с батчингом. Я хитанул на этом свежий Next.js-проект в прошлом квартале, TTFB на listing-странице был 1.4 с; после dataloader стал 180 мс.

Отсутствие compression

Brotli на nginx: brotli on; brotli_types text/html application/json; brotli_comp_level 4;. Уровень 4 даёт компромисс между CPU и размером. Динамический HTML жмётся в 4-5 раз, что снижает сетевую часть TTFB и, что важнее, ускоряет транспорт первого чанка при стриминге.

HTTP keep-alive и connection pooling

Между приложением и БД держите пул соединений (pg-pool, mysql2 pool). Каждое новое соединение, это TCP+TLS handshake до БД, легко +50 мс к TTFB. То же для upstream HTTP: agentkeepalive в Node.js. О размерах бандла и последствиях для парсинга я подробнее писал в гайде по оптимизации JavaScript-бандла, там про сторону клиента, но связь с TTFB очевидна: чем меньше HTML+JS, тем короче транспорт.

// Node.js: Server-Timing для дебага серверных этапов TTFB
import { performance } from 'node:perf_hooks';

app.use(async (req, res, next) => {
  const timings: Record<string, number> = {};
  const mark = (name: string) => { timings[name] = performance.now(); };
  const measure = (name: string, start: string) =>
    `${name};dur=${(performance.now() - timings[start]).toFixed(1)}`;

  mark('start');
  res.on('finish', () => {
    // Заголовок Server-Timing виден в DevTools -> Network -> Timing
    res.setHeader('Server-Timing', [
      measure('db', 'dbStart'),
      measure('render', 'renderStart'),
      measure('total', 'start'),
    ].join(', '));
  });
  next();
});

Стриминговый SSR: TTFB против TTFCB

Классический SSR ждёт готовности всей страницы, только потом отдаёт HTML: плохой TTFB. Стриминговый SSR (React 19 с renderToPipeableStream, Next.js App Router с Suspense) отправляет HTML чанками по мере готовности: сначала shell (навигация, footer), потом медленные секции. TTFB становится равным времени отдачи shell, обычно 30-80 мс.

// Next.js 15: стриминг с помощью Suspense
// app/dashboard/page.tsx
import { Suspense } from 'react';
import { SlowChart, SlowTable, HeaderShell } from './components';

export default function Dashboard() {
  return (
    <>
      {/* Отдаётся мгновенно: определяет TTFB */}
      <HeaderShell />
      <Suspense fallback={<SkeletonChart />}>
        <SlowChart /> {/* стримится позже */}
      </Suspense>
      <Suspense fallback={<SkeletonTable />}>
        <SlowTable />
      </Suspense>
    </>
  );
}

Один нюанс, за который меня когда-то ругали ребята из SRE: стриминг ломает Content-Length и требует Transfer-Encoding: chunked. Если перед вашим сервером стоит обратный прокси, он должен поддерживать chunked-передачу (nginx делает это по умолчанию, некоторые enterprise-LB нет).

Для мгновенной навигации между уже-загруженными страницами стоит также включить Speculation Rules API. Подробнее в статье про пререндеринг и мгновенную навигацию с нулевым LCP.

Чек-лист оптимизации TTFB на 2026

  1. Замерьте p75 TTFB через Chrome UX Report BigQuery датасет или RUM с web-vitals.js.
  2. Определите текущий тип страниц (статика / ISR / SSR / персонализированный) и целевое значение из таблицы выше.
  3. Включите HTTP/3 и TLS 1.3 на сервере/CDN. Проверьте alt-svc и curl --http3.
  4. Настройте Cache-Control: s-maxage + stale-while-revalidate для всех кэшируемых ответов.
  5. Внедрите ISR или полную статическую генерацию для маркетинговых и продуктовых страниц.
  6. Добавьте 103 Early Hints для критических ресурсов (CSS, hero-шрифт, hero-скрипт).
  7. Проаудитьте бэкенд: N+1 запросы, отсутствие индексов, connection pooling.
  8. Включите Brotli со сжатием уровня 4 для HTML/JSON.
  9. Переведите SSR-страницы на стриминговый рендер с Suspense-границами.
  10. Настройте теговую инвалидацию (Cache-Tag/revalidateTag) вместо purge-all.
  11. Отправляйте Server-Timing и мониторьте регрессии в CI (Lighthouse CI, WebPageTest scripts).

Часто задаваемые вопросы

Влияет ли TTFB на SEO в 2026 году?

Прямо нет: TTFB не входит в Core Web Vitals и не является отдельным сигналом ранжирования. Косвенно да: высокий TTFB тянет за собой плохой LCP, а LCP уже влияет на ранжирование. Google Search Console показывает TTFB как диагностический параметр к LCP.

Как быстро снизить TTFB, если нет доступа к бэкенду?

Поставьте CDN перед origin (Cloudflare Free, Fastly, Bunny.net), настройте кэширование HTML с коротким s-maxage и stale-while-revalidate, включите Brotli. Это стандартно снижает TTFB в 3-10 раз без изменения кода приложения.

Что важнее для LCP, низкий TTFB или preload изображения?

Оба, но TTFB это фундамент. Пока не пришёл HTML, браузер не знает, что грузить: preload-хинт стартует после парсинга. 103 Early Hints частично решают эту проблему, отправляя preload-ссылки ещё до основного ответа.

Можно ли иметь низкий TTFB на персонализированной странице?

Да, при правильной сегментации кэша. Разделите публичную часть страницы (кэшируемую) и персональную (fetch на клиенте после первичной отрисовки, или через ESI/edge includes на CDN). Vercel и Cloudflare предлагают edge middleware, где персонализация вычисляется за 5-15 мс.

Как отличить проблему в сети от проблемы на сервере?

Сравните time_starttransfer и time_connect в curl. Если между ними больше 100-200 мс, это server think time (медленный бэкенд, БД, шаблонизация). Если много уходит на connect/appconnect, это сеть, DNS или TLS-рукопожатие. Server-Timing заголовок из бэкенда разложит серверную часть по компонентам.

Mateo Silva
Об авторе Mateo Silva

Full-stack performance lead bridging frontend perf with backend latency. Cache invalidation is his love language.