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):
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ì.
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.
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.
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.
Không dùng HTTP/3. Trên mạng di động không ổn định, TCP retransmission "khóa" cả stream.
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:
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; và 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.
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 cho
Static asset build-time
SSR dynamic
SSR dynamic
Hỗ trợ Chrome
Từ 2016
Từ 2016
Từ v123 (2024)
Hỗ trợ Safari
Đầy đủ
Đầy đủ
Từ 17.4
CPU cost
Cao
Thấp
Rấ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' và 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.
Long Animation Frames API (LoAF) là công cụ mạnh nhất để debug INP trong sản xuất. Hướng dẫn đầy đủ về script attribution, sửa third-party CORS, và gate LoAF trong CI Playwright để giữ INP dưới 200ms tại P75 năm 2026.
Hướng dẫn sửa CLS năm 2026: đo bằng web-vitals RUM với attribution, dùng size-adjust triệt tiêu font swap shift, khóa kích thước ảnh/iframe/ads, và debug thực chiến trong Next.js 15 App Router.
Hướng dẫn 2026 giảm LCP xuống dưới 2.5s với fetchpriority, preload, Early Hints HTTP 103 và Speculation Rules API. Kèm code Next.js 15, lệnh DevTools và số đo thực tế trên Chrome 136.