二進位 JSON:MessagePack、CBOR 與 BSON 的取捨
結論:MessagePack、CBOR、BSON 都把 JSON 的資料模型壓成位元組,但體積、型別系統與生態完全不同。本文用同一份資料比較位元組開銷,說明 int64 與二進位欄位的處理,並給出「什麼時候值得換」的判斷線。

結論:對外交換和需要人檢視的資料保持文字 JSON,只有當二進位內容、超過 2^53 的整數或精確小數讓壓縮後的體積與型別表達真正吃虧時,才在內部鏈路換 MessagePack 或 CBOR;BSON 只屬於 MongoDB 生態。
文字 JSON 省掉的是空白,二進位格式省掉的是標點與數字的字元表示;欄位名照樣一條一份,所以換格式前先算清楚到底省了多少。
一、二進位 JSON 到底省了什麼
先把期待放對位置。這三種格式都不是「另一種 JSON 語法」,而是用位元組表達同一套資料模型:物件、陣列、字串、數字、布林、空值。它們省掉的東西很具體:
- 引號、冒號、逗號、括號等結構性字元;
- 數字的十進位文字表示,比如
32變成一個位元組; - 字串前的長度宣告由定長前綴表示,省去重新掃描;
- 二進位內容可以直接是位元組串,不必先做 Base64。
省不掉的東西同樣具體:欄位名仍然以字串形式逐條出現。MessagePack 的 map、CBOR 的 map、BSON 的 document,都沒有「欄位名只出現一次」的機制。因此當你的 JSON 是一萬條同構記錄時,壓縮後的文字 JSON 與二進位格式的體積差距會明顯小於直覺,因為 gzip 最擅長的就是消除重複欄位名。
二、三種格式的出身
| 格式 | 規範與年份 | 設計源頭 | 典型用途 |
|---|---|---|---|
| MessagePack | msgpack.org 規範,2008 年起 | 獨立設計,強調極簡與緊湊 | 快取值、程序間通訊、日誌管道 |
| CBOR | RFC 8949(STD 94),2020 年 | 受 MessagePack 影響,IETF 標準化 | COSE、WebAuthn、CWT、受限裝置 |
| BSON | bsonspec.org,MongoDB 生態 | 為資料庫文件設計 | MongoDB 儲存與線路協定 |
CBOR 的標準化程度最高,有 IANA 註冊的 application/cbor 媒體類型、確定性編碼規則與 CDDL 描述語言;MessagePack 最輕,規範一頁能讀完;BSON 最重,它把 ObjectId、Decimal128、日期、二進位子型別都做進了型別系統,也因此只在 MongoDB 生態裡真正流行。
三、同一份資料,四種位元組
以 {"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 位元組 + 型別 1 位元組 + name\0 + 值長 4 位元組 + joe\0 + 終止符 |
逐位元組拆開 MessagePack 那 9 個位元組:
81:高 4 位1000表示 map,低 4 位0001表示 1 個鍵值對;a3:高 3 位101表示定長字串,長度 3;6e 61 6d 65:name的 ASCII;a3:又是一個長度 3 的字串;6a 6f 65:joe。
CBOR 的 a1 是 major type 5(map)加長度 1,64 是 major type 3(文字字串)加長度 4,語意與 MessagePack 幾乎同構。BSON 反而更大:每個文件都要帶 4 位元組總長,每個欄位都要重複型別位元組與欄位名,字串還要再帶一個長度和結尾的 \0。小文件上,BSON 比 JSON 文字還長並不奇怪。
把這段位元組還原成十六進位,可以直接核對前綴:
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 倍」的說法,先拆成兩件事看:
體積。 未壓縮時,二進位確實更小,因為省掉了標點與空白。但真實鏈路幾乎都會壓縮:文字 JSON 經過 gzip 或 brotli 後,重複欄位名被高度壓縮,二進位格式的領先幅度會縮到很小。可以對照 JSON 壓縮與 gzip 的取捨 裡的判斷方法:先量壓縮後的實際位元組,再決定要不要換格式。
解析速度。 二進位省掉了詞法分析(掃描引號、逗號、數字字元),理論上更快。但 JavaScript 裡真正的成本大頭是建立物件與分配記憶體,解碼器再快也繞不過這一步。資料形狀、欄位數量、實作品質都會影響結果,唯一可靠的結論來自用自己的資料做基準測試,而不是照抄宣傳頁上的倍數。如果單條負載已經大到無法整體讀入記憶體,先用 大 JSON 的解析與串流處理 裡的分片方法切塊,再比較編碼收益。
四、int64 與二進位欄位:換格式的真正理由
很多時候,體積不是換格式的理由,型別表達力才是。
4.1 超過 2^53 的整數
JSON 數字在 JavaScript 裡落到雙精度浮點,9007199254740993 會被解析成 9007199254740992。三種二進位格式的處理方式:
| 格式 | 64 位整數 | 無號 64 位 | 任意精度 |
|---|---|---|---|
| MessagePack | ✓ int64(0xd3),解碼可給 BigInt |
✓ uint64(0xcf) |
✗(需擴充型別) |
| CBOR | ✓ major type 0/1 的 64 位形式 | ✓ 同一編碼空間 | ✓ bignum 標籤 2/3 |
| BSON | ✓ int64(0x12) |
✗ 只有有號型別 | ✗(Decimal128 定點,不是任意精度整數) |
CBOR 是三者裡唯一對任意精度整數有標準表示的:超過 64 位的整數用 tag 2(非負)或 tag 3(負)包一段位元組串。MessagePack 與 BSON 遇到超大整數只能退回字串,或者在擴充型別裡自訂約定。
4.2 二進位內容
JSON 表達二進位只有一條路:Base64,體積膨脹約三分之一,解碼還要額外一次拷貝。MessagePack 有 bin 型別(0xc4–0xc6),CBOR 有 byte string(major type 2),BSON 有 binary 子型別。傳圖片、雜湊、向量、壓縮塊時,這個差異比「省幾個標點」重要得多。
五、三者對比表
| 維度 | MessagePack | CBOR | BSON |
|---|---|---|---|
| 規範來源 | 社群規範 | IETF RFC 8949 | MongoDB 生態 |
| 媒體類型 | application/msgpack(非 IANA 標準) |
application/cbor(IANA 註冊) |
無通用媒體類型 |
| 型別系統 | JSON 六類 + bin + ext | JSON 六類 + bin + tag + 簡單值 | 更寬:ObjectId、Decimal128、日期、正規表達式、時間戳 |
| 擴充機制 | extension type(如 timestamp -1) | tag(日期、bignum、內嵌 CBOR 等) | 固定型別碼,擴充靠生態約定 |
| 確定性編碼 | 無標準要求 | 有明確規則,可用於簽章 | 無 |
| 自描述 | 是,但無欄位級 schema | 是,可配 CDDL 描述 | 是,文件內重複欄位名 |
| 小文件開銷 | 低 | 低 | 高(長度前綴 + 欄位名 + 終止符) |
| 生態 | 快取、日誌、部分 RPC | COSE、WebAuthn、IoT | MongoDB 專用 |
六、瀏覽器與 Node 支援
Node 24 沒有內建這三種編解碼器,需要裝函式庫。常用組合:
| 執行環境 | MessagePack | CBOR | BSON |
|---|---|---|---|
| Node.js | @msgpack/msgpack、msgpackr |
cbor-x、borc |
bson |
| 瀏覽器 | @msgpack/msgpack |
cbor-x 的瀏覽器建置、cbor-web |
bson |
兩類實作都圍繞 Uint8Array 工作,不需要額外的 Buffer。需要注意版本差異:
import { encode, decode } from '@msgpack/msgpack';
/**
* 將物件編碼為 MessagePack 位元組並立即解碼校驗。
* @param payload 待編碼的普通物件
* @returns 解碼結果與編碼後的位元組長度
*/
function roundTrip(payload: Record<string, unknown>): { decoded: unknown; byteLength: number } {
const bytes = encode(payload);
return { decoded: decode(bytes), byteLength: bytes.byteLength };
}
大整數在多數函式庫裡預設解碼成 number,超過安全整數範圍時會丟精度;需要精確值時,要顯式開啟 BigInt 選項(@msgpack/msgpack 提供 useBigInt64,cbor-x 有對應開關),否則等於把 JSON 的老問題原樣搬了過來。
七、可除錯性與協定相容
7.1 可除錯性:最大的隱性成本
文字 JSON 最大的優勢不是體積,而是任何時候都能打開看。換成二進位之後:
grep、awk、jq全部失效,日誌管道要額外一步解碼;- 排查線上問題時,先要拿到正確的解碼器與版本,否則看到的欄位名可能是亂的;
- 擷取封包結果在瀏覽器開發者工具裡顯示為一段不可讀的位元組;
- 程式碼審查時,diff 裡出現的是一串十六進位。
CBOR 有診斷記法(diagnostic notation),能把位元組還原成接近 JSON 的可讀文字,例如 map 寫作 {1: 2};MessagePack 也有若干命令列解碼工具。但這些都需要額外安裝,且不在標準維運鏈路裡。
結論:只要這份資料需要人頻繁查看,二進位格式的隱性成本就會超過它省下的頻寬。
7.2 協定相容與擴充
- CBOR 的 tag 機制最完整:tag 0 表示日期時間字串,tag 1 表示 Unix 時間戳,tag 2/3 表示大整數,tag 24 表示內嵌一段 CBOR。配合確定性編碼,它被 COSE、WebAuthn、CWT 這些需要簽章與跨實作一致性的協定採用。
- MessagePack 的擴充型別留給實作方自行約定,常見的只有 timestamp(擴充型別 -1)。跨團隊使用時,擴充型別必須寫進協定文件,否則對方解不出來。
- BSON 的型別碼是固定的,
ObjectId、Decimal128、MinKey/MaxKey都是 MongoDB 專屬語意。把 BSON 當成通用交換格式,會帶來一堆用不上的型別和用不上的長度前綴。
如果協定要求「同樣的資料必須編碼成同樣的位元組」,只有 CBOR 給出了標準答案;MessagePack 與 BSON 都需要額外約定欄位順序與編碼細節。
八、什麼時候值得換
值得換的訊號:
- 單次傳輸的資料量大,且兩端都能改程式,壓縮後的體積仍有明顯差距;
- 需要傳二進位內容,Base64 的膨脹與解碼成本已經可測;
- 需要 64 位整數、精確小數,而 JSON 的雙精度會丟精度;
- 伺服器到伺服器的內部鏈路,排查工具可以統一配好。
不值得換的訊號:
- 已經用 gzip 或 brotli 壓縮,收益只剩個位數百分比;
- 對外公開 API,第三方只能用通用 HTTP 客戶端;
- 日誌、配置檔案等需要人眼查看的內容;
- 團隊沒有維護解碼工具與版本相容的預算。
中間路線也成立:對外保持 JSON,內部快取用 MessagePack。快取值只被自家服務讀取,體積與解析成本都能省,出問題時把快取清掉即可,不影響契約。
九、實作清單
- [ ] 先用真實資料比較壓縮後的位元組數,而不是未壓縮的位元組數;
- [ ] 明確大整數策略:BigInt 選項、字串承載,還是換成 CBOR 的 bignum;
- [ ] 二進位欄位直接走 bin / byte string,不再套一層 Base64;
- [ ] 擴充型別與 tag 寫進協定文件,並固定解碼器版本;
- [ ] 保留一條文字 JSON 的除錯通道,線上排障時能切過去;
- [ ] 做一次基準測試,把「快多少」寫成可重現的數字,而不是口頭結論。
MessagePack 和 CBOR 有什麼區別?
兩者編碼結構高度相似:map、陣列、字串都用「型別位 + 長度」的緊湊前綴。主要差別在標準化程度:CBOR 是 IETF RFC,有 IANA 媒體類型、tag 機制與確定性編碼規則;MessagePack 是社群規範,擴充型別靠自訂約定。需要簽章、跨實作一致性或受限裝置協定時選 CBOR。
BSON 能當通用 JSON 替代品嗎?
不建議。BSON 為資料庫文件設計,每個文件帶 4 位元組總長、每個欄位重複型別與欄位名,小文件比 JSON 文字還大;它還包含 ObjectId、MinKey 等 MongoDB 專屬型別。離開 MongoDB 生態後,它的型別系統大部分用不上,體積劣勢卻照舊存在。
換成二進位格式後,體積能小多少?
取決於資料形狀。欄位名重複率高時,文字 JSON 壓縮後與二進位的差距很小;欄位名短、結構字元多、數字多時,二進位優勢更明顯。正確做法是拿真實樣本分別測「未壓縮」與「gzip 後」兩組數字,再做決定,不要用通用倍數估算。
二進位格式還能用 jq 除錯嗎?
不能。jq 只處理文字 JSON,遇到 MessagePack 或 CBOR 位元組會直接報解析錯誤。除錯時需要先用對應函式庫解碼成 JSON,再交給 jq;CBOR 還可以用診斷記法工具轉成可讀文字。因此保留一條文字除錯通道,比臨時裝工具更省時間。