Đừng nén JSON sai thứ tự: minify so với gzip so với brotli, có đo đạc
Kết luận: Minify cắt 30-50% khỏi JSON thô, nhưng gzip xóa gần hết lợi ích đó. Các con số đo được trên hình dạng dữ liệu thật, cùng bốn trường hợp minify làm mọi thứ tệ hơn.

Kết luận: trước hết hãy tách nén truyền tải khỏi việc rút gọn cấu trúc. Nếu gzip hoặc brotli chưa được bật, hãy sửa cấu hình máy chủ — chỉ gzip -6 đã đạt 94,5% và để lại cho minify giá trị chỉ 0,9 điểm. Toàn bộ 46,3% của minify chỉ có giá trị khi số byte được ghi lại cục bộ: thân yêu cầu, giới hạn 6 MB của Lambda, 1 MiB của Firestore, hoặc JSON ghi xuống đĩa và nhật ký.
Huy hiệu "tiết kiệm 35%" trên công cụ minify yêu thích của bạn là trung thực - nó đo byte thô. API của bạn chạy trên gzip hoặc brotli, và hai công cụ đó ăn đúng phần dư thừa ấy. Nhân hai con số với nhau và bạn sẽ có câu trả lời sai.
1. Tách "nén" thành hai việc khác nhau
Cái mà người ta gọi là nén JSON thật ra là hai thao tác không liên quan, và việc trộn lẫn chúng chính là khởi đầu của mọi kết luận sai.
| minify (bỏ khoảng trắng) | gzip / brotli (nén truyền tải) | |
|---|---|---|
| Bản chất | Một phép biến đổi tài liệu | Một phép biến đổi truyền tải |
| Đầu ra | Văn bản JSON vẫn hợp lệ, vẫn đọc được | Nhị phân; không dùng được cho tới khi giải mã |
| Nơi lợi ích được ghi nhận | Đĩa, bộ nhớ, nhật ký, giới hạn kích thước | Chỉ trên đường truyền và trong bộ nhớ đệm |
| Sau khi giải mã | Số byte vẫn nhỏ hơn | Số byte trở về kích thước gốc |
| Ai chịu trách nhiệm | Script build của bạn, tham số serializer của bạn | Nginx, CDN, framework máy chủ |
Hai hàng cuối mới là điều quan trọng. Các giới hạn nền tảng được đo sau khi giải mã: mức trần payload 6 MB của AWS Lambda, mức trần 1 MiB mỗi tài liệu của Firestore. Bật gzip không mua cho bạn một byte nào ở đó, vì cả hai đều tính kết quả đã giải mã.
2. minify loại bỏ đúng một thứ: khoảng trắng cấu trúc
RFC 8259 cho phép khoảng trắng (space, \t, \n, \r) quanh bất kỳ ký tự cấu trúc nào. Không có gì khác trong một tài liệu JSON là tùy chọn: tên khóa là dữ liệu, số giữ mọi chữ số, và nội dung chuỗi bất khả xâm phạm từng byte.
Điều đó làm cho toàn bộ thao tác chỉ còn một dòng:
const minified = JSON.stringify(JSON.parse(pretty));
// RFC 8259 allows whitespace only around structural characters,
// so spaces inside a string are data, not formatting
JSON.stringify(JSON.parse('{"note": "two spaces"}'));
// {"note":"two spaces"} - both inner spaces are preserved
Đáng lưu ý là ba thao tác bị xếp nhầm vào "nén" nhưng thật ra sửa đổi tài liệu: bỏ các khóa có giá trị null, làm tròn số thực, và sắp xếp lại khóa. Những thao tác đó thay đổi tài liệu chứ không chỉ định dạng lại, nên bất kỳ công cụ nào quảng cáo "nén sâu" đều cần sự đồng ý của bên tiêu thụ trước khi bạn bật nó.
Vì minify chỉ bỏ khoảng trắng, lợi ích của nó gần như phụ thuộc hoàn toàn vào phần thụt lề chiếm bao nhiêu trong tệp của bạn. Hãy kỳ vọng điều đó lặp lại nhiều lần trong các bảng bên dưới.
3. Phép đo: bốn tài liệu, ba mức gzip, cộng brotli
Môi trường: Node v22.13.1, zlib.gzipSync ở mức 1 / 6 / 9, brotliCompressSync ở chất lượng mặc định (11). Bốn mẫu cố ý bao phủ những hình dạng bạn thật sự phát hành: 50 đơn hàng lồng nhau (cấu trúc sâu, khóa lặp lại), một cấu hình 6 cấp (khóa dài, giá trị ngắn), một mảng nhật ký 200 dòng (chuỗi dài, gần như không thụt lề), và media-manifest.json thật của trang này (hình dạng hỗn hợp, chuỗi nhiều URL).
Bước một: byte thô.
| Mẫu | pretty (2 dấu cách) | pretty (4 dấu cách) | sau minify | minify tiết kiệm |
|---|---|---|---|---|
| 50 đơn hàng lồng nhau | 29.551 B | 40.761 B | 15.881 B | 46,3% |
| Cấu hình 6 cấp | 25.450 B | 38.012 B | 11.679 B | 54,1% |
| Mảng nhật ký 200 dòng | 14.582 B | 14.982 B | 13.981 B | 4,1% |
| media-manifest.json | 25.149 B | 28.114 B | 21.222 B | 15,6% |
Đây là nguồn gốc của huy hiệu "tiết kiệm 35%" đó, và nó không nói dối. Vấn đề xuất hiện ở bước hai.
Bước hai: nén từng biến thể, rồi hỏi minify còn thêm được bao nhiêu.
| Mẫu | trên nền gzip -1 | trên nền gzip -6 | trên nền gzip -9 | trên nền brotli |
|---|---|---|---|---|
| 50 đơn hàng lồng nhau | 21,0% | 17,0% | 8,9% | 10,6% |
| Cấu hình 6 cấp | 74,2% | 59,1% | 58,0% | 56,4% |
| Mảng nhật ký 200 dòng | -0,7% | -0,4% | 0,6% | -0,3% |
| media-manifest.json | 1,5% | 1,0% | 1,0% | 1,6% |
Cùng tài liệu đơn hàng lồng nhau mất 46,3% byte thô chỉ còn giữ 17,0% số đó khi gzip -6 vào cuộc. Mảng nhật ký thậm chí âm: minify-rồi-nén còn lớn hơn một chút so với nén bản pretty gốc. Kết quả âm không phải lỗi, nó đúng là điều bạn nên kỳ vọng, và phần tiếp theo giải thích vì sao.
Bước ba: mức giảm tổng so với tài liệu pretty gốc - con số thật sự đi qua đường truyền.
| Mẫu | chỉ gzip -6 | minify + gzip -6 | minify + brotli |
|---|---|---|---|
| 50 đơn hàng lồng nhau | 94,5% | 95,4% | 96,9% |
| Cấu hình 6 cấp | 96,0% | 98,4% | 99,0% |
| Mảng nhật ký 200 dòng | 89,9% | 89,9% | 95,0% |
| media-manifest.json | 83,5% | 83,6% | 86,1% |
4. Vì sao gzip nuốt mất công lao của minify
gzip là LZ77 theo sau là mã hóa Huffman. Động tác cốt lõi của LZ77 là "nhìn lại qua cửa sổ 32 KB, tìm một chuỗi lặp lại, thay nó bằng con trỏ (khoảng cách, độ dài)." Thụt lề là mục tiêu dễ nhất có thể cho động tác đó: mỗi cấp lồng nhau phát ra một dãy dấu cách giống hệt, và hàng chục dòng liền kề lặp lại đúng tổ hợp ấy. Huffman sau đó gán mã ngắn cho các ký hiệu có tần suất cao, nên một mẫu như \n cộng tám dấu cách - mẫu có thể lặp lại cả nghìn lần trong một tệp - cuối cùng chỉ tốn hai hoặc ba bit.
Vậy nên những byte minify xóa chính là những byte mà bộ nén đã gần như gộp miễn phí. Tự làm việc đó nghĩa là làm lại công việc mà bộ nén vốn đã rất giỏi, và 46% của bạn sụp xuống 17%. Đó là toàn bộ câu chuyện đằng sau bảng ở trên.
Kết quả âm của mảng nhật ký xứng đáng có lời giải thích riêng. Mẫu đó gần như toàn chuỗi dài (dòng nhật ký, URL, base64), nghĩa là ít lặp lại và entropy cao - ít dư thừa để bộ nén loại bỏ ngay từ đầu. Các ký tự xuống dòng trong bản pretty tình cờ tạo cho bộ so khớp những ranh giới ổn định. Bỏ chúng đi và LZ77 mất các điểm neo đó: trong mẫu này, nén bản pretty gốc ở gzip -1 cho 1.508 byte, trong khi minify-rồi-nén cho 1.518 byte. Tệ hơn mười byte.
Xếp theo độ lớn, các đòn bẩy trông như sau:
| Đòn bẩy | Tiết kiệm so với tài liệu pretty gốc (đơn hàng lồng nhau) | Cái giá |
|---|---|---|
| minify | 46,3% (chỉ byte thô) | Mất khả năng đọc; cần một bước build |
| gzip -6 | 94,5% | Tốn chút CPU máy chủ; cấu hình gần như bằng không |
| minify + gzip -6 | 95,4% | Cộng hai mục trên lại |
| minify + brotli | 96,9% | Cần HTTPS và module brotli; nén trước tài sản tĩnh |
Bước nhảy từ 0 lên 94,5% là công của gzip. Bước nhảy từ 94,5% lên 95,4% - chín phần mười điểm - là công của minify. Bảng này xác định nơi công sức của bạn nên đặt.
5. Mức nén là biến số ẩn
Bất kỳ lập luận nào giả định một mức gzip cố định đều đứng trên cát, vì mức nén thay đổi trực tiếp giá trị biên của minify:
| Thiết lập truyền tải | Byte thêm mà minify vẫn tiết kiệm (đơn hàng lồng nhau) |
|---|---|
gzip -1 (mặc định gzip_comp_level của Nginx) |
21,0% |
| gzip -6 (mặc định trong hầu hết thư viện chuẩn) | 17,0% |
| gzip -9 | 8,9% |
| brotli (chất lượng mặc định) | 10,6% |
Nén truyền tải của bạn càng yếu, minify càng đáng giá. Điều đó cho một quy tắc thực tế: trên một bản cài Nginx mặc định nơi gzip_comp_level vẫn là 1, minify mang lại 21%. Khi bạn đã tinh chỉnh lên 9 hoặc chuyển sang brotli, nó giảm còn 9-11% và bạn thường có thể bỏ qua.
Trước khi tranh luận theo hướng nào, hãy đo xem bạn đang ở tầng nào:
# Fetch the same response with three Accept-Encoding values and compare what landed
for enc in identity gzip br; do
size=$(curl -s -o /dev/null -w '%{size_download}' \
-H "Accept-Encoding: $enc" https://api.example.com/v1/orders)
echo "$enc -> $size bytes"
done
Ba con số trả lời ba câu hỏi cùng lúc: nén có đang bật không, ở mức nào, và còn bao nhiêu chỗ cho minify. Nếu identity và gzip trả về gần như giống hệt nhau, nén hoàn toàn không hoạt động - và thứ cần sửa là Nginx, không phải JSON của bạn:
gzip on;
gzip_comp_level 6;
gzip_min_length 256; # skip tiny responses, they only grow
gzip_types application/json application/javascript text/css;
6. Khi nào minify thật sự đáng giá
Một phép thử quyết định: những byte bạn tiết kiệm sẽ được ghi lại ở đâu đó, hay bạn sẽ chạm một giới hạn tính bằng byte thô? Nếu có, toàn bộ 46% được ghi nhận. Nếu câu trả lời là "nó chỉ tồn tại trên đường truyền", thì 17% là giá trị thật của nó.
Thứ nhất, thân yêu cầu HTTP. Hầu hết máy chủ không giải nén thân yêu cầu, và gzip hầu như chỉ được cấu hình cho phản hồi. JSON bạn POST thường không có lớp bảo vệ truyền tải nào, nên mức tiết kiệm của minify đến với giá đầy đủ. Điều tương tự đúng với callback webhook, telemetry MQTT, và khung WebSocket chưa thương lượng permessage-deflate.
Thứ hai, các kênh có trần byte cứng - và mọi kênh trong số đó đều tính byte đã giải mã:
| Kênh | Giới hạn | gzip có giúp được không? |
|---|---|---|
| Payload đồng bộ AWS Lambda | 6 MB | Không, được đo sau khi giải mã |
| Một tài liệu Firestore | 1 MiB | Không |
| Một tin nhắn Kafka (mặc định) | 1 MB | Không, nén phía broker là một lớp khác |
| Hạn mức localStorage | ~5 MB (tính theo ký tự) | Không |
| URL yêu cầu GET | Tùy máy chủ, phổ biến là 8 KB | Không |
Trong nhóm này, minify không phải tối ưu hóa mà là khác biệt giữa một yêu cầu gửi được và một yêu cầu thất bại. Nhúng JSON vào chuỗi truy vấn URL là trường hợp sắc nét nhất: URL không bao giờ được nén và chúng có giới hạn độ dài, nên hãy minify trước, rồi quyết định xem có cần base64 hay không.
Thứ ba, JSON ghi xuống đĩa hoặc nhật ký. Nhật ký và chỉ mục lưu byte nguyên văn mà không có lớp truyền tải nào tham gia. Có một lưu ý khiến người ta vấp: minify chỉ giúp nếu bạn vốn đang ghi JSON pretty. Mẫu nhật ký 200 dòng đó chỉ tiết kiệm 4,1%, vì các dòng của nó vốn đã gọn và thụt lề chiếm gần như không có trong tệp. Ngược lại, nếu logger của bạn gọi JSON.stringify(obj, null, 2), bỏ tham số thứ ba là tiền miễn phí.
Một lưu ý cuối: khi byte thô thật sự chật, tên khóa thường là đòn bẩy lớn hơn thụt lề. Trên media-manifest.json, thay 14 khóa duy nhất của nó bằng k0 đến k13 đưa dạng đã minify từ 21.222 B xuống 18.887 B - thêm 11,0%. Khi nén, lợi thế còn lại đó co lại còn 2,4% dưới gzip -6 và 1,3% dưới brotli. Cơ chế giống hệt mọi thứ ở trên: thụt lề và tên khóa đều là thông tin cấu trúc lặp lại, và bộ nén nhận cùng cổ tức theo cách nào cũng vậy.
7. Bốn trường hợp nó phản tác dụng
Đừng minify các tệp bạn commit. Đổi "qty": 2 thành 3 và tệp pretty hiện một dòng sạch trong git diff; tệp đã minify viết lại toàn bộ dòng, và với hàng trăm trường bạn không thể tìm ra thay đổi. Đây là cơ chế tương tự được trình bày trong Cách giải quyết xung đột merge JSON - công cụ diff hoạt động theo dòng, nên JSON một dòng biến mọi chỉnh sửa thành "cả dòng khác nhau". Hãy giữ tệp pretty trong kho mã và chỉ minify ở đầu ra build hoặc khi ghi vào lưu trữ.
Đừng nén các phản hồi tí hon. Một phản hồi chỉ chứa {"ok": true} là 16 byte khi thêm thụt lề, và 36 byte sau gzip -6 - lớn hơn hai mươi byte, vì từ điển và header của gzip chiếm chỗ. Đây không phải chuyện lý thuyết; đó chính là lý do gzip_min_length tồn tại. Giá trị mặc định 20 byte của Nginx quá thấp cho production, nơi 256 là mức sàn thông thường.
Đừng kỳ vọng phân tích nhanh hơn. Phóng cùng bộ đơn hàng lên 4.000 bản ghi, tài liệu pretty là 2.374.378 byte so với 1.282.358 byte đã minify - nhỏ hơn 46% - nhưng thời gian JSON.parse trung vị chỉ chuyển từ 5,7 ms sang 5,0 ms, nhanh hơn khoảng 12%. Giảm một nửa kích thước không giảm một nửa thời gian, vì trình phân tích dành phần lớn ngân sách để cắt chuỗi và chuyển số; bỏ khoảng trắng chỉ là việc phụ. Thứ thật sự quyết định chi phí phân tích là số khóa và nội dung chuỗi, điều mà Phân tích, kết xuất và truyền luồng JSON lớn phân tích chi tiết.
Đừng gắn nó lên một dịch vụ đã chạy brotli hoặc gzip -9. Lợi ích giảm còn 9-11%, trong khi cái giá - mất vĩnh viễn khả năng đọc khi bạn gỡ lỗi - vẫn còn. Thay vào đó hãy kiểm tra xem gzip_types có bao gồm application/json hay không. Rất nhiều trang bật gzip nhưng chỉ nén text/html, để JSON hoàn toàn không nén; trong tình huống đó minify không phải bước một, sửa cấu hình mới là.
8. Bảng quyết định: bước nào trước
| Tình huống của bạn | Làm điều này trước | Vị trí của minify |
|---|---|---|
| Không có nén truyền tải nào | Cấu hình gzip_types của Nginx hoặc bật tự động nén của CDN |
Bỏ qua; chín phần mười lợi ích bốc hơi |
| Đang chạy gzip -1 (mặc định Nginx) | Nâng gzip_comp_level lên 6 |
Thứ hai, đáng 21% |
| Đã dùng brotli hoặc gzip -9 | Rà soát khóa dài, thêm phân trang hoặc cắt trường | Bỏ qua; chỉ còn 9-11% |
| Đang chạm Lambda 6 MB hoặc Firestore 1 MiB | Minify trực tiếp; toàn bộ 46% được tính | Thứ nhất |
| Thân POST, webhook, MQTT | Minify trực tiếp; truyền tải hiếm khi áp dụng ở đây | Thứ nhất |
| Tệp cấu hình đang được đưa vào Git | Giữ chúng pretty | Chỉ ở đầu ra build |
| Nhật ký, telemetry, bản kết xuất ngoại tuyến | Bỏ tham số null, 2 trong logger |
Đó chính là bước một |
Quy tắc một câu: hãy xem bạn đang cố làm nhỏ sổ byte nào - sổ được ghi cục bộ (đĩa, hạn mức, giới hạn nền tảng, thân yêu cầu) hay sổ chỉ tồn tại trên đường truyền. Cục bộ trước nghĩa là minify; chỉ trên đường truyền nghĩa là bật brotli hoặc gzip trước.
Trong thực tế, hãy chạy vòng lặp curl ở trên để xác nhận nén truyền tải đang hoạt động, rồi đưa một mẫu qua công cụ Minify JSON và đọc con số tiết kiệm của nó. Nếu con số đó trên 40% trong khi curl báo identity và gzip gần như cùng kích thước, nút cổ chai của bạn là cấu hình máy chủ, không phải JSON. Nếu hai con số curl đã gần nhau, tài liệu này không còn nhiều điều để nói và bạn nên cắt bớt trường hoặc phân trang thay vào đó - chủ đề của Những điều cốt yếu khi thiết kế hợp đồng API JSON.
JSON đã minify có còn là JSON hợp lệ không?
Có. minify chỉ bỏ khoảng trắng mà RFC 8259 cho phép quanh các ký tự cấu trúc và không bao giờ chạm vào nội dung chuỗi, nên JSON.parse tạo ra một giá trị giống hệt bản gốc. Nạn nhân duy nhất là khả năng đọc của con người.
brotli hay gzip?
Trên cùng một mẫu đã minify, brotli tiết kiệm 96,9% và gzip -6 tiết kiệm 95,4% - khoảng cách hẹp. Dùng gzip -6 cho phản hồi động để giữ CPU thấp, và để dành brotli cho tài sản tĩnh giao qua HTTPS mà bạn có thể nén trước thành tệp .br.
Tôi đã minify JSON nhưng API không nhanh hơn. Vì sao?
Hãy kiểm tra phản hồi bằng curl -sI để tìm header Content-Encoding: gzip. Nhiều bản cài Nginx mặc định gzip_types chỉ là text/html, nên JSON chưa bao giờ được bao phủ - và chưa tới một phần năm trong 46% của bạn từng đến được đường truyền.
Tệp cấu hình có nên được commit dưới dạng đã minify không?
Không. Công cụ diff so sánh theo dòng, nên JSON một dòng biến mọi chỉnh sửa thành "cả dòng đã thay đổi", và việc giải quyết xung đột tốn kém hơn nhiều so với số byte bạn tiết kiệm. Hãy giữ bản pretty trong kho mã và minify ở lúc build.
Phản hồi phải lớn bao nhiêu thì gzip mới đáng?
Ngưỡng thông thường là 256 byte đến 1 KB. Một phản hồi nhỏ như {"ok": true} - 16 byte khi có thụt lề - lớn lên 36 byte dưới gzip -6, vì từ điển và header tốn nhiều hơn mức chúng thu hồi. Đó chính xác là điều gzip_min_length kiểm soát.