Cross-platform frameworks (React Native's legacy bridge, Flutter platform channels) move every call between the JS/Dart layer and native code across a queue that must serialize the payload to a wire format, hop threads, and deserialize it. Each crossing ("packet") costs a fixed setup fee plus a per-byte encoding fee:
cost(batch) = F + n · s · k
F = fixed bridge overhead (~0.9 ms)
n = messages in the batch
s = bytes per message payload
k = serialize/deserialize cost per byte (~0.0006 ms/B)
Batching amortizes F across many messages — bigger batches raise throughput per crossing. But a batch only flushes once it hits the batch size cap or the oldest queued call reaches the batch window, so a message waits in queue before it even starts crossing. And because the native/UI thread processes an arriving batch synchronously, a crossing that costs more than one frame's budget (~16.7 ms at 60 fps) blocks rendering for that frame — a dropped, janky frame.
- Call rate — how many native calls the JS side fires per second.
- Payload size — bytes serialized per call (a JSON object, an image buffer reference, etc.).
- Batch size cap — max messages coalesced into one crossing; 1 = no batching.
- Batch window — max time a call waits for the batch to fill before it flushes anyway.
Real-world relevance: this is exactly why React Native moved to JSI/TurboModules (synchronous, no serialization) and why Flutter's platform channels recommend batching high-frequency calls — the fixed-cost-vs-frame-budget trade-off modeled here is the actual bottleneck native app teams profile in production.