Đâ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ặc curl) 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.

Ảnh chụp đoạn mã nền tối minh hoạ nén trong HTTP cách trình duyệt và server thương lượng Accept-Encoding Content-Encoding Vary gzip vs brotli, thương lượng nén client nói tôi hiểu gì server chọn client trình duyệt gửi trong request Accept-Encoding gzip br zstd các kiểu nén tôi giải được server chọn một kiểu nén body trả về Content-Encoding gzip kiểu đã dùng để nén body Vary Accept-Encoding báo cache response đổi theo header này body trên đường truyền là gzip trình duyệt tự giải trước khi render, đo bằng curl có và không Accept-Encoding curl size_download với identity với gzip curl -D header grep content-encoding so size_download 2 lần biết nén giúp giảm bao nhiêu byte trên mạng, vì sao Vary Accept-Encoding là bắt buộc nếu thiếu Vary CDN proxy có thể cache bản gzip rồi trả cho client không hiểu gzip nội dung hỏng Vary báo cache lưu riêng theo Accept-Encoding thiếu nó là bug cache kinh điển, chỉ nén text không nén ảnh đã nén bài 09 server chỉ nén text html text css application javascript application json không nén image jpeg image png video mp4 woff2 đã nén khoảng 1x nginx gzip_types liệt kê rõ các kiểu text

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ề:

Ảnh chụp bảng kết quả chạy thật nén HTTP output thật go-lab curl tới coffeecode.vn thật cộng nén cục bộ gzip vs brotli, a curl coffeecode.vn header và kích thước tải về Content-Encoding gzip Vary Accept-Encoding server cấu hình đúng Accept-Encoding identity 250841 byte tải về không nén Accept-Encoding gzip 25588 byte tải về 9.8x nhỏ hơn Accept-Encoding br 250841 byte tải về server không hỗ trợ br trên mạng trang 245KB chỉ còn 25KB tải nhanh hơn khoảng 10 lần, b nén cục bộ chính HTML đó gzip vs brotli phương pháp size B ratio gốc 250841 1.0x gzip -6 21841 11.5x gzip -9 21688 11.6x brotli -5 19843 12.6x brotli -11 17474 14.4x brotli nhỏ hơn gzip khoảng 24 phần trăm cho web text, đọc kết quả coffeecode.vn dùng gzip 9.8x thật trên đường truyền chuẩn tương thích rộng brotli -11 nén web text tốt hơn gzip khoảng 24 phần trăm 17.4KB vs 21.7KB nhiều CDN phục vụ br cho asset tĩnh nhưng server này chưa bật br báo trung thực nén diễn ra tự động nếu server cộng client đều hỗ trợ lập trình viên chỉ cần bật nén đúng kiểu text và đặt Vary giảm băng thông và thời gian tải

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 đặt Vary: 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ề

  1. 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.
  2. 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).
  3. Cấu hình đúng: Vary + chỉ nén text: Vary: Accept-Encoding bắ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

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ớ.