TCP slow start và giới hạn 14KB mà mọi frontend dev nên biết
Server xử lý xong request trong 5ms. HTML response nhỏ thôi, khoảng 40KB. Browser nhận được toàn bộ sau 120ms. Nhưng nếu tối ưu đúng chỗ, browser có thể bắt đầu render sau đúng 1 RTT — dù response chưa về hết.
Sự khác biệt nằm ở cách TCP hoạt động lúc mới bắt đầu kết nối, và tại sao 14KB lại là con số có ý nghĩa thực tế với web performance.
TCP không gửi toàn bộ data ngay lập tức
Khi một TCP connection mới được thiết lập, cả hai bên không biết gì về bandwidth và tình trạng tắc nghẽn của mạng phía kia. Nếu server cứ tống thẳng toàn bộ dữ liệu ra thì với mạng yếu hoặc router đang tải nặng, packet sẽ bị drop, phải retransmit, latency tăng vọt.
TCP giải quyết bằng cơ chế gọi là slow start: bắt đầu gửi ít, từng bước tăng dần, dò xem mạng chịu được bao nhiêu.
Cơ chế hoạt động theo đơn vị là congestion window (cwnd) — số lượng packet server được phép gửi đi mà không cần đợi ACK từ client.
Slow start từng bước
Ban đầu, cwnd = 10 segments (mỗi segment thường là 1460 bytes với MTU 1500 byte Ethernet).
Tức là lần đầu server gửi được:
10 segments × 1460 bytes ≈ 14.2 KBĐây là con số 14KB nổi tiếng. Đây không phải giới hạn cứng của bất kỳ spec nào, mà là giá trị khởi đầu cwnd theo RFC 6928 (trước đó là 3–4 segments theo RFC cũ hơn).
Mỗi RTT (round-trip time) mà không có packet loss, cwnd tăng gấp đôi:
RTT 1: 10 segments → ~14 KB
RTT 2: 20 segments → ~28 KB
RTT 3: 40 segments → ~56 KB
RTT 4: 80 segments → ~112 KB
...Quá trình này tiếp tục cho đến khi gặp packet loss (mạng báo quá tải) hoặc đạt đến ssthresh (slow start threshold), lúc đó chuyển sang congestion avoidance — tăng tuyến tính thay vì nhân đôi.
Slow start: cwnd tăng gấp đôi mỗi RTT
Congestion avoidance: cwnd tăng 1 segment mỗi RTTVì sao 14KB lại quan trọng với web
Với RTT điển hình Việt Nam tới server Singapore khoảng 20–40ms, mỗi round-trip là thứ bạn không lấy lại được.
Nếu HTML response của bạn là 60KB:
- RTT 1: client nhận 14KB
- RTT 2: client nhận thêm ~28KB (cộng dồn ~42KB)
- RTT 3: client nhận phần còn lại
Browser không thể bắt đầu parse <head> đầy đủ cho đến khi phần đó về. Nếu <link rel="stylesheet"> nằm ở byte 15.000 trở đi, browser cần đợi thêm ít nhất 1 RTT mới biết có stylesheet để fetch.
Ngược lại, nếu toàn bộ <head> và phần đầu <body> nằm gọn trong 14KB đầu tiên, browser parse xong phần quan trọng nhất trong ngay RTT đầu — và có thể bắt đầu fetch CSS, font, preload hints song song trong khi phần còn lại của response đang truyền.
Tác động thực tế lên Time to First Byte và LCP
TTFB (Time to First Byte) chỉ đo thời gian đến byte đầu tiên. Nó không nói gì về bao lâu browser mới nhận đủ để làm việc.
LCP phụ thuộc vào thời điểm browser có thể bắt đầu render element lớn nhất. Nếu LCP element là ảnh hero, browser cần:
- Parse
<head>để tìm thấy ảnh hoặc preload hint - Fetch ảnh
- Render
Mỗi bước trên đều chờ nhau. Nếu step 1 bị delay vì HTML chưa về đủ, toàn bộ chain bị kéo dài.
Ví dụ so sánh
Hai site cùng server response time là 5ms, cùng RTT 30ms:
Site A — HTML 60KB, CSS là tag đầu tiên trong <head>:
t=0ms: Browser gửi request
t=30ms: Server nhận request
t=35ms: Server gửi response (xong trong 5ms)
t=65ms: Browser nhận 14KB đầu tiên
→ parse `<head>`, thấy <link rel="stylesheet"> → gửi request CSS
t=95ms: Browser nhận thêm 28KB
→ (HTML vẫn đang về, CSS request đang chờ)
t=125ms: HTML về đủ
t=155ms: CSS về
→ Browser bắt đầu renderSite B — HTML 12KB (chỉ <head> + above-the-fold), inlined critical CSS:
t=0ms: Browser gửi request
t=30ms: Server nhận request
t=35ms: Server gửi response
t=65ms: Browser nhận TOÀN BỘ HTML (12KB < 14KB)
→ parse xong, tìm thấy tất cả preload hints
→ gửi request font, JS
→ render above-the-fold ngay với critical CSS inline
t=95ms: Render đầu tiên xongKhông có gì thay đổi về server, bandwidth hay RTT. Chỉ khác cách đóng gói nội dung trong response đầu tiên.
Các kỹ thuật tận dụng 14KB đầu
1. Inline critical CSS
Thay vì dùng <link rel="stylesheet"> block rendering trong <head>, extract CSS cho above-the-fold và inline thẳng vào HTML:
<head>
<style>
/* critical CSS: layout, above-the-fold typography, LCP element */
body { margin: 0; font-family: system-ui; }
.hero { ... }
h1 { ... }
</style>
<link rel="preload" href="/css/full.css" as="style">
<link rel="stylesheet" href="/css/full.css" media="print" onload="this.media='all'">
</head>Critical CSS thường 3–8KB. Inline thẳng vào <head>, không cần round-trip riêng.
2. <link rel="preload"> trong 14KB đầu
Preload hint có tác dụng khi nằm trong phần HTML mà browser đã nhận và parse:
<head>
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/images/hero.webp" as="image">
</head>Nếu những dòng này nằm trong 14KB đầu, browser biết cần fetch font và ảnh hero ngay từ RTT đầu. Nếu nằm sau byte 14.336, chúng phải đợi thêm ít nhất một RTT mới được xử lý.
3. Giữ <head> gọn
Mỗi <meta> thừa, comment HTML, whitespace không cần thiết, attribute dài đều chiếm byte trong 14KB đầu. Ở scale nhỏ không đáng kể, nhưng nếu <head> của bạn đã 10KB thì còn rất ít chỗ cho nội dung quan trọng.
Minification HTML không phải chỉ để tiết kiệm bandwidth. Nó còn giúp nhồi thêm nội dung có ý nghĩa vào packet đầu tiên.
4. Server-side rendering cho above-the-fold
SPA với empty <div id="root"> và bundle JS 200KB không tận dụng được 14KB đầu. Browser nhận HTML, thấy không có gì để render, đợi JS về, hydrate, rồi mới render.
SSR đặt HTML thật sự vào response. Nếu above-the-fold HTML + critical CSS nằm gọn trong 14KB, user thấy nội dung trước khi JS về.
5. HTTP headers chiếm băng thông
Một phần thường bị quên: HTTP response headers cũng chiếm byte trong packet đầu tiên.
Headers điển hình:
HTTP/2 200 OK
content-type: text/html; charset=utf-8
content-length: 12043
cache-control: max-age=0, must-revalidate
set-cookie: session=abc123; Path=/; HttpOnly; Secure; SameSite=Lax
x-request-id: 7f3a2b1c-...
server: nginx/1.25.3Headers có thể dễ dàng chiếm 500–2000 bytes. Với HTTP/2 HPACK compression thì ít hơn nhiều sau request đầu tiên, nhưng vẫn là chi phí thực tế cho connection mới.
Cookie đặc biệt tệ: một cookie session lớn có thể chiếm cả KB trong mỗi request header.
Kết nối đã có, slow start không áp dụng lại từ đầu
Slow start chỉ bắt đầu từ đầu với new TCP connection. HTTP/1.1 với keep-alive và HTTP/2 reuse connection — nghĩa là request thứ hai trên cùng kết nối không phải slow start lại từ cwnd = 10.
Vì vậy:
- HTTP/2 giảm thiểu tác động bằng multiplexing nhiều request trên 1 connection
- Connection coalescing giúp tái dùng connection qua nhiều subdomain cùng IP
- Preconnect (
<link rel="preconnect">) warm-up connection trước, handshake xong trước khi cần fetch
Nhưng connection đầu tiên của user với domain bạn — luôn luôn là slow start từ đầu. Và đó là connection quyết định LCP.
QUIC và HTTP/3: slow start vẫn còn
Một câu hỏi hợp lý: HTTP/3 dùng QUIC thay TCP, có thoát khỏi slow start không?
Không. QUIC vẫn implement congestion control tương tự, với initial congestion window tương đương. Cải thiện chính của QUIC là ở latency khi có packet loss (không block toàn bộ stream như TCP HOL blocking) và 0-RTT resumption cho known connections. Nhưng slow start trên connection mới vẫn tồn tại.
Đo lường thực tế
Bạn không cần đoán HTML của mình nằm trong bao nhiêu RTT. Chrome DevTools → Network → chọn request HTML chính → xem Timing tab:
- Stalled: thời gian chờ queue
- TTFB: thời gian đến byte đầu tiên (server processing + 1 RTT)
- Content Download: thời gian nhận toàn bộ body
Nếu Content Download chiếm nhiều RTT hơn cần thiết, HTML của bạn đang spread qua nhiều packet.
Công cụ hữu ích hơn: webpagetest.org → Connection View → thấy từng packet, từng RTT, từng round-trip cho response chính.
Checklist kiểm tra nhanh
- HTML response < 14KB? Nếu không,
<head>có nằm trong 14KB đầu không? - Critical CSS đã inline chưa?
- Preload hints cho font, ảnh hero có nằm trong 14KB đầu không?
<head>đã minified chưa?- Cookie session có quá lớn không?
- HTTP response headers có headers thừa không cần thiết không?
- Đã dùng HTTP/2 chưa (giảm overhead handshake)?
Con số cần nhớ
| Giá trị | Ý nghĩa |
|---|---|
| 10 segments | Initial cwnd theo RFC 6928 |
| ~14 KB | Dữ liệu server gửi được trong RTT đầu tiên |
| 1 RTT | Thời gian tối thiểu để browser nhận và bắt đầu parse |
| 2× | cwnd tăng gấp đôi mỗi RTT trong slow start |
| ~112 KB | cwnd sau 4 RTT không có packet loss |
Không có gì ma thuật ở con số 14KB. Nó chỉ là sản phẩm của default MTU Ethernet và quyết định trong RFC. Nhưng hiểu nó giúp bạn biết tại sao critical path matter, tại sao bytes đầu tiên của response có giá trị hơn bytes cuối, và tại sao một số kỹ thuật performance tưởng vô nghĩa lại cải thiện LCP rõ ràng trong thực tế.