Version: 1.19.0 | Updated: 2026-04-14
Nota: Este recurso (
sendBinaryDelta) est\u00e1 dispon\u00edvel no@fluxstack/livemas n\u00e3o \u00e9 usado nos componentes de exemplo do FluxStack app.
Binary Delta allows Live Components to send state updates as raw binary frames instead of JSON. This bypasses the JSON batcher and sends directly over the WebSocket, making it ideal for high-frequency updates like game state (positions, rotations, physics) or real-time sensor data.
| Scenario | Use | Why |
|---|---|---|
| Forms, chat, CRUD | JSON (default setState) |
Low frequency, readability matters |
| Dashboard metrics | JSON | Updates every few seconds |
| Game state (30-60 fps) | Binary Delta | Hundreds of updates/sec, payload size matters |
| Real-time collaboration (cursors) | Binary Delta | High frequency, small payloads |
| IoT sensor streams | Binary Delta | Continuous data, compact encoding |
Rule of thumb: If you're sending state updates more than ~10 times per second, Binary Delta will reduce bandwidth and latency significantly.
Each binary frame has this structure:
[0x01] [idLen:u8] [componentId:utf8] [payload:bytes]
1B 1B N bytes M bytes
| Field | Size | Description |
|---|---|---|
0x01 |
1 byte | BINARY_STATE_DELTA marker |
idLen |
1 byte | Length of componentId string |
componentId |
N bytes | UTF-8 encoded component ID |
payload |
M bytes | Your custom-encoded delta |
Total overhead: 2 + componentId.length bytes. The payload is entirely yours to define.
public sendBinaryDelta(
delta: Partial<TState>,
encoder: (delta: Partial<TState>) => Uint8Array
): void- delta - Object with the state fields that changed (same shape as
setState) - encoder - Function that serializes the delta into bytes
- Compares
deltaagainst current state - only actually changed fields are kept - Updates internal state (same as
setState) - Calls your
encoderwith only the changed fields - Wraps the result in the wire format and sends it
- If nothing changed, no frame is sent
- If WebSocket is closed (readyState !== 1), state updates but no frame is sent
// app/server/live/LiveTracker.ts
import { LiveComponent } from '@core/types/types'
// Encoder: convert delta to binary using DataView
function encodePosition(delta: Record<string, any>): Uint8Array {
// Calculate size: 1 byte flags + 4 bytes per float field
let flags = 0
let size = 1 // flags byte
if ('x' in delta) { flags |= 0x01; size += 4 }
if ('y' in delta) { flags |= 0x02; size += 4 }
if ('speed' in delta) { flags |= 0x04; size += 4 }
const buffer = new ArrayBuffer(size)
const dv = new DataView(buffer)
let offset = 0
dv.setUint8(offset, flags); offset += 1
if ('x' in delta) { dv.setFloat32(offset, delta.x, true); offset += 4 }
if ('y' in delta) { dv.setFloat32(offset, delta.y, true); offset += 4 }
if ('speed' in delta) { dv.setFloat32(offset, delta.speed, true); offset += 4 }
return new Uint8Array(buffer)
}
export class LiveTracker extends LiveComponent<typeof LiveTracker.defaultState> {
static componentName = 'LiveTracker'
static publicActions = ['updatePosition'] as const
static defaultState = {
x: 0,
y: 0,
speed: 0
}
declare x: number
declare y: number
declare speed: number
private _interval?: ReturnType<typeof setInterval>
protected onMount() {
// Send position 30 times per second
this._interval = setInterval(() => {
this.sendBinaryDelta(
{ x: this.x + Math.random(), y: this.y + Math.random() },
encodePosition
)
}, 33) // ~30fps
}
protected onDestroy() {
clearInterval(this._interval)
}
async updatePosition(payload: { x: number; y: number }) {
this.sendBinaryDelta(
{ x: payload.x, y: payload.y },
encodePosition
)
return { success: true }
}
}Pass the binaryDecoder option when mounting the component. The decoder receives the raw payload bytes (without the wire format header - that's already stripped) and must return an object to merge into state.
// app/client/src/live/TrackerDemo.tsx
import { useLiveComponent } from '@fluxstack/live-react'
// Decoder: must mirror the encoder logic
function decodePosition(buffer: Uint8Array): Record<string, any> {
const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
let offset = 0
const flags = dv.getUint8(offset); offset += 1
const result: Record<string, any> = {}
if (flags & 0x01) { result.x = dv.getFloat32(offset, true); offset += 4 }
if (flags & 0x02) { result.y = dv.getFloat32(offset, true); offset += 4 }
if (flags & 0x04) { result.speed = dv.getFloat32(offset, true); offset += 4 }
return result
}
export function TrackerDemo() {
const { state, call, connected } = useLiveComponent('LiveTracker', {
initialState: { x: 0, y: 0, speed: 0 },
binaryDecoder: decodePosition // <-- register decoder here
})
return (
<div>
<p>Position: ({state.x.toFixed(2)}, {state.y.toFixed(2)})</p>
<p>Speed: {state.speed.toFixed(2)}</p>
<p>{connected ? 'Connected' : 'Disconnected'}</p>
</div>
)
}import { LiveConnection, LiveComponentHandle } from '@fluxstack/live-client'
const conn = new LiveConnection({ url: 'ws://localhost:3000/api/live/ws' })
const tracker = new LiveComponentHandle(conn, 'LiveTracker', {
x: 0, y: 0, speed: 0
})
await tracker.mount()
// Register binary decoder AFTER mount
tracker.setBinaryDecoder(decodePosition)
tracker.onStateChange((state, delta) => {
console.log('Position:', state.x, state.y)
})Important: setBinaryDecoder() must be called AFTER mount(). The component needs a componentId (assigned by the server on mount) to register the binary handler.
Use DataView with typed fields. Best for fixed schemas with numbers.
// Shared between server and client (e.g. app/shared/codec/trackerCodec.ts)
export function encode(delta: Record<string, any>): Uint8Array {
let flags = 0, size = 1
if ('x' in delta) { flags |= 0x01; size += 4 }
if ('y' in delta) { flags |= 0x02; size += 4 }
const buf = new ArrayBuffer(size)
const dv = new DataView(buf)
let off = 0
dv.setUint8(off, flags); off += 1
if (flags & 0x01) { dv.setFloat32(off, delta.x, true); off += 4 }
if (flags & 0x02) { dv.setFloat32(off, delta.y, true); off += 4 }
return new Uint8Array(buf)
}
export function decode(buffer: Uint8Array): Record<string, any> {
const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
let off = 0
const flags = dv.getUint8(off); off += 1
const result: Record<string, any> = {}
if (flags & 0x01) { result.x = dv.getFloat32(off, true); off += 4 }
if (flags & 0x02) { result.y = dv.getFloat32(off, true); off += 4 }
return result
}Tip: Put codec files in app/shared/ so both server and client can import them.
If you want binary transport without writing a custom codec, just JSON-encode into bytes:
function encode(delta: Record<string, any>): Uint8Array {
return new TextEncoder().encode(JSON.stringify(delta))
}
function decode(buffer: Uint8Array): Record<string, any> {
return JSON.parse(new TextDecoder().decode(buffer))
}This still bypasses the JSON batcher (lower latency) but doesn't save bandwidth. Good for prototyping before writing a proper codec.
For state with many optional fields (like game state with tanks, bullets, explosions), use bitmask flags to indicate which fields are present:
// Field presence flags
const FLAG_TANKS = 0x01
const FLAG_BULLETS = 0x02
const FLAG_EXPLOSIONS = 0x04
function encode(delta: Record<string, any>): Uint8Array {
let flags = 0
if (delta.tanks) flags |= FLAG_TANKS
if (delta.bullets) flags |= FLAG_BULLETS
if (delta.explosions) flags |= FLAG_EXPLOSIONS
// Calculate total size, allocate buffer, write fields...
// See the full game codec example below
}Helper functions for encoding/decoding strings inside binary payloads:
const textEncoder = new TextEncoder()
const textDecoder = new TextDecoder()
// Write: [1 byte length][N bytes UTF-8]
function writeString(dv: DataView, offset: number, str: string): number {
const bytes = textEncoder.encode(str)
dv.setUint8(offset, bytes.length) // max 255 chars
offset += 1
for (let i = 0; i < bytes.length; i++) {
dv.setUint8(offset + i, bytes[i])
}
return offset + bytes.length
}
// Read: [1 byte length][N bytes UTF-8]
function readString(dv: DataView, offset: number): [string, number] {
const len = dv.getUint8(offset)
offset += 1
const bytes = new Uint8Array(dv.buffer, dv.byteOffset + offset, len)
return [textDecoder.decode(bytes), offset + len]
}This codec is used by Battle Tanks to encode tanks, bullets, explosions, and laser beams into a single binary frame. It uses bitmask flags, DataView for typed fields, and string helpers for IDs.
// app/shared/codec/gameCodec.ts
interface TankDynamic {
id: string
x: number
z: number
rot: number
tRot: number
hp: number
alive: boolean
laserCharge: number
}
const FLAG_TANKS = 0x01
const FLAG_BULLETS = 0x02
const FLAG_EXPLOSIONS = 0x04
const FLAG_LASERS = 0x08
export function encodeGameState(delta: Record<string, any>): Uint8Array {
let size = 1 + 4 // flags (1B) + matchTime (4B)
let flags = 0
const tanks: TankDynamic[] | undefined = delta.tanks
if (tanks) {
flags |= FLAG_TANKS
size += 2 // tank count (uint16)
for (const t of tanks) {
const idBytes = new TextEncoder().encode(t.id)
// 1B idLen + id + 4 floats (x,z,rot,tRot) + hp (2B) + alive (1B) + laserCharge (4B)
size += 1 + idBytes.length + 16 + 2 + 1 + 4
}
}
// ... similar for bullets, explosions, lasers ...
const buffer = new ArrayBuffer(size)
const dv = new DataView(buffer)
let offset = 0
dv.setUint8(offset, flags); offset += 1
dv.setUint32(offset, delta.matchTime ?? 0, true); offset += 4
if (tanks) {
dv.setUint16(offset, tanks.length, true); offset += 2
for (const t of tanks) {
offset = writeString(dv, offset, t.id)
dv.setFloat32(offset, t.x, true); offset += 4
dv.setFloat32(offset, t.z, true); offset += 4
dv.setFloat32(offset, t.rot, true); offset += 4
dv.setFloat32(offset, t.tRot, true); offset += 4
dv.setUint16(offset, t.hp, true); offset += 2
dv.setUint8(offset, t.alive ? 1 : 0); offset += 1
dv.setFloat32(offset, t.laserCharge, true); offset += 4
}
}
return new Uint8Array(buffer)
}
export function decodeGameState(buffer: Uint8Array): Record<string, any> {
const dv = new DataView(buffer.buffer, buffer.byteOffset, buffer.byteLength)
let offset = 0
const flags = dv.getUint8(offset); offset += 1
const matchTime = dv.getUint32(offset, true); offset += 4
const result: Record<string, any> = { matchTime }
if (flags & FLAG_TANKS) {
const count = dv.getUint16(offset, true); offset += 2
const tanks: TankDynamic[] = []
for (let i = 0; i < count; i++) {
let id: string
;[id, offset] = readString(dv, offset)
const x = dv.getFloat32(offset, true); offset += 4
const z = dv.getFloat32(offset, true); offset += 4
const rot = dv.getFloat32(offset, true); offset += 4
const tRot = dv.getFloat32(offset, true); offset += 4
const hp = dv.getUint16(offset, true); offset += 2
const alive = dv.getUint8(offset) === 1; offset += 1
const laserCharge = dv.getFloat32(offset, true); offset += 4
tanks.push({ id, x, z, rot, tRot, hp, alive, laserCharge })
}
result.tanks = tanks
}
// ... similar for bullets, explosions, lasers ...
return result
}import { encodeGameState } from '@app/shared/codec/gameCodec'
export class LiveBattleTanks extends LiveComponent<typeof LiveBattleTanks.defaultState> {
static componentName = 'LiveBattleTanks'
static singleton = true
static publicActions = ['join', 'move', 'shoot'] as const
static defaultState = {
tanks: [] as TankDynamic[],
bullets: [] as any[],
explosions: [] as any[],
matchTime: 0
}
private _loop?: ReturnType<typeof setInterval>
protected onMount() {
// Game loop at 30fps
this._loop = setInterval(() => {
this.tick()
this.sendBinaryDelta(
{
tanks: this.state.tanks,
bullets: this.state.bullets,
explosions: this.state.explosions,
matchTime: this.state.matchTime
},
encodeGameState
)
}, 33)
}
protected onDestroy() {
clearInterval(this._loop)
}
private tick() {
// Update physics, process collisions, etc.
this.state.matchTime += 33
}
}import { useLiveComponent } from '@fluxstack/live-react'
import { decodeGameState } from '@app/shared/codec/gameCodec'
export function BattleTanks() {
const { state, call } = useLiveComponent('LiveBattleTanks', {
initialState: { tanks: [], bullets: [], explosions: [], matchTime: 0 },
binaryDecoder: decodeGameState
})
// Render game using state.tanks, state.bullets, etc.
return <GameCanvas tanks={state.tanks} bullets={state.bullets} />
}For a game with 8 tanks, 20 bullets, and 3 explosions at 30fps:
| Method | Payload Size | Per Second | Savings |
|---|---|---|---|
JSON (setState) |
~2.4 KB | ~72 KB/s | baseline |
| Binary (DataView) | ~0.5 KB | ~15 KB/s | ~80% |
Binary encoding is especially effective when state contains many numeric fields (floats, integers) since JSON encodes numbers as variable-length text while DataView uses fixed-size typed representations.
setState |
sendBinaryDelta |
|
|---|---|---|
| Format | JSON | Custom binary |
| Batching | Merged per microtask | Immediate send |
| Deduplication | Yes (by componentId) | No |
| Encoder | Built-in (JSON.stringify) | You provide it |
| Client decoder | Built-in (JSON.parse) | You provide it |
| Best for | Low-frequency, readable data | High-frequency, compact data |
| State update | Yes | Yes (same internal behavior) |
Both methods update internal state identically. The difference is only in how the data is serialized and sent over the wire.
- Encoder and decoder must be symmetric - what you encode, you must decode in the same order and format
- Put codec files in
app/shared/so server and client share the same code sendBinaryDeltaonly sends fields that actually changed (same diffing assetState)- Binary frames bypass the JSON batcher and message deduplication
- Use
setBinaryDecoder()only AFTERmount()(vanilla JS client) - With React, just pass
binaryDecoderin options - lifecycle is handled automatically - If both
setStateandsendBinaryDeltaare used on the same component, the client handles both (JSON messages go through the normal path, binary frames go through the decoder)
Core (Server)
packages/core/src/component/LiveComponent.ts-sendBinaryDelta()methodpackages/core/src/component/managers/ComponentStateManager.ts- Wire format implementation
Client (Browser)
packages/client/src/component.ts-setBinaryDecoder()methodpackages/client/src/connection.ts-handleBinaryMessage()+registerBinaryHandler()
React
packages/react/src/hooks/useLiveComponent.ts-binaryDecoderoption inUseLiveComponentOptions
Tests
packages/core/src/__tests__/component/LiveComponent.binary.test.ts- Wire format and behavior testspackages/core/src/__tests__/component/fixtures/gameCodec.ts- Full game codec example
- Live Components - Core Live Component documentation
- Live Upload - Chunked file upload (different binary protocol)
- Live Rooms - Multi-room communication