Sending binary data: Blob vs ArrayBufferБинарные данные: Blob или ArrayBuffer
Why send bytes at all?Зачем вообще слать байты?
JSON over WebSocket is convenient, but text is heavy: numbers become strings, keys repeat on every message, and parsing costs CPU. For high-frequency data — game state, audio chunks, sensor streams, position updates — a compact binary frame can be several times smaller and noticeably faster. WebSocket carries binary natively, so there is no base64 tax.
JSON поверх WebSocket удобен, но текст тяжёл: числа становятся строками, ключи повторяются в каждом сообщении, а парсинг тратит CPU. Для высокочастотных данных — состояние игры, аудио-чанки, потоки сенсоров, обновления координат — компактный бинарный кадр может быть в несколько раз меньше и заметно быстрее. WebSocket передаёт бинарные данные нативно, поэтому налога на base64 нет.
Choosing a binaryTypeВыбор binaryType
Incoming binary messages arrive as a Blob by default. For most apps you want an ArrayBuffer instead — it's synchronous to read and works directly with typed arrays:
const ws = new WebSocket(url); ws.binaryType = "arraybuffer"; // or the default "blob" ws.onmessage = (e) => { if (typeof e.data === "string") { handleText(e.data); // JSON / text frame } else { const view = new DataView(e.data); handleBinary(view); // raw bytes } };
- Blob — best when you'll hand the data straight to an
<img>,URL.createObjectURL, or store it; reading needs an async step. - ArrayBuffer — best when you parse fields immediately with
DataView/typed arrays.
Входящие бинарные сообщения по умолчанию приходят как Blob. В большинстве приложений нужен ArrayBuffer — он читается синхронно и работает напрямую с типизированными массивами:
const ws = new WebSocket(url); ws.binaryType = "arraybuffer"; // или "blob" по умолчанию ws.onmessage = (e) => { if (typeof e.data === "string") { handleText(e.data); // JSON / текстовый кадр } else { const view = new DataView(e.data); handleBinary(view); // сырые байты } };
- Blob — лучше, когда данные сразу идут в
<img>,URL.createObjectURLили на хранение; чтение требует асинхронного шага. - ArrayBuffer — лучше, когда вы сразу разбираете поля через
DataView/типизированные массивы.
Framing a structured messageФормирование структурного сообщения
Define a fixed layout and read/write it with DataView. Here a position update packs a 1-byte type tag, a 32-bit id and two floats — 13 bytes total instead of ~60 as JSON:
const TYPE_MOVE = 0x01; function packMove(id, x, y) { const buf = new ArrayBuffer(13); const dv = new DataView(buf); dv.setUint8(0, TYPE_MOVE); dv.setUint32(1, id); dv.setFloat32(5, x); dv.setFloat32(9, y); return buf; } function unpackMove(dv) { return { id: dv.getUint32(1), x: dv.getFloat32(5), y: dv.getFloat32(9), }; } ws.send(packMove(42, 12.5, -3.0)); // 13 bytes on the wire
Pick one endianness and document it.DataViewdefaults to big-endian; passtrueas the last argument for little-endian, and be consistent on both sides.
Определите фиксированную раскладку и читайте/пишите её через DataView. Здесь обновление позиции упаковывает 1-байтовый тег типа, 32-битный id и два float — всего 13 байт вместо ~60 в JSON:
const TYPE_MOVE = 0x01; function packMove(id, x, y) { const buf = new ArrayBuffer(13); const dv = new DataView(buf); dv.setUint8(0, TYPE_MOVE); dv.setUint32(1, id); dv.setFloat32(5, x); dv.setFloat32(9, y); return buf; } function unpackMove(dv) { return { id: dv.getUint32(1), x: dv.getFloat32(5), y: dv.getFloat32(9), }; } ws.send(packMove(42, 12.5, -3.0)); // 13 байт «на проводе»
Выберите один порядок байт и зафиксируйте его.DataViewпо умолчанию big-endian; передайтеtrueпоследним аргументом для little-endian и будьте последовательны на обеих сторонах.
Watch bufferedAmountСледите за bufferedAmount
Binary streams are easy to over-send. ws.bufferedAmount reports bytes queued but not yet sent. If it grows without bound, you're producing faster than the network drains — pause until it falls:
function trySend(ws, chunk) { const LIMIT = 1 << 20; // 1 MiB high-water mark if (ws.bufferedAmount > LIMIT) return false; // drop / defer ws.send(chunk); return true; }
Бинарные потоки легко «перелить». ws.bufferedAmount сообщает число байт в очереди, ещё не отправленных. Если оно растёт безгранично — вы производите быстрее, чем сеть успевает отдавать; приостановитесь, пока не упадёт:
function trySend(ws, chunk) { const LIMIT = 1 << 20; // порог 1 МиБ if (ws.bufferedAmount > LIMIT) return false; // отложить / отбросить ws.send(chunk); return true; }