Nén nội dung là một trong những đòn bẩy hiệu năng rẻ nhất của HTTP: thêm một header, và thân trả lời co lại còn một phần nhỏ, tiết kiệm băng thông lẫn thời gian tải. Nhưng "co lại bao nhiêu" phụ thuộc mạnh vào nội dung gì và nén bằng gì. Bài này nén cùng một bộ dữ liệu bằng gzip và Brotli ở nhiều mức, đo chính xác số byte giảm và thời gian tốn — và chỉ ra một so sánh thiếu công bằng suýt khiến tôi kết luận sai về Brotli.
Cách nó hoạt động: thỏa thuận rồi nén thân
Nén HTTP là một cuộc thỏa thuận. Client báo nó hiểu những cách nén nào qua header Accept-Encoding: gzip, br; server chọn một, nén thân trả lời, rồi báo lại cách đã dùng qua Content-Encoding: br. Chỉ thân được nén — dòng trạng thái và các header vẫn ở dạng thường để client đọc được ngay. gzip có mặt gần như khắp nơi từ hàng chục năm; br (Brotli) mới hơn, nén chặt hơn cho văn bản, và ngày nay đa số trình duyệt đều hỗ trợ.
Đo: văn bản co lại cực nhiều
Tôi lấy một JSON kiểu API — một mảng 400 bản ghi giống nhau, 20.941 byte — rồi nén bằng gzip và Brotli ở vài mức. Kết quả:
| Bộ nén | Kích thước | Giảm | Thời gian nén |
|---|---|---|---|
| gzip mức 6 (mặc định) | 2358 B | 88,7% | 0,06 ms |
| Brotli mức 4 | 1091 B | 94,8% | 0,05 ms |
| Brotli mức 11 (cao nhất) | 957 B | 95,4% | 11 ms |
Một JSON 20 KB co xuống còn ~2,3 KB với gzip — tiết kiệm gần 90% băng thông cho đúng cùng dữ liệu. Brotli còn ép mạnh hơn: mức 4 xuống 1 KB, mức 11 xuống 957 byte — nhỏ hơn gzip khoảng 60%. Văn bản, JSON và HTML nén được nhiều đến vậy vì chúng đầy dư thừa: từ khóa lặp, thẻ lặp, cấu trúc lặp. (Số của tôi cao vì dữ liệu mẫu rất lặp; văn bản thật thường co còn 20-30% cỡ gốc — vẫn là một khoản tiết kiệm khổng lồ.)
Cái gì không nén được — và nén còn hại
Đây là mặt kia mà nhiều người quên. Không phải nội dung nào cũng nén được, và nén nhầm còn tốn thay vì tiết kiệm. Tôi nén 20.000 byte ngẫu nhiên (đại diện cho ảnh, video, tệp đã nén sẵn — vốn gần như ngẫu nhiên về mặt thống kê):
gzip: 20.028 byte (PHÌNH thêm 28 byte)
Brotli: 20.004 byte, và Brotli mức 11 tốn 32 ms CPU cho con số đó
Dữ liệu ngẫu nhiên không có dư thừa để khai thác, nên nén nó không giảm được gì, thậm chí làm phình thêm vài byte khung, và Brotli mức 11 còn đốt 32 mili giây CPU để đạt kết quả tệ hơn. Đây là lý do server không nên nén ảnh JPEG/PNG, video, hay tệp .zip — chúng đã nén rồi.
Và một cái bẫy nữa với nội dung tí xíu. Nén một JSON 35 byte:
gzip: 53 byte (PHÌNH 51% — khung gzip lớn hơn phần tiết kiệm)
Brotli: 39 byte (vẫn phình)
Với payload vài chục byte, chi phí khung của định dạng nén (gzip có ~18 byte header/trailer cố định) lớn hơn bất cứ thứ gì nó tiết kiệm được, nên kết quả to hơn bản gốc. Đây là lý do các server đặt ngưỡng "chỉ nén khi thân lớn hơn N byte" (thường ~1 KB).
Một lần tôi đo hớ khi so gzip với Brotli
Nhìn bảng đầu tiên, tôi suýt viết một kết luận sai: "Brotli chậm hơn gzip cả trăm lần". Con số có vẻ ủng hộ — Brotli nén JSON mất 11 mili giây, còn gzip chỉ 0,06 mili giây, chậm gấp gần 200 lần.
Nhưng phép so đó không công bằng, và tôi kịp nhận ra. Tôi đã lấy mức cao nhất của Brotli (11) đấu với mức mặc định của gzip (6) — như so một vận động viên chạy hết sức với một người đang đi bộ. Khi so ở mức nỗ lực tương đương — Brotli mức 4 với gzip mức 6 — Brotli nén JSON trong 0,05 mili giây, tức nhanh hơn gzip, mà kết quả vẫn nhỏ hơn (1091 so với 2358 byte). Brotli không hề chậm; chỉ có Brotli mức 11 mới chậm, và mức đó dành cho việc nén sẵn tệp tĩnh một lần rồi phục vụ nhiều lần, không phải để nén động mỗi request.
Bài học đo lường: so hai công cụ phải đặt chúng ở cùng một mức nỗ lực. Lấy giá trị cực đại của cái này đấu với giá trị mặc định của cái kia cho ra một kết luận nghe có số liệu mà sai bản chất. Với nén, "chậm hay nhanh" và "nhỏ hay to" đều là hàm của mức — và mức là thứ đầu tiên phải kiểm soát khi đo.
Một cảnh báo: nén có thể rò rỉ bí mật
Có một mặt tối ít người biết: nén một phản hồi trộn lẫn bí mật với dữ liệu kẻ tấn công điều khiển được có thể làm rò rỉ bí mật đó. Cơ chế là kích thước sau nén phụ thuộc vào độ giống nhau của nội dung — nếu phần kẻ tấn công chèn vào trùng với một phần bí mật (ví dụ một token CSRF), dữ liệu nén lại nhỏ hơn một chút, và bằng cách thử từng ký tự rồi quan sát kích thước, kẻ tấn công đoán ra được bí mật. Đây là gốc của các tấn công CRIME và BREACH.
Hệ quả thực tế: đừng nén chung, trong một phản hồi, những dữ liệu bí mật (token, mã phiên) với nội dung phản chiếu lại đầu vào của người dùng. Cách phòng gồm tách bí mật ra khỏi phần nén được, thêm ngẫu nhiên vào phản hồi, hoặc không nén các endpoint nhạy cảm. Nén là công cụ tốt, nhưng biết chỗ không nên bật cũng quan trọng như biết chỗ nên.
Vì sao điều này quan trọng khi lập trình
Hệ quả đầu tiên, và đáng làm ngay: bật nén cho nội dung văn bản. HTML, JSON, CSS, JavaScript, SVG — tất cả co lại 70-90%, cắt thẳng chừng đó băng thông và thời gian tải, đặc biệt trên mạng di động hay đường xa nơi mỗi byte đắt. Đây cũng nối với bài về một GET: nén không giúp được phần header (chúng nhỏ và cần đọc ngay), nhưng với thân lớn thì nó là khoản tiết kiệm lớn nhất bạn có thể bật bằng một dòng cấu hình.
Hệ quả thứ hai: đừng nén thứ không nén được. Cấu hình server đúng cách là chỉ nén các kiểu văn bản (gzip_types/brotli_types) và đặt ngưỡng kích thước tối thiểu (gzip_min_length ~1 KB). Nén ảnh, video, tệp đã nén, hay câu trả lời vài chục byte chỉ đốt CPU và đôi khi làm phình thêm — đúng như phép đo cho thấy. Một cấu hình "nén tất cả" là một cấu hình phí.
Hệ quả thứ ba: chọn mức theo tình huống. Với tài nguyên tĩnh (CSS, JS gói sẵn), nén trước bằng Brotli mức 11 một lần lúc build rồi phục vụ mãi — 11 mili giây trả một lần, tiết kiệm băng thông vô số lần. Với nội dung động sinh mỗi request, dùng mức thấp (gzip mức 6, hoặc Brotli mức 4) để nén nhanh mà vẫn nhỏ — như đo được, Brotli mức 4 vừa nhanh hơn gzip vừa nhỏ hơn. Con số mang theo: nén cắt được 80-95% băng thông cho văn bản, gần như bằng không cho dữ liệu đã nén, và làm phình cho nội dung tí xíu — nên bật cho văn bản, tắt cho phần còn lại, và chọn mức theo tĩnh hay động. Đó là một trong những đòn bẩy hiệu năng có tỉ lệ lợi-trên-công lớn nhất trong HTTP.
Thử ba mươi giây
Lấy một tài nguyên văn bản với và không nén rồi so kích thước: curl -s -H 'Accept-Encoding: gzip, br' -o /dev/null -w 'nen: %{size_download} byte\n' https://mot-trang so với curl -s -o /dev/null -w 'thuong: %{size_download} byte\n' https://mot-trang. Hiệu hai con số chính là số byte nén tiết kiệm được. Thêm -v và tìm dòng content-encoding: trong header trả về để biết server đã dùng gzip hay br. Thử trên một trang HTML (co nhiều) rồi trên một ảnh (gần như không co) để thấy đúng khác biệt bài này đo.