JSON בינארי: בחירה בין MessagePack, CBOR ו-BSON
שורה תחתונה: MessagePack, CBOR ו-BSON כולם דוחסים JSON לבתים. השוו עלות בתים, טיפול ב-int64 ובינארי, וקבלו קו ברור מתי החלפה שווה.

שורה תחתונה: שמרו JSON טקסטואלי להחלפה חיצונית ולנתונים שאנשים צריכים לבדוק, ועברו ל-MessagePack או CBOR בקישורים פנימיים רק כשמטענים בינאריים, מספרים שלמים מעבר ל-2^53 או עשרוניים מדויקים הופכים את הגודל הדחוס או את כושר הביטוי של הטיפוסים לבעיה אמיתית; BSON שייך לאקוסיסטם של MongoDB.
JSON טקסטואלי חוסך רווחים; פורמטים בינאריים חוסכים את הפיסוק ואת הצורה הטקסטואלית של מספרים. שמות שדות עדיין מופיעים פעם אחת בכל רשומה, ולכן חשבו את החיסכון האמיתי לפני המעבר.
1. מה JSON בינארי באמת חוסך
קבעו ציפיות קודם. אף אחד מהפורמטים האלה אינו "עוד תחביר JSON". הם מקודדים את אותו מודל נתונים בבתים: אובייקטים, מערכים, מחרוזות, מספרים, בוליאנים ו-null. מה שהם מסירים הוא קונקרטי:
- תווים מבניים כמו מרכאות, נקודותיים, פסיקים וסוגריים;
- הצורה העשרונית הטקסטואלית של מספרים, כך ש-
32הופך לבית בודד; - סריקה חוזרת אחר גבולות מחרוזת, שהוחלפה בקידומת באורך קבוע;
- קידוד Base64 למטענים בינאריים, שאפשר לאחסן ישירות כבתים.
מה שהם לא יכולים להסיר הוא באותה מידה קונקרטי: שמות שדות עדיין מופיעים כמחרוזות בכל רשומה. מפת MessagePack, מפת CBOR ומסמך BSON כולם חוזרים על המפתח בכל רשומה. לכן כשה-JSON שלכם הוא עשרת אלפים רשומות בעלות אותה צורה, פער הגודל הדחוס בין טקסט לבינארי קטן בהרבה ממה שהאינטואיציה מרמזת, כי gzip הוא הטוב ביותר בחיסול שמות שדות חוזרים.
2. מהיכן מגיעים שלושת הפורמטים
| פורמט | מפרט ושנה | מקור | שימוש טיפוסי |
|---|---|---|---|
| MessagePack | מפרט msgpack.org, מאז 2008 | עיצוב עצמאי שהתמקד במינימליזם | ערכי מטמון, IPC, צינורות לוגים |
| CBOR | RFC 8949 (STD 94), 2020 | הושפע מ-MessagePack, תוקנן ב-IETF | COSE, WebAuthn, CWT, התקנים מוגבלים |
| BSON | bsonspec.org, אקוסיסטם MongoDB | תוכנן למסמכי בסיס נתונים | אחסון ופרוטוקול רשת של MongoDB |
CBOR הוא הסטנדרטי ביותר: סוג מדיה application/cbor רשום ב-IANA, כללי קידוד דטרמיניסטיים ו-CDDL לתיאור. MessagePack הוא הקליל ביותר, עם מפרט שנכנס לעמוד אחד. BSON הוא הכבד ביותר: הוא משלב ObjectId, Decimal128, תאריכים ותתי-טיפוס בינאריים במערכת הטיפוסים שלו, ולכן הוא פופולרי באמת רק בתוך האקוסיסטם של MongoDB.
3. אותם נתונים, ארבעה קידודים
קחו {"name":"joe"} כדוגמה. עלויות הבתים קלות להשוואה:
| קידוד | בתים | רצף בתים |
|---|---|---|
| JSON טקסטואלי | 14 | 7b 22 6e 61 6d 65 22 3a 22 6a 6f 65 22 7d |
| MessagePack | 9 | 81 a3 6e 61 6d 65 a3 6a 6f 65 |
| CBOR | 10 | a1 64 6e 61 6d 65 63 6a 6f 65 |
| BSON | 19 | אורך כולל של 4 בתים + בית טיפוס + name\0 + אורך ערך של 4 בתים + joe\0 + מסיים |
פרקו את תשעת הבתים של MessagePack:
81: הניבל הגבוה1000אומר map, הניבל הנמוך0001אומר זוג מפתח-ערך אחד;a3: שלושת הביטים הגבוהים101אומרים מחרוזת באורך קבוע של 3;6e 61 6d 65: ASCII שלname;a3: מחרוזת נוספת באורך 3;6a 6f 65:joe.
ה-a1 של CBOR הוא סוג ראשי 5 (map) עם אורך 1, וה-64 הוא סוג ראשי 3 (מחרוזת טקסט) עם אורך 4, כך שהסמנטיקה כמעט איזומורפית ל-MessagePack. BSON גדול יותר: כל מסמך נושא אורך כולל של 4 בתים, כל שדה חוזר על בית טיפוס ועל שמו, וכל מחרוזת חוזרת על אורך בתוספת \0 בסוף. במסמכים קטנים, BSON יכול בקלות להיות גדול יותר מ-JSON טקסטואלי.
אפשר לבדוק את הקידומות ישירות על ידי הצגת הבתים האלה כ-hex:
const messagePackBytes = [0x81, 0xa3, 0x6e, 0x61, 0x6d, 0x65, 0xa3, 0x6a, 0x6f, 0x65];
const hexText = messagePackBytes.map((byte) => byte.toString(16)).join(' ');
console.log(hexText); // 81 a3 6e 61 6d 65 a3 6a 6f 65
3.1 גודל ומהירות פרסור: התעלמו ממספרי השיווק
טענות שבינארי "מהיר פי 10" צריכות להתפצל לשתי שאלות.
גודל. ללא דחיסה, בינארי קטן יותר כי הפיסוק והרווחים נעלמו. אבל צינורות אמיתיים כמעט תמיד דוחסים: אחרי gzip או brotli, שמות שדות חוזרים נדחסים, ויתרון הבינארי מתכווץ דרמטית. פעלו לפי השיטה ב-מזעור JSON לעומת gzip: מדדו קודם בתים דחוסים, ואז החליטו אם החלפת פורמט שווה.
מהירות פרסור. בינארי מדלג על לקסינג (סריקת מרכאות, פסיקים ותווי מספרים), ולכן אמור להיות מהיר יותר בתיאוריה. ב-JavaScript, לעומת זאת, העלות הדומיננטית היא הקצאת אובייקטים וזיכרון, שאין מפענח שיכול להימנע ממנה. צורת הנתונים, מספר השדות ואיכות המימוש כולם משנים, ולכן המסקנה האמינה היחידה מגיעה מהרצת benchmark על המטענים שלכם ולא מציטוט מכפיל של ספק. אם מטען בודד גדול מדי כדי להיקרא לזיכרון בבת אחת, חתכו אותו בגישה המתוארת ב-פרסור וזרימה של JSON גדול לפני מדידת רווח הקידוד.
4. שדות int64 ובינאריים: הסיבה האמיתית להחליף
גודל הוא לעתים קרובות לא הסיבה לשנות פורמט. כושר הביטוי של הטיפוסים הוא.
4.1 מספרים שלמים מעבר ל-2^53
מספרי JSON נוחתים כ-double ב-JavaScript, כך ש-9007199254740993 הופך ל-9007199254740992. שלושת הפורמטים הבינאריים מטפלים בזה אחרת:
| פורמט | מספרים שלמים של 64 ביט | 64 ביט ללא סימן | דיוק שרירותי |
|---|---|---|---|
| MessagePack | ✓ int64 (0xd3), ניתן לפענוח ל-BigInt |
✓ uint64 (0xcf) |
✗ (דורש סוג הרחבה) |
| CBOR | ✓ צורות 64 ביט של סוג ראשי 0/1 | ✓ אותו מרחב קידוד | ✓ תגי bignum 2/3 |
| BSON | ✓ int64 (0x12) |
✗ עם סימן בלבד | ✗ (Decimal128 הוא נקודה קבועה, לא מספר שלם שרירותי) |
CBOR הוא היחיד עם ייצוג סטנדרטי למספרים שלמים בדיוק שרירותי: ערכים מעבר ל-64 ביט נעטפים בתג 2 (לא שלילי) או תג 3 (שלילי) סביב מחרוזת בתים. MessagePack ו-BSON נופלים חזרה למחרוזות או למוסכמות הרחבה ייעודיות.
4.2 מטענים בינאריים
ל-JSON יש בדיוק דרך אחת לבטא בינארי: Base64, שמנפח את המטען בכשליש בערך ודורש עותק פענוח נוסף. ל-MessagePack יש טיפוסי bin (0xc4–0xc6), ל-CBOR יש מחרוזות בתים (סוג ראשי 2), ול-BSON יש תתי-טיפוס בינאריים. כשמעבירים תמונות, hashes, וקטורים או בלוקים דחוסים, ההבדל הזה חשוב הרבה יותר מחיסכון של כמה בתי פיסוק.
5. השוואה צד-לצד
| ממד | MessagePack | CBOR | BSON |
|---|---|---|---|
| מפרט | מפרט קהילתי | IETF RFC 8949 | אקוסיסטם MongoDB |
| סוג מדיה | application/msgpack (לא סטנדרטי ב-IANA) |
application/cbor (רשום ב-IANA) |
אין סוג מדיה כללי |
| מערכת טיפוסים | שישה טיפוסי JSON בתוספת bin ו-ext | שישה טיפוסי JSON בתוספת bin, תגים וערכים פשוטים | רחבה יותר: ObjectId, Decimal128, תאריכים, regex, חותמות זמן |
| מנגנון הרחבה | סוגי הרחבה (למשל timestamp -1) | תגים (תאריכים, bignums, CBOR מוטבע) | קודי טיפוס קבועים, הרחבות לפי מוסכמת האקוסיסטם |
| קידוד דטרמיניסטי | אין דרישה סטנדרטית | כללים מפורשים, שמישים לחתימות | אין |
| מתאר את עצמו | כן, אבל בלי סכימה ברמת שדה | כן, וניתן לתיאור עם CDDL | כן, עם שמות שדות חוזרים בכל מסמך |
| תקורת מסמך קטן | נמוכה | נמוכה | גבוהה (קידומת אורך, שמות שדות, מסיים) |
| אקוסיסטם | מטמונים, לוגים, חלק מ-RPC | COSE, WebAuthn, IoT | MongoDB בלבד |
6. תמיכה בדפדפן וב-Node
Node 24 אינו כולל אף אחד מהקודקים האלה, ולכן מתקינים ספרייה. אפשרויות נפוצות:
| סביבת הרצה | MessagePack | CBOR | BSON |
|---|---|---|---|
| Node.js | @msgpack/msgpack, msgpackr |
cbor-x, borc |
bson |
| דפדפן | @msgpack/msgpack |
build דפדפן של cbor-x, cbor-web |
bson |
שתי המשפחות עובדות על Uint8Array ואינן דורשות Buffer. שימו לב להבדלי גרסאות:
import { encode, decode } from '@msgpack/msgpack';
/**
* Encodes an object to MessagePack bytes and decodes it immediately for verification.
* @param payload Plain object to encode
* @returns The decoded value and the encoded byte length
*/
function roundTrip(payload: Record<string, unknown>): { decoded: unknown; byteLength: number } {
const bytes = encode(payload);
return { decoded: decode(bytes), byteLength: bytes.byteLength };
}
רוב הספריות מפענחות מספרים שלמים גדולים ל-number כברירת מחדל ומאבדות דיוק מעבר לטווח הבטוח. אם הערכים המדויקים חשובים, הפעילו את אפשרות BigInt במפורש (useBigInt64 ב-@msgpack/msgpack, מתג שקול ב-cbor-x), אחרת רק העברתם את הבעיה הישנה של JSON לפורמט חדש.
7. יכולת דיבוג ותאימות פרוטוקול
7.1 יכולת דיבוג: העלות הנסתרת הגדולה ביותר
היתרון הגדול של JSON טקסטואלי אינו הגודל; הוא שאפשר לפתוח אותו בכל רגע. אחרי המעבר לבינארי:
grep,awkו-jqמפסיקים לעבוד, וצינורות לוגים צריכים שלב פענוח נוסף;- דיבוג production דורש את המפענח הנכון בגרסה הנכונה, אחרת שמות השדות שתראו יהיו שגויים;
- תעבורה שנלכדה מופיעה בכלי המפתחים של הדפדפן כבלוב בתים בלתי קריא;
- diff-ים בסקירת קוד מציגים מחרוזת hex.
ל-CBOR יש notation אבחוני, שמציג בתים חזרה כטקסט קריא הקרוב ל-JSON, למשל map כ-{1: 2}; גם ל-MessagePack יש כמה מפענחי שורת פקודה. אבל כולם דורשים התקנה נוספת ונמצאים מחוץ למסלול הפעולות הסטנדרטי.
המסקנה: בכל פעם שאנשים צריכים לבדוק את הנתונים לעתים קרובות, העלות הנסתרת של פורמט בינארי עולה על רוחב הפס שהוא חוסך.
7.2 תאימות פרוטוקול והרחבות
- ל-CBOR יש מנגנון התגים השלם ביותר: תג 0 למחרוזת תאריך-שעה, תג 1 לחותמת זמן Unix, תגים 2 ו-3 ל-bignums, ותג 24 ל-CBOR מוטבע. בשילוב עם קידוד דטרמיניסטי, הוא משמש פרוטוקולים שצריכים חתימות ועקביות בין מימושים, כמו COSE, WebAuthn ו-CWT.
- MessagePack משאיר סוגי הרחבה למיישמים. בפועל היחיד הנפוץ הוא timestamp (סוג הרחבה -1). בין צוותים, כל סוג הרחבה חייב להיכתב במסמך הפרוטוקול, אחרת הצד השני לא יוכל לפענח.
- ל-BSON יש קודי טיפוס קבועים, ו-
ObjectId,Decimal128,MinKeyו-MaxKeyהם סמנטיקה ייחודית ל-MongoDB. התייחסות ל-BSON כפורמט החלפה כללי מביאה ערמה של טיפוסים שאינם בשימוש וקידומות אורך שאינן בשימוש.
אם פרוטוקול דורש "אותם נתונים חייבים לייצר אותם בתים", רק CBOR מספק תשובה סטנדרטית; MessagePack ו-BSON צריכים שניהם הסכמה נוספת על סדר שדות ופרטי קידוד.
8. מתי שווה להחליף
אותות שמצדיקים החלפה:
- המטענים גדולים, שני הקצוות יכולים להשתנות, והגדלים הדחוסים עדיין שונים באופן משמעותי;
- מעבירים תוכן בינארי והניפוח ועלות הפענוח של Base64 מדידים;
- צריך מספרים שלמים של 64 ביט או עשרוניים מדויקים שה-double של JSON לא יכול לייצג;
- הקישור הוא שרת-לשרת וכלי אבחון יכולים להיות מתוקננים.
אותות שמתנגדים לזה:
- gzip או brotli כבר קיימים והרווח הוא אחוז חד-ספרתי;
- ה-API ציבורי ולצדדים שלישיים יש רק לקוחות HTTP כלליים;
- הנתונים הם לוגים או הגדרות שאנשים קוראים ביד;
- אין לצוות תקציב לתחזוקת מפענחים ותאימות גרסאות.
גם מסלול אמצע עובד: שמרו JSON כלפי חוץ והשתמשו ב-MessagePack למטמונים פנימיים. ערכי מטמון נקראים רק על ידי השירותים שלכם, כך שגם הגודל וגם עלות הפרסור משתפרים, וניקוי המטמון פותר כל בעיה בלי לגעת בחוזה.
9. רשימת בדיקה
- [ ] השוו כמויות בתים דחוסות מנתונים אמיתיים, לא כמויות לא דחוסות;
- [ ] החליטו את מדיניות המספרים הגדולים: אפשרות BigInt, העברה כמחרוזת או bignums של CBOR;
- [ ] שלחו שדות בינאריים כ-bin או כמחרוזות בתים במקום לעטוף אותם ב-Base64;
- [ ] כתבו סוגי הרחבה ותגים במסמך הפרוטוקול והצמידו גרסאות מפענחים;
- [ ] שמרו נתיב דיבוג של JSON טקסטואלי שאפשר להפעיל בתקלות production;
- [ ] הריצו benchmark ותעדו מספרים שניתנים לשחזור במקום טענות מילוליות.
מה ההבדל בין MessagePack ל-CBOR?
הקידודים שלהם דומים מאוד: מפות, מערכים ומחרוזות משתמשים כולם בקידומת קומפקטית של טיפוס ואורך. ההבדל העיקרי הוא תיקון. CBOR הוא RFC של IETF עם סוג מדיה ב-IANA, מנגנון תגים וכללי קידוד דטרמיניסטיים; MessagePack הוא מפרט קהילתי שסוגי ההרחבה שלו מסתמכים על הסכמות פרטיות. בחרו CBOR כשצריך חתימות, עקביות בין מימושים או פרוטוקולים להתקנים מוגבלים.
האם BSON יכול להחליף את JSON כפורמט כללי?
לא מומלץ. BSON תוכנן למסמכי בסיס נתונים: כל מסמך נושא אורך כולל של 4 בתים, כל שדה חוזר על הטיפוס והשם שלו, ומסמכים קטנים בסופו של דבר גדולים יותר מ-JSON טקסטואלי. הוא גם כולל טיפוסים ייחודיים ל-MongoDB כמו ObjectId ו-MinKey. מחוץ ל-MongoDB, רוב מערכת הטיפוסים שלו אינה בשימוש בעוד קנס הגודל נשאר.
בכמה פורמט בינארי יקטין את המטען שלי?
זה תלוי בצורת הנתונים. כששמות שדות חוזרים הרבה, JSON טקסטואלי דחוס ופורמטים בינאריים מתקרבים זה לזה; כשהשמות קצרים והנתונים מלאים בתווים מבניים ובמספרים, יתרון הבינארי ברור יותר. מדדו דגימות אמיתיות גם ללא דחיסה וגם אחרי gzip, ואז החליטו, במקום להעריך עם מכפיל גנרי.
אפשר עדיין לדבג נתונים בינאריים עם jq?
לא. jq מעבד רק JSON טקסטואלי וייכשל מיד על בתים של MessagePack או CBOR. חייבים לפענח עם הספרייה המתאימה ל-JSON קודם ואז להעביר ל-jq; CBOR יכול גם לעבור דרך כלי notation אבחוני כדי להפוך לטקסט קריא. שמירה על נתיב דיבוג טקסטואלי זולה יותר מהתקנת כלים לפי דרישה.