Đây là nơi nén dữ liệu chạm vào công việc của mọi lập trình viên web mỗi ngày, dù bạn có để ý hay không. Mỗi lần trình duyệt tải một trang, nó và server thực hiện một cuộc thương lượng nhỏ trong âm thầm: "Tôi hiểu gzip, brotli — bạn có nén được không?" — "Có, đây là bản gzip." Kết quả là một trang HTML 250KB có thể đi qua mạng chỉ với 25KB, tải nhanh gấp mười lần, và người dùng không thấy gì khác — trình duyệt tự giải nén trước khi hiển thị. Bài này (phần 10 loạt Nén) đo thật cuộc thương lượng đó trên chính coffeecode.vn, so gzip với brotli, và chỉ ra những cấu hình mà lập trình viên phải làm đúng.
Cơ chế thương lượng: client nói, server chọn
HTTP dùng hai header để thương lượng nén:
- Request →
Accept-Encoding: client (trình duyệt, hoặccurl) liệt kê các kiểu nén nó giải được, ví dụAccept-Encoding: gzip, br, zstd. - Response →
Content-Encoding: server chọn một kiểu, nén body, và ghi kiểu đã dùng, ví dụContent-Encoding: gzip.
Body trên đường truyền là đã nén; trình duyệt đọc Content-Encoding để biết cách giải nén trước khi render. Nếu client không gửi Accept-Encoding (hoặc gửi identity), server trả nội dung không nén. Và có một header thứ ba bắt buộc: Vary: Accept-Encoding — báo cho cache/CDN rằng response thay đổi theo Accept-Encoding, nên phải lưu riêng bản nén và bản không nén.

Hình 1: Thương lượng nén HTTP — client gửi Accept-Encoding (kiểu nó giải được), server chọn một và trả Content-Encoding + body đã nén; Vary: Accept-Encoding bắt buộc để cache lưu đúng; và chỉ nén text (html/css/js/json), không nén ảnh đã nén.
Đo thật: coffeecode.vn nhỏ đi 9.8 lần
Mình dùng curl gọi thẳng coffeecode.vn với các Accept-Encoding khác nhau, xem header và đo kích thước tải về:

Hình 2: Chạy thật — coffeecode.vn trả Content-Encoding: gzip + Vary: Accept-Encoding; identity tải 250.841 byte, gzip chỉ 25.588 byte (9.8x nhỏ hơn), br trả 250.841 (server chưa hỗ trợ brotli); nén cục bộ chính HTML đó: gzip -9 21.688B (11.6x), brotli -11 17.474B (14.4x — nhỏ hơn gzip ~24%).
- gzip giảm 9.8 lần trên đường truyền — con số thật: trang chủ coffeecode.vn không nén là 250.841 byte; với
Accept-Encoding: gzip, chỉ 25.588 byte đi qua mạng — nhỏ hơn 9.8 lần. Với người dùng, đây là chênh lệch giữa tải trong 1 giây và 10 giây trên mạng chậm. Server cũng đặtVary: Accept-Encodingđúng chuẩn. - Server chưa hỗ trợ brotli — báo trung thực: khi mình chỉ gửi
Accept-Encoding: br, server trả về không nén (250.841 byte) — nghĩa là nó chưa bật brotli, chỉ có gzip. Điều này hoàn toàn ổn (gzip tương thích rộng nhất), nhưng cho thấy hỗ trợ nén là tùy cấu hình server. - brotli nén web text tốt hơn gzip ~24%: để so, mình nén chính file HTML đó cục bộ. gzip -9 cho 21.688 byte (11.6x), nhưng brotli -11 cho 17.474 byte (14.4x) — nhỏ hơn gzip ~24%. Brotli (Google, 2015) được thiết kế riêng cho web text: nó có một từ điển tĩnh dựng sẵn chứa các chuỗi HTML/CSS/JS phổ biến (như dictionary ở phần 8, nhưng cố định và chung cho cả web). Đây là lý do hầu hết CDN và trình duyệt hiện đại ưu tiên brotli cho asset tĩnh.
Đánh đổi cần cân nhắc
Thiếu Vary: Accept-Encoding là bug cache kinh điển và nguy hiểm. Nếu server nén nhưng quên Vary, một CDN/proxy có thể cache bản gzip rồi phục vụ nó cho một client không gửi Accept-Encoding: gzip — client nhận một mớ byte gzip mà không biết giải, trang hỏng hoàn toàn. Hoặc ngược lại. Đây là lỗi rất khó lần vì nó chỉ xuất hiện sau khi cache, với một số client. Luôn đặt Vary: Accept-Encoding khi bật nén động.
Chỉ nén text — cấu hình gzip_types/compressible types cho đúng. Như phần 9 đã đo, nén ảnh/video/font đã nén (.jpg, .png, .mp4, .woff2) là vô ích và tốn CPU. Server nên chỉ nén các kiểu text: text/html, text/css, application/javascript, application/json, image/svg+xml... nginx dùng gzip_types, các framework có danh sách tương tự. Bật nén cho mọi kiểu là lãng phí CPU trên chính những response không nén được.
Nén động vs nén sẵn (pre-compression). Nén động (server nén mỗi response khi có request) tốn CPU mỗi lần và thường dùng mức thấp để nhanh — hợp nội dung động. Nhưng với asset tĩnh (JS/CSS bundle), tốt hơn là nén sẵn một lần ở mức cao nhất (gzip -9, brotli -11, thậm chí zopfli) khi build, rồi server chỉ việc gửi file .gz/.br có sẵn. Đây đúng là "nén một lần đọc nhiều lần" của phần 6 — asset tĩnh xứng đáng mức nén cao vì chi phí nén trả một lần lúc build.
Ba ý mang về
- HTTP thương lượng nén qua Accept-Encoding / Content-Encoding: đo thật coffeecode.vn trả
Content-Encoding: gzip, và trang chủ 250.841 byte tải về chỉ 25.588 byte (9.8x nhỏ hơn) — nén diễn ra tự động, người dùng chỉ thấy trang tải nhanh hơn. - brotli nén web text tốt hơn gzip: đo thật trên cùng HTML, brotli -11 cho 17.474B (14.4x) vs gzip -9 21.688B (11.6x) — nhỏ hơn ~24% nhờ từ điển tĩnh dựng sẵn cho web; nhiều CDN ưu tiên brotli, nhưng hỗ trợ tùy cấu hình server (coffeecode.vn hiện chỉ có gzip).
- Cấu hình đúng: Vary + chỉ nén text:
Vary: Accept-Encodingbắt buộc để cache không phục vụ nhầm bản nén; chỉ nén text (html/css/js/json), bỏ qua ảnh/video đã nén; và nén sẵn asset tĩnh ở mức cao khi build (nén một lần đọc nhiều lần).
Nguồn
- MDN — Content-Encoding, Accept-Encoding, Vary: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Encoding
- Google — Brotli Compression: https://github.com/google/brotli
- nginx docs — ngx_http_gzip_module: https://nginx.org/en/docs/http/ngx_http_gzip_module.html
Phần sau ta chuyển từ dòng lệnh sang code: nén trong Go — dùng compress/flate và compress/gzip, chọn mức nén, và nén theo luồng (streaming) cho dữ liệu lớn mà không nạp hết vào bộ nhớ.