Giảm TTFB 2026: Edge Rendering, ISR, Streaming SSR và HTTP/3

Hướng dẫn thực chiến kéo TTFB từ 1.200ms xuống dưới 200ms bằng edge rendering, ISR, streaming SSR với React 19 và HTTP/3 — kèm code mẫu và benchmark.

Giảm TTFB 2026: Edge, ISR & HTTP/3

Cập nhật: 18 tháng 7, 2026

TTFB (Time to First Byte) là khoảng thời gian tính từ khi trình duyệt gửi HTTP request đến khi nhận được byte đầu tiên của response. Để đạt ngưỡng "Good" trong Core Web Vitals năm 2026, bạn cần giữ TTFB dưới 800ms tại P75. Trong bài này tôi sẽ hướng dẫn cách kết hợp edge rendering, ISR (Incremental Static Regeneration), streaming SSR với React 19 và HTTP/3 để kéo TTFB từ 1.200ms trên VPS truyền thống xuống dưới 200ms trong production. Tất cả dựa trên các dự án thương mại điện tử và blog tôi đã triển khai suốt 12 tháng gần nhất (vài cái là e-com có 40 nghìn SKU, nên số liệu là thật, không phải demo).

  • Google khuyến nghị TTFB ≤ 800ms ở phân vị 75; TTFB tệ trực tiếp kéo LCP và INP theo.
  • Edge rendering (Cloudflare Workers, Vercel Edge Functions, Deno Deploy) giảm round-trip xuống 20–50ms nhờ chạy code cách user vài trăm km thay vì vài nghìn.
  • ISR biến trang động thành HTML tĩnh có cache dài, revalidate nền. Lý tưởng cho e-commerce hàng chục nghìn SKU.
  • Streaming SSR với React 19 và renderToPipeableStream gửi shell HTML đầu tiên trong <100ms, không phải chờ query DB xong.
  • HTTP/3 (QUIC) loại bỏ head-of-line blocking ở tầng TCP, cải thiện TTFB 10–30% trên mạng di động 4G.
  • 103 Early Hints cho phép trình duyệt preload CSS/font/JS trước khi backend trả xong HTML cuối cùng.

TTFB là gì và ngưỡng "tốt" là bao nhiêu?

TTFB đo thời gian từ lúc trình duyệt bắt đầu gửi request đến lúc nhận được byte đầu tiên của HTTP response. Nó bao gồm bốn giai đoạn: DNS lookup, TCP + TLS handshake, thời gian request đi tới server, và thời gian server xử lý (bao gồm query database, render template, gọi API upstream). Theo chỉ dẫn TTFB chính thức của web.dev, ngưỡng cho một website "Good" là dưới 800ms ở phân vị 75, còn "Needs improvement" là 800–1.800ms.

Điều nhiều team bỏ qua: TTFB không phải một chỉ số Core Web Vitals độc lập, nhưng nó là trần cho LCP. Nếu server trả byte đầu tiên sau 900ms, không cách nào LCP dưới 2,5s trên mạng 3G Fast được. Ở phía backend, tôi luôn ép budget TTFB ≤ 400ms tại P75 để dành 2,1 giây còn lại cho download HTML, CSS, ảnh LCP và paint. Đó là cách duy nhất bảo vệ LCP khỏi hạ tầng chậm ở tail.

Nói ngắn gọn: TTFB là chỉ số duy nhất bạn không thể "tối ưu" bằng thủ thuật frontend. Nó phản ánh chất lượng backend, chọn CDN, và mạng lưới edge của bạn. Trước khi tối ưu bất cứ thứ gì ở client, hãy nhìn thẳng vào con số này trong Chrome UX Report (CrUX) của trang.

Vì sao TTFB của bạn đang cao? 6 thủ phạm phổ biến

Trong 30+ audit tôi chạy năm 2025–2026, hầu hết trang có TTFB > 1s do một trong sáu vấn đề dưới đây (thường là kết hợp vài thứ cùng lúc):

  1. Origin đặt xa user. Web đặt server duy nhất ở US-East nhưng user chủ yếu ở Việt Nam. Round-trip đã 220ms trước khi server làm gì.
  2. Không có CDN cache, hoặc cache miss. HTML trả về mỗi lần đều "chạm" origin, chưa kể query DB.
  3. N+1 query trên trang chi tiết. Trang sản phẩm gọi 40+ query nhỏ vì thiếu preload/eager loading.
  4. SSR block trên upstream chậm. Frontend render đồng bộ đợi API bên thứ ba trả (recommendation engine, exchange rate...) và mất 600–1.200ms.
  5. Không dùng HTTP/3. Trên mạng di động không ổn định, TCP retransmission "khóa" cả stream.
  6. Server nén Brotli mức cao ở CPU chính. Nén ở mức 11 tốn 100–300ms per response, đặc biệt trên VPS 2 vCPU.

Edge rendering: Chạy code gần user để cắt latency

Edge rendering nghĩa là chạy code render (thường là JavaScript hoặc WASM) trong hàng trăm PoP CDN toàn cầu thay vì một origin duy nhất. Cloudflare Workers có >330 city, Vercel Edge Functions dùng hạ tầng Cloudflare, Deno Deploy chạy V8 isolates ở 35+ region. Với user Việt Nam, edge Singapore/Hong Kong cách ~40ms, thay vì US-East ~220ms.

Ví dụ triển khai Cloudflare Worker phục vụ HTML với cache hit tại edge:

// worker.ts — Cloudflare Worker chạy tại edge
export default {
  async fetch(request: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    const url = new URL(request.url);
    const cacheKey = new Request(url.toString(), request);
    const cache = caches.default;

    // Bước 1: Thử cache edge trước
    let response = await cache.match(cacheKey);
    if (response) {
      // Cache hit: TTFB ~15-30ms tại Singapore
      return response;
    }

    // Bước 2: Cache miss, gọi origin
    response = await fetch(`https://origin.example.com${url.pathname}`, {
      cf: { cacheEverything: true, cacheTtl: 300 }
    });

    // Bước 3: Đưa vào cache edge (không block response cho user)
    response = new Response(response.body, response);
    response.headers.set('Cache-Control', 's-maxage=300, stale-while-revalidate=86400');
    ctx.waitUntil(cache.put(cacheKey, response.clone()));

    return response;
  }
};

Kỹ thuật stale-while-revalidate ở đây rất quan trọng. Nếu cache TTL đã hết, edge vẫn trả bản cache cũ ngay lập tức (TTFB ~20ms) và revalidate ngầm ở background. Chi tiết cấu hình cache HTTP tôi đã giải thích kỹ trong bài chiến lược caching web 2026 với Cache-Control và Service Worker.

ISR (Incremental Static Regeneration) hoạt động thế nào?

ISR do Next.js giới thiệu, nay đã có mặt trong Astro, Nuxt và Remix. Nó kết hợp ưu điểm SSG (HTML tĩnh, TTFB <100ms) với ưu điểm SSR (nội dung luôn cập nhật). Cơ chế thế này: trang được build tĩnh lần đầu, cache trên CDN; sau khoảng thời gian TTL, request tiếp theo trigger revalidation ở background, người dùng nhận bản cũ ngay lập tức và bản mới thay thế cho request kế tiếp.

Ví dụ Next.js 15 App Router với ISR bằng revalidate:

// app/product/[slug]/page.tsx
export const revalidate = 60; // Revalidate sau 60s

export async function generateStaticParams() {
  const products = await db.product.findMany({ take: 1000, select: { slug: true } });
  return products.map((p) => ({ slug: p.slug }));
}

export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
  const { slug } = await params;
  const product = await db.product.findUnique({ where: { slug } });
  if (!product) return notFound();

  return (
    <article>
      <h1>{product.name}</h1>
      <p>{product.description}</p>
    </article>
  );
}

Với on-demand revalidation, bạn có thể invalidate cache ngay khi CMS cập nhật, không phải chờ 60s TTL:

// app/api/revalidate/route.ts
import { revalidatePath } from 'next/cache';

export async function POST(request: Request) {
  const { slug, secret } = await request.json();
  if (secret !== process.env.REVALIDATE_SECRET) {
    return new Response('Unauthorized', { status: 401 });
  }
  revalidatePath(`/product/${slug}`);
  return Response.json({ revalidated: true });
}

Streaming SSR với React 19 và Suspense

Trong SSR truyền thống, server phải đợi toàn bộ query xong mới bắt đầu gửi HTML. Nếu API recommendation mất 800ms, TTFB không thể tốt hơn 800ms. Streaming SSR đảo ngược điều này: server gửi ngay phần shell (header, navigation, layout) trong <100ms, sau đó stream tiếp các phần Suspense boundary khi dữ liệu sẵn sàng.

React 19 tối ưu hóa renderToPipeableStream cho pattern này, và Next.js App Router bật streaming mặc định khi bạn dùng loading.tsx. Bản chất về mặt HTML thô:

// Server (Node.js với React 19)
import { renderToPipeableStream } from 'react-dom/server';
import { Suspense } from 'react';

app.get('/product/:slug', (req, res) => {
  const stream = renderToPipeableStream(
    <html>
      <body>
        <Header />
        <Suspense fallback={<ProductSkeleton />}>
          <ProductDetails slug={req.params.slug} /> {/* fetch trong đây */}
        </Suspense>
        <Suspense fallback={<RecsSkeleton />}>
          <Recommendations slug={req.params.slug} /> {/* fetch chậm hơn */}
        </Suspense>
      </body>
    </html>,
    {
      onShellReady() {
        // Đây là khoảnh khắc TTFB, shell HTML sẵn sàng
        res.setHeader('Content-Type', 'text/html');
        stream.pipe(res);
      },
    }
  );
});

Kỹ thuật này tách "TTFB backend" ra khỏi "thời gian tải nội dung động". Thú thật, tôi hơi ngại pattern Suspense khi mới thử. Nhưng trong dự án gần đây, tôi đưa TTFB từ 1.100ms xuống 140ms bằng cách bọc mọi upstream API chậm trong Suspense. Người dùng nhìn thấy skeleton ngay, còn nội dung stream về sau. Tài liệu Next.js về streaming hướng dẫn chi tiết pattern loading.tsx. Lợi ích phụ: LCP thường cải thiện 30–40% vì browser nhận HTML sớm để bắt đầu preload ảnh LCP.

HTTP/3 và QUIC giảm TTFB được bao nhiêu?

HTTP/3 chạy trên giao thức QUIC (UDP-based) thay vì TCP. Điều này giải quyết bốn vấn đề TCP có từ 40 năm: head-of-line blocking ở tầng transport, connection setup 3-way handshake, mất gói làm khóa cả stream, và không migrate được kết nối khi mạng thay đổi (Wi-Fi sang 4G).

Theo benchmark của Cloudflare, HTTP/3 giảm TTFB 10–35% trên mạng có packet loss >2% (điển hình mạng di động). Trên Wi-Fi tốt, lợi ích chủ yếu đến từ 0-RTT resumption. Client kết nối lại chỉ với 0 round-trip nếu đã trao đổi khóa trước đó.

Bật HTTP/3 trên các stack phổ biến:

  • Cloudflare: Bật mặc định cho mọi domain, không cần cấu hình gì thêm.
  • Nginx 1.25+: Cần build với --with-http_v3_module, thêm listen 443 quic reuseport;add_header Alt-Svc 'h3=":443"; ma=86400';.
  • Caddy 2.6+: Bật mặc định khi có TLS.
  • Vercel/Netlify: Mặc định trên tất cả deployment.

Kiểm tra bằng curl --http3 -I https://your-site.com hoặc DevTools → Network → cột Protocol phải hiển thị h3.

103 Early Hints: Preload trước khi HTML sẵn sàng

103 Early Hints là status code HTTP cho phép server gửi header phản hồi (đặc biệt là Link: rel=preload) trước khi hoàn thành xử lý request. Trình duyệt có thể bắt đầu tải CSS, font, ảnh LCP song song trong khi backend vẫn đang query DB.

Ví dụ triển khai trong Fastify:

fastify.get('/', async (request, reply) => {
  // Gửi Early Hints ngay lập tức (TTFB ảo ~20ms)
  reply.raw.writeHead(103, {
    'Link': '</css/critical.css>; rel=preload; as=style, ' +
            '</fonts/inter.woff2>; rel=preload; as=font; crossorigin, ' +
            '</img/hero.avif>; rel=preload; as=image; fetchpriority=high'
  });

  // Backend vẫn tiếp tục xử lý (query DB, render...)
  const data = await slowDatabaseQuery();
  return reply.type('text/html').send(renderPage(data));
});

Kết quả thực tế: LCP cải thiện 200–400ms vì browser đã bắt đầu tải ảnh hero trong khi backend còn đang xử lý. Về cách kết hợp Early Hints với fetchpriority và Speculation Rules, tôi đã viết trong bài tối ưu LCP 2026 với fetchpriority và Early Hints.

Lưu ý tương thích: Chrome/Edge hỗ trợ đầy đủ, Firefox từ 120, Safari 17.2+. Cloudflare tự động chuyển đổi Link header thành 103 Early Hints từ 2023. Chỉ cần bật ở Speed → Optimization là xong.

Đo TTFB đúng cách với Server-Timing và RUM

Bạn không thể tối ưu điều bạn không đo. Lighthouse cho TTFB một điểm dữ liệu duy nhất, nhưng nó không nói cho bạn phần nào đang chậm. Header Server-Timing giải quyết vấn đề đó, cho phép backend gửi metrics cho browser, hiển thị trong DevTools và có thể thu thập bằng RUM.

// Node.js — thêm Server-Timing header
app.get('/product/:slug', async (req, res) => {
  const t0 = performance.now();
  const product = await db.product.findUnique({ where: { slug: req.params.slug } });
  const t1 = performance.now();

  const recs = await recommendationApi.get(product.id);
  const t2 = performance.now();

  const html = renderPage(product, recs);
  const t3 = performance.now();

  res.setHeader('Server-Timing',
    `db;dur=${(t1 - t0).toFixed(0)}, ` +
    `recs;dur=${(t2 - t1).toFixed(0)}, ` +
    `render;dur=${(t3 - t2).toFixed(0)}`
  );
  res.send(html);
});

Kết hợp với RUM ở client bằng PerformanceObserver:

new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.entryType === 'navigation') {
      const ttfb = entry.responseStart - entry.requestStart;
      const timings = entry.serverTiming || [];

      navigator.sendBeacon('/rum', JSON.stringify({
        ttfb,
        serverTimings: timings.map(t => ({ name: t.name, dur: t.duration })),
        url: location.pathname,
      }));
    }
  }
}).observe({ type: 'navigation', buffered: true });

Với dữ liệu này, bạn có thể trả lời câu hỏi "TTFB p95 tăng vì lớp DB hay lớp render?" thay vì đoán mò. Về debug INP đi kèm với backend, tham khảo thêm bài Long Animation Frames API để debug INP.

Brotli vs Zstd: Nén ở edge cho response nhỏ hơn

TTFB bao gồm cả thời gian truyền bytes đầu tiên qua mạng. Response nhỏ hơn = TTFB thực tế tốt hơn, đặc biệt trên mạng chậm. Brotli đã là chuẩn từ 2016, nhưng Zstandard (Zstd), được MDN ghi nhận trong Content-Encoding, được Chrome hỗ trợ từ v123 và trở nên phổ biến năm 2025–2026.

Tiêu chíBrotli (level 11)Brotli (level 5)Zstd (level 3)
Tỷ lệ nén HTML điển hình~78%~72%~74%
Thời gian nén 100KB HTML~180ms~8ms~3ms
Phù hợp choStatic asset build-timeSSR dynamicSSR dynamic
Hỗ trợ ChromeTừ 2016Từ 2016Từ v123 (2024)
Hỗ trợ SafariĐầy đủĐầy đủTừ 17.4
CPU costCaoThấpRất thấp

Chiến lược tôi áp dụng: Brotli mức 11 lúc build cho CSS/JS/HTML tĩnh (Nginx dùng module ngx_brotli với brotli_static on;), và Zstd/Brotli mức 4–5 cho response SSR động. Không bao giờ nén Brotli 11 ở runtime, vì CPU sẽ ăn hết TTFB budget.

Câu hỏi thường gặp

TTFB bao nhiêu là tốt trong Core Web Vitals 2026?

Google định nghĩa TTFB "Good" khi dưới 800ms tại phân vị 75, "Needs improvement" 800–1.800ms, và "Poor" trên 1.800ms. Trên thực tế, để LCP đạt "Good" (2,5s), bạn nên đặt budget TTFB dưới 400ms cho các trang quan trọng.

ISR có tốt hơn SSR không?

ISR tốt hơn cho nội dung có thể chia sẻ giữa nhiều user (blog, catalog, landing page) vì trả HTML tĩnh từ CDN với TTFB <100ms. SSR cần thiết cho nội dung personalized (dashboard, giỏ hàng) hoặc dữ liệu phải luôn tươi tức thì. Nhiều team dùng cả hai: ISR cho shell, streaming SSR cho phần động.

HTTP/3 giảm TTFB được bao nhiêu trên mạng di động?

Trên mạng 4G với packet loss 2–5%, HTTP/3 giảm TTFB khoảng 15–30% so với HTTP/2 nhờ loại bỏ head-of-line blocking. Trên Wi-Fi ổn định lợi ích nhỏ hơn (5–10%). Trên kết nối lặp lại (0-RTT resumption), TTFB có thể giảm 100–200ms so với TCP + TLS 1.3.

Làm thế nào để đo TTFB thực tế từ user thay vì lab?

Dùng Chrome UX Report (CrUX) qua PageSpeed Insights hoặc BigQuery public dataset cho dữ liệu 28 ngày ở phân vị 75. Bổ sung RUM riêng bằng PerformanceObserver với type: 'navigation'entry.responseStart - entry.requestStart, gửi về endpoint phân tích qua navigator.sendBeacon.

Streaming SSR có ảnh hưởng đến SEO không?

Không, ngược lại là tốt cho SEO. Googlebot xử lý HTML streaming bình thường và nội dung Suspense-boundary được render trong lần index đầu tiên. Điều kiện: các dữ liệu quan trọng cho SEO (title, meta, structured data, nội dung chính) nên ở trong shell HTML, không đặt trong Suspense muộn.

Mateo Silva
Về Tác Giả Mateo Silva

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