הפסיקו לבצע דחיסת JSON בסדר הלא נכון: minify מול gzip מול brotli, נמדד
שורה תחתונה: מזעור חוסך 30-50% מה-JSON הגולמי, אבל gzip מוחק את רוב הרווח הזה. מספרים נמדדים על צורות אמיתיות, ועוד ארבעה מקרים שבהם מזעור דווקא מחמיר את המצב.

שורה תחתונה: הפרידו קודם בין דחיסת תעבורה לבין קיצוץ מבני. אם gzip או brotli עדיין לא פעילים, תקנו את תצורת השרת — gzip -6 לבדו מגיע ל-94.5% ומשאיר ל-minify רק 0.9 נקודות. ה-46.3% המלאים של minify מתממשים רק כשחשבון הבייטים נרשם מקומית: גופי בקשות, 6 MB של Lambda, 1 MiB של Firestore, או JSON שנכתב לדיסק וללוגים.
התג "חסכת 35%" על המזער האהוב עליכם הוא כנה - הוא מודד בייטים גולמיים. ה-API שלכם רץ מעל gzip או brotli, ושניהם אוכלים בדיוק את אותה יתירות. הכפילו את שני המספרים ותקבלו תשובה שגויה.
1. פצלו את "דחיסה" לשני דברים שונים
מה שאנשים קוראים לו דחיסת JSON הוא בעצם שתי פעולות לא קשורות, וערבובן הוא איך כל מסקנה שגויה מתחילה.
| minify (הסרת רווח לבן) | gzip / brotli (דחיסת תעבורה) | |
|---|---|---|
| טבע | טרנספורמציה של מסמך | טרנספורמציה של תעבורה |
| פלט | עדיין טקסט JSON חוקי וקריא | בינארי; לא שמיש עד פיענוח |
| היכן הרווח נוחת | דיסק, זיכרון, לוגים, מגבלות גודל | רק על הרשת ובמטמונים |
| אחרי פיענוח | הבייטים נשארים קטנים יותר | הבייטים חוזרים לגודל המקורי |
| מי אחראי | סקריפט הבנייה שלכם, ארגומנטי הסריאליזר | Nginx, CDN, מסגרת השרת |
השתיים האחרונות הן השורות החשובות. מגבלות פלטפורמה נמדדות אחרי פיענוח: תקרת ה-payload של 6 MB ב-AWS Lambda, תקרת 1 MiB לכל מסמך ב-Firestore. הפעלת gzip לא קונה שם אפילו בייט אחד, כי שתיהן סופרות את התוצאה המפוענחת.
2. minify מסיר בדיוק דבר אחד: רווח לבן מבני
RFC 8259 מתיר רווח לבן (space, \t, \n, \r) סביב כל תו מבני. שום דבר אחר במסמך JSON אינו אופציונלי: שמות מפתחות הם נתונים, מספרים שומרים כל ספרה, ותוכן מחרוזות הוא בייט-אחר-בייט בלתי נגיש.
זה הופך את כל הפעולה לשורה אחת:
const minified = JSON.stringify(JSON.parse(pretty));
// RFC 8259 מתיר רווח לבן רק סביב תווים מבניים,
// ולכן רווחים בתוך מחרוזת הם נתונים, לא פורמט
JSON.stringify(JSON.parse('{"note": "two spaces"}'));
// {"note":"two spaces"} - שני הרווחים הפנימיים נשמרים
שלוש פעולות שמוכנסות תחת "דחיסה" אבל בעצם משנות את המסמך ראויות לציון: השמטת מפתחות שערכם null, עיגול מספרים עשרוניים וסידור מחדש של מפתחות. הן משנות את המסמך ולא מפרמטות אותו מחדש, לכן כל כלי שמפרסם "דחיסה עמוקה" צריך את ברכת הצרכן שלכם לפני שמפעילים אותו.
מכיוון ש-minify מסיר רק רווח לבן, התמורה שלו תלויה כמעט לגמרי בכמה מהקובץ שלכם הוא הזחה. צפו שזה יופיע שוב ושוב בטבלאות שלהלן.
3. המדידה: ארבעה מסמכים, שלוש רמות gzip ועוד brotli
סביבה: Node v22.13.1, zlib.gzipSync ברמה 1 / 6 / 9, brotliCompressSync באיכות ברירת המחדל (11). ארבע הדגימות מכסות בכוונה את הצורות שאתם באמת שולחים: 50 הזמנות מקוננות (מבנה עמוק, מפתחות חוזרים), תצורה בת 6 רמות (מפתחות ארוכים, ערכים קצרים), מערך לוג של 200 שורות (מחרוזות ארוכות, כמעט ללא הזחה), ו-media-manifest.json האמיתי של האתר הזה (צורה מעורבת, מחרוזות עתירות URL).
צעד ראשון: בייטים גולמיים.
| דגימה | pretty (2 רווחים) | pretty (4 רווחים) | אחרי minify | minify חוסך |
|---|---|---|---|---|
| 50 הזמנות מקוננות | 29,551 B | 40,761 B | 15,881 B | 46.3% |
| תצורה בת 6 רמות | 25,450 B | 38,012 B | 11,679 B | 54.1% |
| מערך לוג של 200 שורות | 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% |
מכאן מגיע התג "חסכת 35%", והוא לא משקר. הבעיה מופיעה בצעד השני.
צעד שני: דחסו כל וריאנט, ואז שאלו כמה minify עדיין מוסיף.
| דגימה | מעל gzip -1 | מעל gzip -6 | מעל gzip -9 | מעל brotli |
|---|---|---|---|---|
| 50 הזמנות מקוננות | 21.0% | 17.0% | 8.9% | 10.6% |
| תצורה בת 6 רמות | 74.2% | 59.1% | 58.0% | 56.4% |
| מערך לוג של 200 שורות | -0.7% | -0.4% | 0.6% | -0.3% |
| media-manifest.json | 1.5% | 1.0% | 1.0% | 1.6% |
אותו מסמך הזמנות מקוננות שמאבד 46.3% מהבייטים הגולמיים שלו שומר רק 17.0% מזה כש-gzip -6 נכנס לתמונה. מערך הלוג הופך לשלילי לחלוטין: minify ואז דחיסה מעט גדול יותר מדחיסת ה-pretty המקורי. שלילי אינו באג, זה בדיוק מה שצריך לצפות, והסעיף הבא מסביר למה.
צעד שלישי: הפחתה כוללת מול מסמך ה-pretty המקורי - המספר שבאמת עובר על הרשת.
| דגימה | gzip -6 בלבד | minify + gzip -6 | minify + brotli |
|---|---|---|---|
| 50 הזמנות מקוננות | 94.5% | 95.4% | 96.9% |
| תצורה בת 6 רמות | 96.0% | 98.4% | 99.0% |
| מערך לוג של 200 שורות | 89.9% | 89.9% | 95.0% |
| media-manifest.json | 83.5% | 83.6% | 86.1% |
4. מדוע gzip אוכל את הקרדיט של minify
gzip הוא LZ77 ואחריו קידוד Huffman. המהלך המרכזי של LZ77 הוא "הסתכל אחורה בחלון של 32 KB, מצא רצף חוזר, החלף אותו במצביע (מרחק, אורך)". הזחה היא המטרה הקלה ביותר למהלך הזה: כל רמת קינון פולטת רצף זהה של רווחים, ועשרות שורות סמוכות חוזרות על אותו צירוף. לאחר מכן Huffman מקצה קודים קצרים לסמלים בתדירות גבוהה, כך שדפוס כמו \n ועוד שמונה רווחים - כזה שעשוי לחזור אלף פעמים בקובץ אחד - עולה שניים או שלושה ביטים.
לכן הבייטים ש-minify מוחק הם בדיוק הבייטים שהמדחס היה מכווץ כמעט בחינם. לעשות את זה בעצמכם פירושו לחזור על עבודה שהמדחס כבר מצטיין בה, וה-46% שלכם מתמוטט ל-17%. זה כל הסיפור מאחורי הטבלה שלמעלה.
התוצאה השלילית של מערך הלוג ראויה להסבר משלה. הדגימה הזו מורכבת כמעט כולה ממחרוזות ארוכות (שורות לוג, URLs, base64), מה שאומר מעט חזרות ואנטרופיה גבוהה - מעט יתירות שהמדחס יכול להסיר מלכתחילה. תווי השורה בגרסת ה-pretty מעניקים למתאים גבולות יציבים. הסירו אותם ו-LZ77 מאבד את העוגנים האלה: בדגימה הזו, דחיסת ה-pretty המקורי ב-gzip -1 מניבה 1,508 בייטים, בעוד minify ואז דחיסה מניב 1,518 בייטים. עשרה בייטים גרוע יותר.
מדורגים לפי גודל, המנופים מסתדרים כך:
| מנוף | נחסך מול מסמך ה-pretty המקורי (הזמנות מקוננות) | עלות |
|---|---|---|
| minify | 46.3% (בייטים גולמיים בלבד) | הקריאות אבדה; דורש שלב בנייה |
| gzip -6 | 94.5% | קצת CPU של שרת; תצורה כמעט אפסית |
| minify + gzip -6 | 95.4% | שני הנ"ל, מחוברים |
| minify + brotli | 96.9% | דורש HTTPS ומודול brotli; דחיסה מוקדמת של נכסים סטטיים |
הקפיצה מ-0 ל-94.5% היא העבודה של gzip. הקפיצה מ-94.5% ל-95.4% - תשע עשיריות נקודה - היא של minify. הטבלה הזו מכריעה היכן המאמץ שלכם שייך.
5. רמת הדחיסה היא המשתנה הנסתר
כל טיעון שמניח רמת 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 עדיין 1, minify מעניק לכם 21%. אחרי כיוונון ל-9 או מעבר ל-brotli, זה יורד ל-9-11% ובדרך כלל אפשר לדלג עליו.
לפני שאתם מתווכחים לכאן או לכאן, מדדו באיזו מדרגה אתם באמת נמצאים:
# שליפת אותה תשובה עם שלושה ערכי Accept-Encoding והשוואה של מה שהגיע
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; # דלגו על תשובות קטנות, הן רק גדלות
gzip_types application/json application/javascript text/css;
6. מתי minify באמת משתלם
מבחן אחד מכריע: האם הבייטים שחסכתם יירשם אי-שם, או שתיתקלו במגבלה שסופרת בייטים גולמיים? אם כן, ה-46% המלאים מתממשים. אם התשובה היא "זה קיים רק על הרשת", אז 17% הוא הערך האמיתי שלו.
ראשית, גופי בקשות HTTP. רוב השרתים לא מפענחים גופי בקשות, ו-gzip מוגדר כמעט אך ורק לתשובות. ל-JSON שאתם שולחים ב-POST אין בדרך כלל שום הגנת שכבת תעבורה, ולכן החסכון של minify מגיע במחיר מלא. כך גם בקולבקים של webhook, בטלמטריית MQTT ובמסגרות WebSocket שלא ניהלו משא ומתן על permessage-deflate.
שנית, ערוצים עם תקרת בייטים קשיחה - וכולם סופרים בייטים מפוענחים:
| ערוץ | מגבלה | האם gzip עוזר? |
|---|---|---|
| AWS Lambda עומס סינכרוני | 6 MB | לא, נמדד אחרי פיענוח |
| מסמך בודד ב-Firestore | 1 MiB | לא |
| הודעה בודדת ב-Kafka (ברירת מחדל) | 1 MB | לא, דחיסה בצד ה-broker היא שכבה אחרת |
| מכסת localStorage | ~5 MB (נספר בתווים) | לא |
| כתובת בקשה GET | תלוי שרת, 8 KB נפוץ | לא |
בקטגוריה הזו minify אינו אופטימיזציה, הוא ההבדל בין בקשה שנשלחת לבין בקשה שנכשלת. הטמעת JSON במחרוזת שאילתה של URL היא המקרה החד ביותר: URLs לעולם לא נדחסים ויש להם תקרת אורך, לכן מזערו קודם, ואז החליטו אם אתם בכלל צריכים base64.
שלישית, JSON שנכתב לדיסק או ללוגים. לוגים ואינדקסים שומרים בייטים מילוליים בלי שכבת תעבורה מעורבת. יש הסתייגות שמכשילה אנשים: minify עוזר רק אם מלכתחילה כתבתם JSON בפורמט יפה. דגימת הלוג של 200 השורות חסכה רק 4.1%, כי השורות שלה היו כבר קומפקטיות וההזחה היוותה כמעט כלום מהקובץ. לעומת זאת, אם הלוגר שלכם קורא ל-JSON.stringify(obj, null, 2), השמטת הארגומנט השלישי היא כסף חינם.
הערה אחרונה: כשבייטים גולמיים באמת מתוחים, שמות מפתחות הם בדרך כלל מנוף גדול יותר מהזחה. ב-media-manifest.json, החלפת 14 המפתחות הייחודיים שלו ב-k0 עד k13 מורידה את הצורה הממוזערת מ-21,222 B ל-18,887 B - עוד 11.0%. בדחיסה, היתרון הנותר הזה מתכווץ ל-2.4% תחת gzip -6 ול-1.3% תחת brotli. אותו מנגנון כמו כל מה שלמעלה: הזחה ושמות מפתחות הם שניהם מידע מבני חוזר, והמדחס גובה את אותו דיבידנד בכל מקרה.
7. ארבעה מקרים שבהם זה מתפוצץ בפנים
אל תמזערו קבצים שאתם מבצעים להם commit. שנו את "qty": 2 ל-3 והקובץ ה-pretty מציג שורה אחת נקייה ב-git diff; הקובץ הממוזער משכתב את כל השורה, ועם מאות שדות לא תמצאו את השינוי בכלל. זו אותה מכניקה שנדונה בכיצד לפתור התנגשויות מיזוג ב-JSON - כלי diff פועלים שורה-שורה, ולכן JSON בשורה אחת הופך כל עריכה ל"השורה כולה שונה". שמרו קבצים בפורמט יפה במאגר ומזערו רק בפלט הבנייה או בכתיבה לאחסון.
אל תדחסו תשובות זעירות. תשובה שמכילה רק {"ok": true} היא 16 בייטים אחרי תוספת הזחה, ו-36 בייטים אחרי gzip -6 - עשרים בייטים גדול יותר, כי המילון והכותרות של gzip עצמו תופסים מקום. זה לא תיאורטי; בדיוק בשביל זה קיים gzip_min_length. ברירת המחדל שלו ב-Nginx, 20 בייטים, נמוכה מדי לפרודקשן, שבו 256 הוא הרצפה הרגילה.
אל תצפו לניתוח מהיר יותר. בהגדלת אותה קבוצת הזמנות ל-4,000 רשומות, המסמך ה-pretty הוא 2,374,378 בייטים מול 1,282,358 ממוזערים - קטן ב-46% - ובכל זאת זמן ה-JSON.parse החציוני רק עובר מ-5.7 ms ל-5.0 ms, בערך 12% מהיר יותר. חציית הגודל לא חוצה את הזמן, כי הפרסר מבזבז את רוב התקציב שלו על חיתוך מחרוזות והמרת מספרים; דילוג על רווח לבן הוא עניין שולי. מה שבאמת מניע את עלות הניתוח הוא מספר המפתחות ותוכן המחרוזות, וזה מפורט בניתוח, רינדור והזרמה של קבצי JSON גדולים.
אל תתאימו את זה בדיעבד לשירות שכבר רץ על brotli או gzip -9. התמורה יורדת ל-9-11%, בעוד העלות - אובדן קבוע של קריאות בזמן דיבוג - נשארת. בדקו במקום זאת אם gzip_types בכלל מכסה application/json. אתרים רבים מפעילים gzip ודוחסים רק text/html, ומשאירים את ה-JSON לגמרי לא דחוס; במצב כזה minify אינו הצעד הראשון, תיקון התצורה הוא.
8. טבלת החלטה: איזה צעד קודם
| המצב שלכם | עשו זאת קודם | היכן minify מדורג |
|---|---|---|
| אין דחיסת תעבורה בכלל | הגדירו gzip_types ב-Nginx או הפעילו דחיסה אוטומטית ב-CDN |
דלגו עליו; תשע עשיריות מהרווח מתאדות |
| רצים על gzip -1 (ברירת מחדל של Nginx) | העלו את gzip_comp_level ל-6 |
שני, שווה 21% |
| כבר על brotli או gzip -9 | בדקו מפתחות ארוכים, הוסיפו עימוד או קצצו שדות | דלגו עליו; נשארו רק 9-11% |
| פוגעים ב-6 MB של Lambda או ב-1 MiB של Firestore | מזערו ישירות; ה-46% המלאים נספרים | ראשון |
| גופי POST, webhooks, MQTT | מזערו ישירות; תעבורה כמעט לא חלה כאן | ראשון |
| קבצי תצורה שנכנסים ל-Git | שמרו אותם יפים | רק בפלט הבנייה |
| לוגים, טלמטריה, ייצוא נתונים לא מקוון | הסירו את הארגומנט null, 2 בלוגר |
זה הוא הצעד הראשון |
הכלל במשפט אחד: הסתכלו על איזה פנקס בייטים אתם מנסים לכווץ - כזה שנרשם מקומית (דיסק, מכסה, מגבלת פלטפורמה, גוף בקשה) או כזה שקיים רק על הרשת. מקומי קודם אומר minify; רק-רשת אומר הפעילו קודם brotli או gzip.
בפועל, הריצו את לולאת ה-curl שלמעלה כדי לוודא שדחיסת התעבורה פעילה, ואז העבירו דגימה דרך כלי ה-JSON Minify וקראו את נתון החסכון שלו. אם המספר הזה מעל 40% בעוד curl מדווח ש-identity ו-gzip כמעט באותו גודל, צוואר הבקבוק שלכם הוא תצורת השרת, לא ה-JSON. אם שני מספרי ה-curl כבר קרובים, למסמך הזה לא נשאר הרבה לתת וכדאי לקצץ שדות או להוסיף עימוד במקום - הנושא של יסודות תכנון חוזה JSON API.
האם JSON ממוזער עדיין חוקי?
כן. minify מסיר רק את הרווח הלבן ש-RFC 8259 מתיר סביב תווים מבניים ולעולם לא נוגע בתוכן מחרוזות, ולכן JSON.parse מפיק ערך זהה למקור. הקורבן היחיד הוא הקריאות האנושית.
brotli או gzip?
על אותה דגימה ממוזערת, brotli חוסך 96.9% ו-gzip -6 חוסך 95.4% - פער צר. השתמשו ב-gzip -6 לתשובות דינמיות כדי לשמור על CPU נמוך, ושמרו את brotli לנכסים סטטיים שמשודרים ב-HTTPS ושאפשר לדחוס מראש לקובצי .br.
מיזעתי את ה-JSON אבל ה-API לא נהיה מהיר יותר. למה?
בדקו את התשובה עם curl -sI לחיפוש כותרת Content-Encoding: gzip. התקנות Nginx רבות משאירות את gzip_types כברירת מחדל על text/html בלבד, כך שה-JSON מעולם לא כוסה - ופחות מחמישית מה-46% שלכם הגיע אי-פעם לרשת.
האם קבצי תצורה צריכים להיכנס ל-commit ממוזערים?
לא. כלי diff משווים שורה-שורה, ולכן JSON בשורה אחת הופך כל עריכה ל"השורה כולה השתנתה", ופתרון התנגשויות עולה הרבה יותר מהבייטים שחסכתם. שמרו גרסאות יפות במאגר ומזערו בזמן הבנייה.
כמה גדולה צריכה להיות תשובה כדי ש-gzip ישתלם?
הסף הרגיל הוא 256 בייטים עד 1 KB. תשובה קטנה כמו {"ok": true} - 16 בייטים עם הזחה - גדלה ל-36 בייטים תחת gzip -6, כי המילון והכותרות עולים יותר ממה שהם מחזירים. בדיוק את זה שולט gzip_min_length.