ข้ามไปยังเนื้อหา

JSON vs Binary

คุณตัดสินใจแล้วว่าจะส่งหนึ่ง object ที่มีโครงสร้างต่อหนึ่ง message ทางแยกถัดไปคือ จะ encode object นั้นบน wire อย่างไร WebSocket รับส่งได้ทั้ง text หรือ binary และทางเลือกเดียวนั้น — JSON เทียบกับ format แบบ binary — ส่งผลเงียบ ๆ ต่อความ debug ได้ง่าย ต่อ bandwidth และต่อ CPU ของคุณ

JSON.stringify และ JSON.parse ถูกฝังอยู่ในทุกเบราว์เซอร์และทุก server runtime JSON นั้น อธิบายตัวเองได้ (self-describing) (ชื่อ field เดินทางไปพร้อมกับข้อมูล) มนุษย์อ่านได้ใน network inspector และทุกภาษาเข้าใจได้โดยไม่ต้องมีขั้น build สำหรับแอปพลิเคชันส่วนใหญ่อย่างท่วมท้น — chat, dashboards, notifications, การแก้ไขร่วมกัน — JSON คือค่าเริ่มต้นที่ถูกต้อง คุณอ่าน message ได้ในพริบตา และการรับ client ใหม่เข้ามาก็แค่ตกลงกันเรื่องชื่อ field เท่านั้น ไม่มีอะไรมากกว่านั้น

ต้นทุนก็มีจริงไม่แพ้กัน ชื่อ field ซ้ำอยู่ใน ทุก message ตัวเลขเก็บเป็น text ฐานสิบ และไม่มี type ดั้งเดิมสำหรับ bytes ดิบ position update อย่าง {"x":12,"y":48} ใช้ bytes ส่วนใหญ่ไปกับ quotes, braces และตัวอักษร x กับ y มากกว่าค่าจริง

format การ serialize แบบ binary แลกความอ่านได้กับความหนาแน่น:

  • MessagePack — “JSON แต่เป็น binary” data model เดียวกัน (objects, arrays, numbers, strings) ไม่มี schema เหมือน JSON แต่ encode อย่างกระชับ มักเป็นการอัปเกรดจาก JSON ที่ง่ายที่สุด
  • Protocol Buffers (Protobuf) — schema-first คุณประกาศรูปร่าง message ในไฟล์ .proto ชื่อ field ไม่เคยเดินทางบน wire (มีแค่ numeric tag เล็ก ๆ เท่านั้น) จึงกระชับที่สุดและ parse เร็วที่สุด แลกมากับขั้น code-generation และความยืดหยุ่นแบบ ad-hoc ที่น้อยลง
  • CBOR — format แบบ binary มาตรฐาน IETF (RFC 8949) ในตระกูล MessagePack พบได้ทั่วไปใน IoT และสภาพแวดล้อมที่มีข้อจำกัด

สิ่งเหล่านี้เปล่งประกายเมื่อ message เล็ก ถี่ และมีปริมาณสูง: สถานะเกม multiplayer ที่ 60 อัปเดตต่อวินาที, market data feed, telemetry จาก sensor หลายพันตัว การเฉือน bytes และเวลา parse ต่อ message สะสมเพิ่มขึ้นอย่างรวดเร็ว

flowchart LR
  subgraph json["JSON (text frame)"]
    j1["readable · universal · self-describing"]
    j2["larger · field names repeated"]
  end
  subgraph bin["Binary (binary frame)"]
    b1["compact · fast to parse"]
    b2["needs tooling · not human-readable"]
  end
  json -- "high volume? tiny messages?" --> bin
  bin -- "debuggability? simplicity?" --> json
พื้นที่ของการแลกเปลี่ยน: ความอ่านได้ เทียบกับ ความหนาแน่น

หลักคิดเชิงปฏิบัติ:

  • ตั้งค่าเริ่มต้นเป็น JSON ใช้ไปก่อนจนกว่าจะวัดได้ว่ามีปัญหาจริง ความอ่านง่ายและการไม่ต้องมี tooling มีค่ามาก และแอปส่วนใหญ่ยังห่างจากขีดจำกัด bandwidth หรือ CPU ที่รูปแบบ encode จะเริ่มมีผลอยู่อีกไกล
  • เปลี่ยนเป็น binary เมื่อตัวเลขเรียกร้อง — อัตรา message สูง, payload ใหญ่, mobile client บนข้อมูลที่คิดค่าตามปริมาณ หรือ budget latency ที่เข้มงวด วัดก่อน profiler ชนะการคาดเดาเสมอ
  • คุณผสมกันได้ ส่ง message ควบคุม/ตั้งค่าเป็น JSON เพื่อความชัดเจน และส่งสตรีมร้อนความถี่สูงเป็น binary วินัย field type จากบทเรียนที่แล้วใช้ได้กับ encoding ทั้งสองแบบ

เดโมที่รันได้ด้านล่างส่ง JSON message แล้วรายงานขนาดจริงบน wire — ให้เห็นกับตาว่าโครงสร้างเองกินไปกี่ bytes โดยใช้ WebSocket API จริงบน echo socket ภายในหน้า

JavaScript

ในการตั้งค่า binary จริง คุณจะ encode object เดียวกัน ด้วย library ก่อนส่งออกเป็น binary frame ถ้าใช้ MessagePack รูปร่างจะหน้าตาแบบนี้ — สังเกต binary frame และ receive type ที่เป็น arraybuffer:

import { encode, decode } from '@msgpack/msgpack';
// Tell the socket to hand binary messages back as ArrayBuffer, not Blob.
const ws = new WebSocket('wss://your-server');
ws.binaryType = 'arraybuffer';
ws.onopen = () => {
const update = { type: 'pos', x: 12, y: 48 };
const bytes: Uint8Array = encode(update); // compact binary, no repeated field-name text
ws.send(bytes); // sent as a BINARY frame, not text
};
ws.onmessage = (event: MessageEvent) => {
// event.data is an ArrayBuffer for binary frames.
const msg = decode(new Uint8Array(event.data)) as { type: string; x: number; y: number };
console.log(msg.type, msg.x, msg.y);
};

ตรรกะของแอปพลิเคชันเหมือนกับเวอร์ชัน JSON ทุกประการ — มีแค่การเรียก encode/decode และ frame type เท่านั้นที่เปลี่ยน นั่นคือเหตุผลที่คุณเริ่มด้วย JSON ได้และค่อย migrate hot path ไปเป็น binary ทีหลังหากการวัดเรียกร้อง

FormatขนาดDebugSetup
JSONใหญ่กว่า (field names ซ้ำ)อ่านได้ใน DevToolsไม่ต้องมี tooling
MessagePackเล็กกว่า JSON ~30-40%ต้อง decode ก่อนอ่านinstall library
Protobufเล็กที่สุด, schema-firstต้อง .proto + generated codebuild step
CBORเล็กกว่า JSON, IETF standardต้อง decodelibrary

เปลี่ยนเป็น Binary ก่อนวัด Performance อาการ:

  • เปลี่ยน JSON เป็น MessagePack โดยไม่มีหลักฐานว่าช้าจริง
  • เพิ่ม complexity โดยไม่ได้ benefit ที่ชัดเจน
  • ใช้ JSON ก่อน วัด latency/bandwidth ถ้าเป็น bottleneck จริงค่อยเปลี่ยน

ผสม Binary และ JSON ใน Connection เดียวโดยไม่มี Protocol อาการ:

  • บาง message เป็น JSON บาง message เป็น binary โดยไม่มี type indicator
  • onmessage ต้อง guess ว่า frame ไหนเป็น format อะไร
  • กำหนด convention ชัดเจน: control message = JSON, data stream = binary

💡 ตัวอย่างจากของจริง

Binance WebSocket Streams:

  • ใช้ JSON สำหรับ subscription management
  • ใช้ binary (custom binary format) สำหรับ high-frequency order book update — ลด payload ได้ 60%

Figma Multiplayer:

  • ใช้ binary (custom format) สำหรับ drawing operation — compact สำหรับ position/color/stroke data
  • JSON สำหรับ metadata และ configuration
อะไรคือเหตุผลที่หนักแน่นที่สุดในการตั้งค่าเริ่มต้นเป็น JSON สำหรับ WebSocket message?
เมื่อไรที่ format แบบ binary ที่กระชับจึงคุ้มต้นทุนชัดเจนที่สุด?
ใน code MessagePack ทำไมต้องตั้ง ws.binaryType = "arraybuffer"?