Binarny JSON: wybór między MessagePack, CBOR i BSON
Podsumowanie: MessagePack, CBOR i BSON kompresują JSON do bajtów. Porównaj koszt bajtowy, obsługę int64 i danych binarnych oraz zobacz wyraźną linię, kiedy przejście ma sens.

Najważniejsze: trzymaj tekstowy JSON do wymiany zewnętrznej i do danych, które ludzie muszą obejrzeć, a na łączach wewnętrznych przechodź na MessagePack lub CBOR tylko wtedy, gdy ładunki binarne, liczby całkowite powyżej 2^53 lub dokładne ułamki dziesiętne czynią skompresowany rozmiar albo siłę wyrazu typów realnym problemem; BSON należy do ekosystemu MongoDB.
Tekstowy JSON oszczędza białe znaki; formaty binarne oszczędzają interpunkcję i tekstową postać liczb. Nazwy pól nadal występują raz per rekord, więc przed przejściem oblicz realną oszczędność.
1. Co naprawdę oszczędza binarny JSON
Najpierw ustal oczekiwania. Żaden z tych formatów nie jest „inną składnią JSON”. Kodują ten sam model danych w bajtach: obiekty, tablice, łańcuchy, liczby, wartości logiczne i null. To, co usuwają, jest konkretne:
- Znaki strukturalne, takie jak cudzysłowy, dwukropki, przecinki i nawiasy;
- Dziesiętną postać tekstową liczb, więc
32staje się pojedynczym bajtem; - Ponowne skanowanie granic łańcuchów, zastąpione prefiksem o stałej długości;
- Kodowanie Base64 dla ładunków binarnych, które można przechowywać bezpośrednio jako bajty.
To, czego nie potrafią usunąć, jest równie konkretne: nazwy pól nadal występują jako łańcuchy w każdym rekordzie. Mapa MessagePack, mapa CBOR i dokument BSON powtarzają klucz przy każdym wpisie. Więc gdy twoje JSON to dziesięć tysięcy rekordów o tym samym kształcie, różnica skompresowanego rozmiaru między tekstem a binariem jest znacznie mniejsza, niż podpowiada intuicja, bo gzip jest najlepszy w eliminowaniu powtarzających się nazw pól.
2. Skąd pochodzą trzy formaty
| Format | Specyfikacja i rok | Pochodzenie | Typowe użycie |
|---|---|---|---|
| MessagePack | spec msgpack.org, od 2008 | Niezależny projekt skupiony na minimalizmie | Wartości cache, IPC, potoki logów |
| CBOR | RFC 8949 (STD 94), 2020 | Zainspirowany MessagePack, ustandaryzowany przez IETF | COSE, WebAuthn, CWT, urządzenia o ograniczonych zasobach |
| BSON | bsonspec.org, ekosystem MongoDB | Zaprojektowany dla dokumentów bazodanowych | Przechowywanie i protokół sieciowy MongoDB |
CBOR jest najlepiej ustandaryzowany: typ nośny application/cbor zarejestrowany w IANA, deterministyczne reguły kodowania i CDDL do opisu. MessagePack jest najlżejszy, ze specyfikacją mieszczącą się na jednej stronie. BSON jest najcięższy: wpisuje ObjectId, Decimal128, daty i podtypy binarne w swój system typów, dlatego jest naprawdę popularny tylko w ekosystemie MongoDB.
3. Te same dane, cztery kodowania
Weźmy {"name":"joe"} jako przykład. Koszty bajtowe są łatwe do porównania:
| Kodowanie | Bajty | Sekwencja bajtów |
|---|---|---|
| Tekst 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-bajtowa długość całkowita + bajt typu + name\0 + 4-bajtowa długość wartości + joe\0 + terminator |
Rozłóżmy dziewięć bajtów MessagePack na części:
81: wysoki półbajt1000oznacza mapę, niski półbajt0001oznacza jedną parę klucz-wartość;a3: trzy wysokie bity101oznaczają łańcuch o stałej długości 3;6e 61 6d 65: ASCII dlaname;a3: kolejny łańcuch o długości 3;6a 6f 65:joe.
a1 w CBOR to typ główny 5 (mapa) o długości 1, a 64 to typ główny 3 (łańcuch tekstowy) o długości 4, więc semantyka jest niemal izomorficzna z MessagePack. BSON jest większy: każdy dokument niesie 4-bajtową długość całkowitą, każde pole powtarza bajt typu i nazwę, a każdy łańcuch powtarza długość plus końcowy \0. Przy małych dokumentach BSON łatwo bywa większy niż tekst JSON.
Możesz sprawdzić prefiksy bezpośrednio, renderując te bajty jako 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 Rozmiar i szybkość parsowania: zignoruj liczby marketingowe
Twierdzenia, że binarny format jest „10x szybszy”, należy rozbić na dwa pytania.
Rozmiar. Nieskompresowany binarny jest mniejszy, bo interpunkcja i białe znaki znikają. Ale realne potoki prawie zawsze kompresują: po gzip lub brotli powtarzające się nazwy pól zostają skompresowane, a przewaga binarna dramatycznie maleje. Podążaj metodą z Minifikacja JSON a gzip: najpierw zmierz skompresowane bajty, potem zdecyduj, czy zmiana formatu jest warta zachodu.
Szybkość parsowania. Binarny pomija leksowanie (skanowanie cudzysłowów, przecinków i znaków liczb), więc powinien być szybszy w teorii. W JavaScript dominującym kosztem jest jednak alokacja obiektów i pamięci, której żaden dekoder nie uniknie. Kształt danych, liczba pól i jakość implementacji mają znaczenie, więc jedyny wiarygodny wniosek pochodzi z benchmarkowania własnych ładunków, a nie z cytowania mnożnika dostawcy. Jeśli pojedynczy ładunek jest zbyt duży, by wczytać go naraz do pamięci, pokrój go podejściem opisanym w parsowaniu i strumieniowaniu dużego JSON, zanim zmierzysz zysk kodowania.
4. Pola int64 i binarne: prawdziwy powód zmiany
Rozmiar często nie jest powodem zmiany formatu. Siła wyrazu typów jest.
4.1 Liczby całkowite powyżej 2^53
Liczby JSON lądują w double w JavaScript, więc 9007199254740993 staje się 9007199254740992. Trzy formaty binarne radzą sobie z tym różnie:
| Format | Liczby całkowite 64-bit | 64-bit bez znaku | Dowolna precyzja |
|---|---|---|---|
| MessagePack | ✓ int64 (0xd3), dekodowalne do BigInt |
✓ uint64 (0xcf) |
✗ (wymaga typu rozszerzenia) |
| CBOR | ✓ 64-bitowe formy typu głównego 0/1 | ✓ Ten sam obszar kodowania | ✓ tagi bignum 2/3 |
| BSON | ✓ int64 (0x12) |
✗ Tylko ze znakiem | ✗ (Decimal128 jest stałoprzecinkowy, a nie dowolną liczbą całkowitą) |
CBOR jest jedynym ze standardowym przedstawieniem liczb całkowitych o dowolnej precyzji: wartości przekraczające 64 bity są opakowane w tag 2 (nieujemne) lub tag 3 (ujemne) wokół łańcucha bajtowego. MessagePack i BSON cofają się do łańcuchów lub własnych konwencji rozszerzeń.
4.2 Ładunki binarne
JSON ma dokładnie jeden sposób wyrażania danych binarnych: Base64, które rozdyma ładunek o około jedną trzecią i wymaga dodatkowej kopii dekodowania. MessagePack ma typy bin (0xc4–0xc6), CBOR ma łańcuchy bajtowe (typ główny 2), a BSON ma podtypy binarne. Gdy transportujesz obrazy, hashes, wektory lub skompresowane bloki, ta różnica znaczy znacznie więcej niż oszczędzenie kilku bajtów interpunkcji.
5. Porównanie obok siebie
| Wymiar | MessagePack | CBOR | BSON |
|---|---|---|---|
| Specyfikacja | Specyfikacja społeczności | RFC 8949 IETF | Ekosystem MongoDB |
| Typ nośny | application/msgpack (nie standard IANA) |
application/cbor (zarejestrowany w IANA) |
Brak ogólnego typu nośnego |
| System typów | Sześć typów JSON plus bin i ext | Sześć typów JSON plus bin, tagi i wartości proste | Szerszy: ObjectId, Decimal128, daty, regex, znaczniki czasu |
| Mechanizm rozszerzeń | Typy rozszerzeń (np. timestamp -1) | Tagi (daty, bignumy, osadzony CBOR) | Stałe kody typów, rozszerzenia według konwencji ekosystemu |
| Deterministyczne kodowanie | Brak standardowego wymogu | Jawne reguły, użyteczne do podpisów | Brak |
| Samoopisujący się | Tak, ale bez schematu pól | Tak, opisywalny przez CDDL | Tak, z nazwami pól powtarzanymi per dokument |
| Narzut małych dokumentów | Niski | Niski | Wysoki (prefiks długości, nazwy pól, terminator) |
| Ekosystem | Cache, logi, część RPC | COSE, WebAuthn, IoT | Tylko MongoDB |
6. Wsparcie przeglądarek i Node
Node 24 nie dostarcza żadnego z tych kodeków, więc instalujesz bibliotekę. Częste wybory:
| Środowisko | MessagePack | CBOR | BSON |
|---|---|---|---|
| Node.js | @msgpack/msgpack, msgpackr |
cbor-x, borc |
bson |
| Przeglądarka | @msgpack/msgpack |
kompilacja przeglądarkowa cbor-x, cbor-web |
bson |
Obie rodziny działają na Uint8Array i nie wymagają Buffer. Uważaj na różnice wersji:
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 };
}
Większość bibliotek domyślnie dekoduje duże liczby całkowite do number i traci precyzję poza bezpiecznym zakresem. Jeśli dokładne wartości mają znaczenie, jawnie włącz opcję BigInt (useBigInt64 w @msgpack/msgpack, równoważny przełącznik w cbor-x), inaczej po prostu przeniosłeś stary problem JSON-a do nowego formatu.
7. Debugowalność i zgodność protokołu
7.1 Debugowalność: największy ukryty koszt
Największą zaletą tekstowego JSON jest nie rozmiar, ale to, że możesz go otworzyć w każdej chwili. Po przejściu na binarny:
grep,awkijqprzestają działać, a potoki logów wymagają dodatkowego etapu dekodowania;- Debugowanie produkcji wymaga właściwego dekodera we właściwej wersji, inaczej zobaczysz złe nazwy pól;
- Przechwycony ruch pojawia się w narzędziach deweloperskich przeglądarki jako nieczytelny blok bajtów;
- Diffy przeglądu kodu pokazują ciąg szesnastkowy.
CBOR ma notację diagnostyczną, która renderuje bajty z powrotem do czytelnego tekstu zbliżonego do JSON, na przykład mapę jako {1: 2}; MessagePack też ma kilka dekoderów wiersza poleceń. Ale wszystkie wymagają dodatkowej instalacji i leżą poza standardową ścieżką operacji.
Wniosek: gdy tylko ludzie muszą często przeglądać dane, ukryty koszt formatu binarnego przewyższa oszczędzoną przepustowość.
7.2 Zgodność protokołu i rozszerzenia
- CBOR ma najbardziej kompletny mechanizm tagów: tag 0 dla łańcucha data-czas, tag 1 dla znacznika czasu Unix, tagi 2 i 3 dla bignumów, tag 24 dla osadzonego CBOR. W połączeniu z deterministycznym kodowaniem jest używany przez protokoły wymagające podpisów i spójności między implementacjami, takie jak COSE, WebAuthn i CWT.
- MessagePack zostawia typy rozszerzeń implementatorom. W praktyce jedyny powszechny to timestamp (typ rozszerzenia -1). Między zespołami każdy typ rozszerzenia musi trafić do dokumentu protokołu, inaczej druga strona nie potrafi go zdekodować.
- BSON ma stałe kody typów, a
ObjectId,Decimal128,MinKeyiMaxKeyto semantyka specyficzna dla MongoDB. Traktowanie BSON jako ogólnego formatu wymiany przynosi stos nieużywanych typów i nieużywanych prefiksów długości.
Jeśli protokół wymaga „te same dane muszą dawać te same bajty”, tylko CBOR daje standardową odpowiedź; MessagePack i BSON wymagają dodatkowych ustaleń co do kolejności pól i szczegółów kodowania.
8. Kiedy warto się przełączyć
Sygnały uzasadniające przejście:
- Ładunki są duże, oba końce można zmienić, a skompresowane rozmiary nadal istotnie się różnią;
- Transportujesz treść binarną, a rozdęcie Base64 i koszt dekodowania są wymierne;
- Potrzebujesz 64-bitowych liczb całkowitych lub dokładnych ułamków dziesiętnych, których double w JSON-ie nie potrafi przedstawić;
- Łącze jest serwer-do-serwera i narzędzia diagnostyczne można ustandaryzować.
Sygnały przeciwne:
- gzip lub brotli już jest, a zysk to jednocyfrowy procent;
- API jest publiczne, a podmioty zewnętrzne mają tylko ogólne klienty HTTP;
- Dane to logi lub konfiguracja czytana ręcznie;
- Zespół nie ma budżetu na utrzymanie dekoderów i zgodności wersji.
Działa też ścieżka pośrednia: trzymaj JSON zewnętrznie i używaj MessagePack do wewnętrznych cache. Wartości cache czytają tylko twoje własne usługi, więc zarówno rozmiar, jak i koszt parsowania się poprawiają, a wyczyszczenie cache rozwiązuje każdy problem bez dotykania kontraktu.
9. Lista kontrolna
- [ ] Porównuj skompresowane liczby bajtów z prawdziwych danych, a nie liczby nieskompresowane;
- [ ] Ustal politykę dużych liczb całkowitych: opcja BigInt, transport łańcuchowy albo bignumy CBOR;
- [ ] Wysyłaj pola binarne jako bin lub łańcuchy bajtowe, zamiast opakowywać je w Base64;
- [ ] Wpisz typy rozszerzeń i tagi do dokumentu protokołu i przypnij wersje dekoderów;
- [ ] Zachowaj ścieżkę debugowania tekstowym JSON-em, którą można włączyć podczas incydentów produkcyjnych;
- [ ] Przeprowadź benchmark i zapisz powtarzalne liczby, zamiast twierdzeń słownych.
Jaka jest różnica między MessagePack a CBOR?
Ich kodowania są bardzo podobne: mapy, tablice i łańcuchy używają zwartego prefiksu typ-plus-długość. Główna różnica to standaryzacja. CBOR to RFC IETF z typem nośnym IANA, mechanizmem tagów i deterministycznymi regułami kodowania; MessagePack to specyfikacja społeczności, której typy rozszerzeń opierają się na prywatnych ustaleniach. Wybierz CBOR, gdy potrzebujesz podpisów, spójności między implementacjami lub protokołów dla urządzeń o ograniczonych zasobach.
Czy BSON może zastąpić JSON jako format ogólny?
Nie jest to zalecane. BSON zaprojektowano dla dokumentów bazodanowych: każdy dokument niesie 4-bajtową długość całkowitą, każde pole powtarza typ i nazwę, a małe dokumenty kończą się większe niż tekst JSON. Zawiera też typy specyficzne dla MongoDB, takie jak ObjectId i MinKey. Poza MongoDB większość jego systemu typów jest nieużywana, a kara rozmiarowa zostaje.
O ile mniejszy czyni mój ładunek format binarny?
Zależy od kształtu danych. Gdy nazwy pól powtarzają się intensywnie, skompresowany tekstowy JSON i formaty binarne wypadają podobnie; gdy nazwy są krótkie, a dane pełne znaków strukturalnych i liczb, przewaga binarna jest wyraźniejsza. Zmierz prawdziwe próbki zarówno nieskompresowane, jak i po gzip, a potem zdecyduj, zamiast szacować ogólnym mnożnikiem.
Czy nadal mogę debugować dane binarne przez jq?
Nie. jq przetwarza tylko tekstowy JSON i natychmiast zawiedzie na bajtach MessagePack lub CBOR. Musisz najpierw zdekodować pasującą biblioteką do JSON i dopiero podać go jq; CBOR może też przejść przez narzędzie notacji diagnostycznej, aby stać się czytelnym tekstem. Utrzymanie ścieżki debugowania tekstem jest tańsze niż instalowanie narzędzi na żądanie.