فشردهسازی JSON را به ترتیب اشتباه انجام ندهید: minify در برابر gzip در برابر brotli، اندازهگیریشده
جمعبندی: فشردهسازی متنی ۳۰ تا ۵۰ درصد از JSON خام را میکوبد، اما gzip بیشتر این بهره را پاک میکند. اعداد اندازهگیریشده روی شکلهای واقعی، بهعلاوه چهار موردی که فشردهسازی وضع را بدتر میکند.

جمعبندی: اول فشردهسازی انتقال را از هرس ساختاری جدا کنید. اگر gzip یا brotli هنوز فعال نیست، پیکربندی سرور را درست کنید — gzip -6 تنها به ۹۴.۵٪ میرسد و minify فقط ۰.۹ واحد ارزش دارد. ۴۶.۳٪ کامل minify فقط وقتی به دست میآید که صورتحساب بایت محلی ثبت شود: بدنههای درخواست، ۶ مگابایت Lambda، ۱ MiB Firestore، یا JSON نوشتهشده روی دیسک و لاگ.
نشان «۳۵٪ صرفهجویی» روی minifier محبوب شما صادقانه است — بایت خام را اندازه میگیرد. API شما روی gzip یا brotli اجرا میشود و آن دو دقیقاً همان افزونگی را میخورند. دو عدد را ضرب کنید و پاسخ اشتباه میگیرید.
۱. «فشردهسازی» را به دو چیز متفاوت بشکافید
چیزی که مردم فشردهسازی JSON مینامند واقعاً دو عملیات نامرتبط است و قاطی کردنشان سرآغاز هر نتیجه اشتباه است.
| minify (حذف فضای سفید) | gzip / brotli (فشردهسازی انتقال) | |
|---|---|---|
| ماهیت | تبدیل سند | تبدیل انتقال |
| خروجی | همچنان متن JSON معتبر و خوانا | دودویی؛ تا decode نشود استفادهنشدنی |
| برد کجا مینشیند | دیسک، حافظه، لاگ، محدودیت اندازه | فقط روی سیم و در کش |
| پس از decode | بایتها کوچکتر میمانند | بایتها به اندازه اصلی برمیگردند |
| مالک | اسکریپت ساخت، آرگومانهای سریالایزر | Nginx، CDN، فریمورک سرور |
دو ردیف آخر همانهاییاند که مهماند. محدودیتهای پلتفرم پس از decode اندازهگیری میشوند: سقف ۶ مگابایتی payload همگام AWS Lambda و سقف ۱ MiB به ازای هر سند Firestore. روشن کردن gzip یک بایت هم آنجا نمیخرد، چون هر دو نتیجه decodeشده را میشمارند.
۲. minify دقیقاً یک چیز را حذف میکند: فضای سفید ساختاری
RFC 8259 فضای سفید (space، \t، \n، \r) حول هر نویسه ساختاری را مجاز میداند. هیچ چیز دیگری در سند JSON اختیاری نیست: نام کلید داده است، عددها هر رقم را نگه میدارند و محتوای رشته بایتبهبایت دستنزدنی است.
این کل عملیات را یک خط میکند:
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
سه عملیاتی که زیر «فشردهسازی» طبقهبندی میشوند اما واقعاً سند را تغییر میدهند شایسته اشارهاند: حذف کلیدهایی که مقدارشان null است، گرد کردن شناورها، و بازچینش کلیدها. اینها سند را تغییر میدهند نه بازقالببندی، پس هر ابزاری که «فشردهسازی عمیق» تبلیغ میکند پیش از فعالسازی به تأیید مصرفکننده شما نیاز دارد.
چون minify فقط فضای سفید را حذف میکند، بهرهاش تقریباً کاملاً به اینکه چه مقدار از فایل شما تورفتگی است بستگی دارد. انتظار دارید این موضوع در جدولهای زیر مکرر ظاهر شود.
۳. اندازهگیری: چهار سند، سه سطح gzip، بهعلاوه brotli
محیط: Node v22.13.1، zlib.gzipSync در سطح ۱ / ۶ / ۹، brotliCompressSync با کیفیت پیشفرض (۱۱). چهار نمونه عمداً شکلهایی را پوشش میدهند که واقعاً ارسال میکنید: ۵۰ سفارش تودرتو (ساختار عمیق، کلیدهای تکراری)، پیکربندی ۶ سطحی (کلیدهای بلند، مقدارهای کوتاه)، آرایه لاگ ۲۰۰ خطی (رشتههای بلند، تقریباً بدون تورفتگی)، و media-manifest.json واقعی این سایت (شکل مختلط، رشتههای URL-محور).
گام اول: بایت خام.
| نمونه | pretty (۲ فاصله) | pretty (۴ فاصله) | پس از minify | صرفهجویی minify |
|---|---|---|---|---|
| ۵۰ سفارش تودرتو | 29,551 B | 40,761 B | 15,881 B | 46.3% |
| پیکربندی ۶ سطحی | 25,450 B | 38,012 B | 11,679 B | 54.1% |
| آرایه لاگ ۲۰۰ خطی | 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% |
نشان «۳۵٪ صرفهجویی» از اینجا میآید و دروغ نمیگوید. مشکل در گام دوم ظاهر میشود.
گام دوم: هر گونه را فشرده کنید، سپس بپرسید minify چقدر هنوز اضافه میکند.
| نمونه | روی gzip -1 | روی gzip -6 | روی gzip -9 | روی brotli |
|---|---|---|---|---|
| ۵۰ سفارش تودرتو | 21.0% | 17.0% | 8.9% | 10.6% |
| پیکربندی ۶ سطحی | 74.2% | 59.1% | 58.0% | 56.4% |
| آرایه لاگ ۲۰۰ خطی | -0.7% | -0.4% | 0.6% | -0.3% |
| media-manifest.json | 1.5% | 1.0% | 1.0% | 1.6% |
همان سند سفارشهای تودرتو که ۴۶.۳٪ از بایت خامش را از دست میدهد، پس از دخالت gzip -6 فقط ۱۷.۰٪ از آن را نگه میدارد. آرایه لاگ صراحتاً منفی میشود: minify-سپس-فشردهسازی کمی بزرگتر از فشردهسازی اصل pretty است. منفی بودن باگ نیست، دقیقاً همان چیزی است که باید انتظار داشته باشید، و بخش بعد توضیح میدهد چرا.
گام سوم: کاهش کل در برابر سند pretty اصلی — عددی که واقعاً روی سیم سفر میکند.
| نمونه | فقط gzip -6 | minify + gzip -6 | minify + brotli |
|---|---|---|---|
| ۵۰ سفارش تودرتو | 94.5% | 95.4% | 96.9% |
| پیکربندی ۶ سطحی | 96.0% | 98.4% | 99.0% |
| آرایه لاگ ۲۰۰ خطی | 89.9% | 89.9% | 95.0% |
| media-manifest.json | 83.5% | 83.6% | 86.1% |
۴. چرا gzip اعتبار minify را میخورد
gzip همان LZ77 بهعلاوه کدگذاری هافمن است. حرکت اصلی LZ77 «به عقب در پنجره ۳۲ کیلوبایتی نگاه کن، دنبالهای تکراری پیدا کن، با اشارهگر (فاصله، طول) جایگزینش کن» است. تورفتگی آسانترین هدف ممکن برای آن حرکت است: هر سطح تودرتویی یک ردیف یکسان از فاصله تولید میکند و دهها خط مجاور همان ترکیب را تکرار میکنند. هافمن سپس کدهای کوتاه به نمادهای پرتکرار میدهد، پس الگویی مانند \n بهعلاوه هشت فاصله — که ممکن است هزار بار در یک فایل تکرار شود — در نهایت دو یا سه بیت هزینه دارد.
پس بایتهایی که minify حذف میکند دقیقاً همان بایتهایی هستند که فشردهساز تقریباً مجانی تا میکرد. خودتان انجام دادنش یعنی دوباره کاریای که فشردهساز از قبل در آن خبره است، و ۴۶٪ شما به ۱۷٪ فرو میریزد. کل داستان پشت جدول بالا همین است.
نتیجه منفی آرایه لاگ توضیح خودش را میخواهد. آن نمونه تقریباً کاملاً رشتههای بلند (خطوط لاگ، URL، base64) است، یعنی تکرار کم و آنتروپی بالا — افزونگی کمی برای حذف توسط فشردهساز. خطوط جدید در نسخه pretty تصادفاً به matcher مرزهای پایدار میدهند. حذفشان و LZ77 آن لنگرها را از دست میدهد: در این نمونه، فشردهسازی اصل pretty در gzip -1 به ۱,۵۰۸ بایت میرسد، در حالی که minify-سپس-فشردهسازی ۱,۵۱۸ بایت میدهد. ده بایت بدتر.
به ترتیب بزرگی، اهرمها اینگونه ردیف میشوند:
| اهرم | صرفهجویی در برابر سند pretty اصلی (سفارشهای تودرتو) | هزینه |
|---|---|---|
| minify | 46.3% (فقط بایت خام) | خوانایی رفته؛ گام ساخت لازم دارد |
| gzip -6 | 94.5% | کمی CPU سرور؛ پیکربندی تقریباً صفر |
| minify + gzip -6 | 95.4% | دو مورد بالا، جمعشده |
| minify + brotli | 96.9% | HTTPS و ماژول brotli لازم دارد؛ داراییهای ایستا را پیشفشرده کنید |
جهش از ۰ به ۹۴.۵٪ کار gzip است. جهش از ۹۴.۵٪ به ۹۵.۴٪ — نه دهم واحد — کار minify است. این جدول تعیین میکند تلاش شما کجا تعلق دارد.
۵. سطح فشردهسازی متغیر پنهان است
هر استدلالی که یک سطح gzip ثابت را فرض کند روی شن ایستاده، چون سطح مستقیماً ارزش حاشیهای minify را تغییر میدهد:
| تنظیم انتقال | بایت اضافهای که minify همچنان ذخیره میکند (سفارشهای تودرتو) |
|---|---|
gzip -1 (پیشفرض gzip_comp_level در Nginx) |
21.0% |
| gzip -6 (پیشفرض بیشتر کتابخانههای استاندارد) | 17.0% |
| gzip -9 | 8.9% |
| brotli (کیفیت پیشفرض) | 10.6% |
هر چه فشردهسازی انتقال ضعیفتر، minify بیشتر بهره میدهد. این قاعده عملی میدهد: روی نصب استاندارد Nginx که gzip_comp_level هنوز ۱ است، minify ۲۱٪ میدهد. وقتی به ۹ تنظیم کردید یا به brotli رفتید، به ۹ تا ۱۱٪ میرسد و معمولاً میتوانید ردش کنید.
پیش از بحث در هر جهت، اندازه بگیرید واقعاً در کدام لایه هستید:
# 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
سه عدد سه پرسش را یکجا پاسخ میدهند: آیا فشردهسازی حتی روشن است، در چه سطحی، و چه فضایی برای minify مانده. اگر identity و gzip تقریباً یکسان برگردند، فشردهسازی اصلاً فعال نیست — و چیزی که باید درست شود Nginx است، نه JSON شما:
gzip on;
gzip_comp_level 6;
gzip_min_length 256; # skip tiny responses, they only grow
gzip_types application/json application/javascript text/css;
۶. چه زمانی minify واقعاً بهره میدهد
یک آزمون تصمیم میگیرد: آیا بایتهایی که ذخیره کردید جایی ثبت میشوند، یا به محدودیتی میخورید که بایت خام را میشمارد؟ اگر بله، ۴۶٪ کامل مینشیند. اگر پاسخ «فقط روی سیم وجود دارد» است، ارزش واقعیاش ۱۷٪ است.
اول، بدنههای درخواست HTTP. بیشتر سرورها بدنه درخواست را decompress نمیکنند و gzip تقریباً منحصراً برای پاسخها پیکربندی میشود. JSONی که POST میکنید معمولاً هیچ محافظ لایه انتقالی ندارد، پس صرفهجویی minify با قیمت کامل میرسد. همین برای callbackهای webhook، تلهمتری MQTT و فریمهای WebSocket که هرگز permessage-deflate مذاکره نکردهاند صادق است.
دوم، کانالهایی با سقف بایت سخت — و هر یک از آنها بایت decodeشده را میشمارد:
| کانال | محدودیت | آیا gzip کمک میکند؟ |
|---|---|---|
| payload همگام AWS Lambda | 6 MB | نه، پس از decode اندازهگیری میشود |
| سند واحد Firestore | 1 MiB | نه |
| پیام واحد Kafka (پیشفرض) | 1 MB | نه، فشردهسازی سمت broker لایه دیگری است |
| سهمیه localStorage | ~5 MB (بر اساس نویسه) | نه |
| URL درخواست GET | وابسته به سرور، معمولاً 8 KB | نه |
در این دسته minify بهینهسازی نیست، تفاوت بین درخواستی است که ارسال میشود و درخواستی که شکست میخورد. جاسازی JSON در رشته query URL تیزترین مورد است: URLها هرگز فشرده نمیشوند و سقف طول دارند، پس اول minify کنید، سپس تصمیم بگیرید آیا اصلاً به base64 نیاز دارید.
سوم، JSON نوشتهشده روی دیسک یا لاگ. لاگها و ایندکسها بایت واقعی را بدون لایه انتقال ذخیره میکنند. یک هشدار مردم را به اشتباه میاندازد: minify فقط اگر از ابتدا JSON pretty مینوشتید کمک میکند. آن نمونه لاگ ۲۰۰ خطی فقط ۴.۱٪ ذخیره کرد، چون خطوطش از قبل فشرده بودند و تورفتگی تقریباً هیچ بخشی از فایل نبود. برعکس، اگر logger شما JSON.stringify(obj, null, 2) صدا میزند، حذف آرگومان سوم پول مجانی است.
یک نکته آخر: وقتی بایت خام واقعاً تنگ است، نام کلیدها معمولاً اهرم بزرگتری از تورفتگیاند. روی media-manifest.json، جایگزینی ۱۴ کلید منحصربهفردش با k0 تا k13 شکل minifyشده را از ۲۱,۲۲۲ بایت به ۱۸,۸۸۷ بایت میرساند — ۱۱.۰٪ دیگر. فشرده، آن برتری باقیمانده زیر gzip -6 به ۲.۴٪ و زیر brotli به ۱.۳٪ کوچک میشود. همان مکانیسم همه موارد بالا: تورفتگی و نام کلیدها هر دو اطلاعات ساختاری تکراریاند و فشردهساز در هر حالت همان سود را برمیدارد.
۷. چهار موردی که نتیجه معکوس میدهد
فایلهایی که کامیت میکنید را minify نکنید. "qty": 2 را به ۳ تغییر دهید و فایل pretty در git diff یک خط تمیز نشان میدهد؛ فایل minifyشده کل خط را بازنویسی میکند و با صدها فیلد اصلاً نمیتوانید تغییر را پیدا کنید. این همان مکانیسمی است که در چگونه تعارضهای ادغام JSON را حل کنیم پوشش داده شده — ابزارهای diff خطبهخط کار میکنند، پس JSON تکخطی هر ویرایش را به «کل خط تفاوت دارد» تبدیل میکند. فایلهای pretty را در مخزن نگه دارید و فقط در خروجی ساخت یا هنگام نوشتن در ذخیرهسازی minify کنید.
پاسخهای کوچک را فشرده نکنید. پاسخی فقط حاوی {"ok": true} پس از افزودن تورفتگی ۱۶ بایت است و پس از gzip -6 برابر ۳۶ بایت — بیست بایت بزرگتر، چون دیکشنری و سرآیندهای gzip خودشان جا اشغال میکنند. این نظری نیست؛ دقیقاً دلیل وجود gzip_min_length است. پیشفرض ۲۰ بایتی Nginx برای تولید بسیار پایین است، جایی که ۲۵۶ کف معمول است.
انتظار تجزیه سریعتر نداشته باشید. با مقیاس دادن همان مجموعه سفارش به ۴,۰۰۰ رکورد، سند pretty برابر ۲,۳۷۴,۳۷۸ بایت در برابر ۱,۲۸۲,۳۵۸ بایت minifyشده است — ۴۶٪ کوچکتر — با این حال میانه زمان JSON.parse فقط از ۵.۷ میلیثانیه به ۵.۰ میلیثانیه میرود، حدود ۱۲٪ سریعتر. نصف کردن اندازه زمان را نصف نمیکند، چون پارسر بیشتر بودجهاش را صرف برش رشتهها و تبدیل عددها میکند؛ رد کردن فضای سفید جنبی است. چیزی که واقعاً هزینه تجزیه را میراند تعداد کلید و محتوای رشته است، که تجزیه، رندر و جریان دادن فایلهای بزرگ JSON با جزئیات میشکافد.
آن را به سرویسی که از قبل brotli یا gzip -9 اجرا میکند ضمیمه نکنید. بهره به ۹ تا ۱۱٪ میافتد، در حالی که هزینه — از دست رفتن دائمی خوانایی هنگام عیبیابی — میماند. بهجایش بررسی کنید آیا gzip_types حتی application/json را پوشش میدهد. سایتهای زیادی gzip را فعال میکنند و فقط text/html را فشرده میکنند، و JSON کاملاً فشردهنشده میماند؛ در آن وضعیت minify گام اول نیست، درست کردن پیکربندی است.
۸. جدول تصمیم: اول کدام گام
| وضعیت شما | اول این را انجام دهید | جایگاه minify |
|---|---|---|
| هیچ فشردهسازی انتقالی | gzip_types در Nginx را پیکربندی کنید یا فشردهسازی خودکار CDN را فعال کنید |
ردش کنید؛ نه دهم بهره تبخیر میشود |
| gzip -1 (پیشفرض Nginx) | gzip_comp_level را به ۶ برسانید |
دوم، ارزش ۲۱٪ |
| از قبل روی brotli یا gzip -9 | کلیدهای بلند را حسابرسی کنید، صفحهبندی اضافه کنید یا فیلدها را کوتاه کنید | ردش کنید؛ فقط ۹ تا ۱۱٪ مانده |
| برخورد با ۶ مگابایت Lambda یا ۱ MiB Firestore | مستقیم minify کنید؛ ۴۶٪ کامل حساب میشود | اول |
| بدنههای POST، webhook، MQTT | مستقیم minify کنید؛ انتقال بهندرت اینجا اعمال میشود | اول |
| فایلهای پیکربندی که به Git میروند | pretty نگهشان دارید | فقط در خروجی ساخت |
| لاگ، تلهمتری، دامپهای آفلاین | آرگومان null, 2 را در logger حذف کنید |
همان گام اول است |
قاعده یکجملهای: ببینید میخواهید کدام دفتر بایت را کوچک کنید — دفتری که محلی ثبت میشود (دیسک، سهمیه، محدودیت پلتفرم، بدنه درخواست) یا دفتری که فقط روی سیم وجود دارد. محلی یعنی minify؛ فقط-سیم یعنی اول brotli یا gzip را روشن کنید.
در عمل، حلقه curl بالا را اجرا کنید تا تأیید شود فشردهسازی انتقال زنده است، سپس نمونهای را از ابزار minify JSON عبور دهید و رقم صرفهجوییاش را بخوانید. اگر آن عدد بالای ۴۰٪ است در حالی که curl برای identity و gzip اندازه تقریباً یکسان گزارش میکند، گلوگاه شما پیکربندی سرور است نه JSON. اگر دو عدد curl از قبل نزدیکاند، این سند چیز کمی برای دادن دارد و باید بهجایش فیلدها را کوتاه کنید یا صفحهبندی کنید — موضوع اصول طراحی قرارداد API در JSON.
آیا JSON minifyشده همچنان JSON معتبر است؟
بله. minify فقط فضای سفیدی را حذف میکند که RFC 8259 حول نویسههای ساختاری مجاز میداند و هرگز محتوای رشته را دست نمیزند، پس JSON.parse مقداری یکسان با اصل تولید میکند. تنها قربانی خوانایی انسانی است.
brotli یا gzip؟
روی همان نمونه minifyشده، brotli ۹۶.۹٪ و gzip -6 برابر ۹۵.۴٪ ذخیره میکند — شکاف باریک. برای پاسخهای پویا از gzip -6 استفاده کنید تا CPU پایین بماند، و brotli را برای داراییهای ایستای تحویلشده با HTTPS کنار بگذارید که میتوانید پیشفشرده به فایلهای .br کنید.
JSON را minify کردم اما API سریعتر نشد. چرا؟
پاسخ را با curl -sI برای سرآیند Content-Encoding: gzip بررسی کنید. بسیاری از تنظیمات Nginx gzip_types را فقط text/html میگذارند، پس JSON هرگز پوشش داده نشده — و کمتر از یک پنجم ۴۶٪ شما به سیم رسیده است.
آیا فایلهای پیکربندی باید minifyشده کامیت شوند؟
نه. ابزارهای diff خطبهخط مقایسه میکنند، پس JSON تکخطی هر ویرایش را به «کل خط تغییر کرد» تبدیل میکند و حل تعارض بسیار بیشتر از بایتهای ذخیرهشده هزینه دارد. نسخههای pretty را در مخزن نگه دارید و هنگام ساخت minify کنید.
پاسخ باید چه اندازه باشد تا gzip ارزشش را داشته باشد؟
آستانه معمول ۲۵۶ بایت تا ۱ کیلوبایت است. پاسخی به کوچکی {"ok": true} — ۱۶ بایت با تورفتگی — زیر gzip -6 به ۳۶ بایت میرسد، چون دیکشنری و سرآیندها بیشتر از آنچه بازیابی میکنند هزینه دارند. دقیقاً همان چیزی که gzip_min_length کنترل میکند.