Browser & Frontend Runtime
Structured clone and transferables
You move a 40 MB image-processing job to a worker — and the main thread still freezes for 40 ms before the worker even starts. The work moved, but the data did not.
The structured clone algorithm
postMessage(data) does not share data — it deep-copies it using the structured clone algorithm. Structured clone handles objects, arrays, Maps, Sets, Dates, typed arrays, Blobs, and ArrayBuffers. It does not handle:
- Functions (dropped silently)
- DOM nodes (throws)
- Class prototypes — methods are lost, only data properties survive
The copy is synchronous on the sending thread and scales with payload size: roughly 1 ms per MB for plain objects, more for deeply nested structures. Send a 50 MB object and you have blocked the sending thread for ~50 ms — you moved the work, but paid a clone tax to do it.
- Plain object clone rate
- ~1 ms / MB
- 30 MB object clone
- ~30 ms sender block
- Transfer (ArrayBuffer)
- O(1), microseconds
- Transferables: ArrayBuffer
- Detaches sender (byteLength → 0)
- Transferables: ImageBitmap
- Transferred, source becomes closed
- Transferables: OffscreenCanvas
- Exclusive ownership moves
Transferables: O(1) ownership handoff
The escape from clone cost is the transferable mechanism. Pass a second argument to postMessage — a list of transferable objects:
postMessage({ pixels, width, height }, [pixels.buffer]);Instead of copying the bytes, the browser hands the underlying memory to the receiving thread and detaches it from the sender. After the transfer:
pixels.buffer.byteLengthis0on the sender- The receiver has a new view with the full data
The cost drops from O(size) to O(1). Transfer is the right move for large binary payloads: image bitmaps, audio buffers, file contents.
Transferable types: ArrayBuffer, MessagePort, ImageBitmap, OffscreenCanvas, ReadableStream, WritableStream, TransformStream.
What gets dropped in structured clone
When you postMessage an object that had methods:
class Point {
constructor(x, y) { this.x = x; this.y = y; }
distanceTo(other) { ... }
}
const p = new Point(1, 2);
worker.postMessage(p);
// Worker receives: { x: 1, y: 2 } — no distanceTo methodStructured clone copies data properties but has no representation for functions or prototype chains. This is by design: functions reference closures that may hold DOM nodes or other main-thread-only state. The rule: serialize data, not behaviour. Reconstruct the class on the receiving side if needed.
You `postMessage` a 40 MB Float32Array to a worker and the sending thread freezes for ~40 ms. What is the fix?
A worker's `onmessage` receives an object that had methods on it before being sent. The methods are gone. Why?
When you postMessage a large ArrayBuffer, the default deep-copies it (the structured clone tax). The alternative moves ownership of the buffer to the receiving thread in constant time, leaving it unusable on the sender. What is that alternative called?
Structured clone of a plain object costs roughly 1 ms per MB. A worker is sent a 30 MB object by value. Roughly how many milliseconds does the sending thread block on the clone?
Transfer a buffer to a worker instead of cloning it
1/3Which specification defines the structured clone algorithm used by postMessage, IndexedDB, and the Cache API?
Why this works
Why is structured clone synchronous and why does it block the sender? Because the JavaScript specification guarantees that a postMessage call is a single, atomic operation — the data is fully serialized before the function returns. If serialization were asynchronous, you could mutate the data between the postMessage call and the point where the worker reads it, breaking isolation. The synchronous cost is the price of the isolation guarantee. The transfer mechanism gets around this by moving a memory pointer instead of bytes — the OS can hand ownership of a memory region atomically without copying.
- 01What does structured clone handle and what does it drop?
- 02How do you transfer an ArrayBuffer instead of cloning it, and what happens to the sender's reference?
- 03When should you use SharedArrayBuffer instead of transfer?
postMessage deep-copies data via the structured clone algorithm at ~1 ms per MB — a 30 MB object blocks the sending thread for ~30 ms, erasing the benefit of moving work off the main thread. The fix is the transferable mechanism: pass the ArrayBuffer in postMessage’s second argument and the browser hands ownership to the receiver in O(1), detaching the sender’s reference. Functions and prototype methods cannot cross the boundary — structured clone is a data serializer, not an object copier. For cases where both threads need concurrent access to the same memory, SharedArrayBuffer is the answer, but it requires cross-origin isolation.
appears again in41
- Federation and lookahead: batching beyond DataLoadermiddle
- Senior GraphQL API: scheduling contract, tenant isolation, observabilitysenior
- Lock and single-flight: bounding concurrent rebuildsmiddle
- Stale-while-revalidate and CDN request coalescingmiddle
- Detecting stampedes and designing TTL for productionmiddle
- Metastable failure, fencing tokens, and production postmortemssenior
- What a relation is: tables, rows, keys, and constraintsjunior
- Constraints, keys, and Postgres data typesmiddle
- JSONB, arrays, and when a side table winsmiddle
- Schema integrity: deferral, versioning, and production failure modessenior
- Where data fetching happens — and why it decides LCPjunior
- React Server Components and Suspense streamingmiddle
- Senior internals: RSC payload, caching layers, and production failure modessenior
- The IP envelopejunior
- Reading the IP headermiddle
- What TLS does and why it existsjunior
- Key schedule, SNI, ALPN, and extensionssenior
- 0-RTT defenses, ECH, hybrid PQ, and production TLSsenior
- The twelve layers: one URL, seven actorsjunior
- Resilience: cascading retries, circuit breakers, and error budgetssenior
- What is OpenTelemetry: API, SDK, Collector, OTLPjunior
- OTel signals, Semantic Conventions, and the OTLP wire formatmiddle
- The OTel Collector: receivers, processors, exporters, and deployment patternsmiddle
- Vendor neutrality, eBPF instrumentation, the Operator, and browser/serverless OTelsenior
- Operating the OTel Collector: reliability, version skew, failure modes, and governancesenior
- What is trace propagation and why broken propagation is worse than nonejunior
- traceparent and tracestate: the W3C header format in fullmiddle
- Baggage and async boundaries: carrying context across queues and callbacksmiddle
- Async context per language, service mesh, B3 migration, and securitysenior
- Production propagation failures, span links, and platform designsenior
- The debugging funnel: SLO → RED → trace → profilejunior
- OTel architecture: one SDK, four signals, one wire formatmiddle
- The incident loop: from pager to postmortem to preventionmiddle
- Scale, security, and the ROI of observable systemssenior
- At-most-once, at-least-once, exactly-once: the three delivery contractsjunior
- Consumer-side dedup: the cheapest path to exactly-once processingmiddle
- Exactly-once in production: impossibility proof, hybrid patterns, and real incidentssenior
- What OAuth is and why passwords are not the answerjunior
- Authorization code flow with PKCEmiddle
- Sender-constrained tokens: DPoP and mTLSsenior
- OAuth in production: audience attacks, observability, and real failuressenior