send() takes more than strings
ws.send() accepts a string (sent as a text frame), or any of ArrayBuffer, Blob, or a typed array like Uint8Array (sent as a binary frame). The browser handles the framing for you. There is no chunking at the API level — one send() is one logical message.
JSON is the default protocol
For 95% of applications, your wire format is JSON: stringify on the way out, parse on the way in. Pair this with a type field (Track 5) and you have a perfectly serviceable application protocol.
Binary types and DataView
For compact wire formats — game state, market data, custom protocols — set ws.binaryType = 'arraybuffer' so messages arrive as ArrayBuffer instead of Blob. Then use DataView to read and write fields at known byte offsets. The savings vs. JSON are dramatic: a position update is 8-16 bytes binary vs. 50+ bytes JSON.
bufferedAmount and backpressure
If you call send() faster than the network can drain it, bytes pile up in ws.bufferedAmount. There is no callback for "drained" — you poll. For most apps this never matters; for apps that fan out many messages quickly (cursors, position broadcasts), throttle when bufferedAmount climbs above a few KB.