[{"data":1,"prerenderedAt":1436},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying":1427},{"id":4,"title":5,"body":6,"description":1407,"extension":1408,"meta":1409,"navigation":578,"path":1421,"seo":1422,"stem":1425,"__hash__":1426},"content\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying\u002Findex.md","Transferring ArrayBuffers to Workers Without Copying",{"type":7,"value":8,"toc":1392},"minimark",[9,14,34,59,172,177,217,221,237,243,249,255,348,352,357,447,450,454,461,692,695,699,702,861,864,868,875,882,1018,1022,1025,1201,1205,1228,1232,1278,1282,1300,1317,1330,1350,1354,1377,1382,1385,1388],[10,11,13],"h1",{"id":12},"how-to-transfer-arraybuffers-to-web-workers-without-copying","How to Transfer ArrayBuffers to Web Workers Without Copying",[15,16,17,18,23,24,28,29,33],"p",{},"This guide refines ",[19,20,22],"a",{"href":21},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002F","Offloading Work to Web Workers with Comlink"," within ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",". Moving work to a worker removes its computation from the main thread — but the data still has to get there. By default, ",[30,31,32],"code",{},"postMessage"," uses the structured clone algorithm: it serialises the data on the sending thread and deserialises it on the receiving one. For a 20MB image buffer or a large typed array of samples, the serialisation alone can take 20–60ms on the main thread, long enough to fail an interaction that triggered it.",[15,35,36,37,40,41,44,45,44,48,44,51,54,55,58],{},"Transferable objects avoid the copy. When you list an ",[30,38,39],{},"ArrayBuffer"," (or a ",[30,42,43],{},"MessagePort",", ",[30,46,47],{},"ImageBitmap",[30,49,50],{},"OffscreenCanvas",[30,52,53],{},"ReadableStream"," and a few others) in the transfer list, ownership moves to the receiving thread in constant time. The sender's reference becomes detached — its ",[30,56,57],{},"byteLength"," drops to zero — and the receiver gets the same memory without any copying.",[15,60,61],{},[62,63,69,70,69,77,69,81,69,84,69,93,69,99,69,108,69,114,69,119,69,124,69,127,69,130,69,133,69,136,69,139,69,142,69,147,69,151,69,153,69,157,69,159,69,162,69,164,69,167,69,169,69],"svg",{"viewBox":64,"width":65,"role":66,"ariaLabel":67,"style":68},"0 0 760 228","100%","img","Comparison of sending a large ArrayBuffer to a worker by structured clone, which copies on the main thread, and by transfer, which moves ownership in constant time.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[71,72],"rect",{"className":73,"x":75,"y":75,"width":65,"height":65,"fill":76},[74],"svg-canvas","0","#ffffff",[78,79,80],"title",{},"Structured clone vs transfer",[82,83,67],"desc",{},[71,85],{"x":86,"y":86,"width":87,"height":88,"rx":89,"fill":90,"stroke":91,"style":92},"1","758","226","10","none","currentColor","stroke-opacity:0.18",[94,95,80],"text",{"x":96,"y":97,"fill":91,"style":98},"28.0","34.0","font-size:16px;font-weight:700",[71,100],{"x":96,"y":101,"width":102,"height":103,"rx":104,"fill":105,"stroke":106,"style":107},"56.0","340.0","150.0","6","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[94,109,113],{"x":110,"y":111,"fill":91,"style":112},"42.0","82.0","font-size:14px;font-weight:700","Structured clone (default)",[94,115,118],{"x":110,"y":116,"fill":91,"style":117},"108.0","font-size:12.5px;font-weight:700","•",[94,120,123],{"x":101,"y":116,"fill":91,"style":121,"textAnchor":122},"font-size:12.5px","start","Serialise 24MB on the main thread",[94,125,118],{"x":110,"y":126,"fill":91,"style":117},"132.0",[94,128,129],{"x":101,"y":126,"fill":91,"style":121,"textAnchor":122},"Both threads hold a copy (48MB peak)",[94,131,118],{"x":110,"y":132,"fill":91,"style":117},"156.0",[94,134,135],{"x":101,"y":132,"fill":91,"style":121,"textAnchor":122},"~35ms main-thread cost on mid-tier mobile",[94,137,118],{"x":110,"y":138,"fill":91,"style":117},"180.0",[94,140,141],{"x":101,"y":138,"fill":91,"style":121,"textAnchor":122},"Sender keeps using its buffer",[71,143],{"x":144,"y":101,"width":102,"height":103,"rx":104,"fill":145,"stroke":145,"style":146},"392.0","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[94,148,150],{"x":149,"y":111,"fill":91,"style":112},"406.0","Transfer",[94,152,118],{"x":149,"y":116,"fill":91,"style":117},[94,154,156],{"x":155,"y":116,"fill":91,"style":121,"textAnchor":122},"420.0","Ownership moves, no serialisation",[94,158,118],{"x":149,"y":126,"fill":91,"style":117},[94,160,161],{"x":155,"y":126,"fill":91,"style":121,"textAnchor":122},"One copy in memory",[94,163,118],{"x":149,"y":132,"fill":91,"style":117},[94,165,166],{"x":155,"y":132,"fill":91,"style":121,"textAnchor":122},"Well under 1ms regardless of size",[94,168,118],{"x":149,"y":138,"fill":91,"style":117},[94,170,171],{"x":155,"y":138,"fill":91,"style":121,"textAnchor":122},"Sender's buffer is detached",[173,174,176],"h2",{"id":175},"rapid-diagnosis","Rapid Diagnosis",[178,179,180,191,201,207],"ul",{},[181,182,183,187,188,190],"li",{},[184,185,186],"strong",{},"Find postMessage costs in the trace."," In the Performance panel, a \"Serialize\" or structured-clone block inside the task that calls ",[30,189,32],{}," (or Comlink's proxy call) reveals copying.",[181,192,193,196,197,200],{},[184,194,195],{},"Measure payload sizes."," Log ",[30,198,199],{},"buffer.byteLength"," for data sent to workers. Anything above a few hundred kilobytes is a candidate for transfer.",[181,202,203,206],{},[184,204,205],{},"Check the receiving side too."," Results sent back from the worker are cloned onto the main thread on receipt; a large result costs main-thread time on arrival.",[181,208,209,212,213,216],{},[184,210,211],{},"Look for objects containing typed arrays."," A plain object holding several ",[30,214,215],{},"Float32Array","s is cloned in full unless the underlying buffers are listed as transferables.",[173,218,220],{"id":219},"root-cause-analysis","Root Cause Analysis",[15,222,223,69,226,229,230,232,233,236],{},[184,224,225],{},"1. Default cloning.",[30,227,228],{},"postMessage(data)"," without a transfer list always clones. Comlink calls are ",[30,231,32],{}," calls; arguments and return values are cloned unless wrapped with ",[30,234,235],{},"Comlink.transfer",".",[15,238,239,242],{},[184,240,241],{},"2. Results cloned back."," The worker returns a large array or object; the main thread pays deserialisation on receipt, inside whatever task handles the message.",[15,244,245,248],{},[184,246,247],{},"3. Non-transferable structures."," Arrays of plain objects (JSON-like data) cannot be transferred, only cloned. Their clone cost scales with object count, not just bytes.",[15,250,251,254],{},[184,252,253],{},"4. Accidental reuse after transfer."," Code that transfers a buffer and then reads it gets a detached, zero-length buffer — often failing silently.",[15,256,257],{},[62,258,69,261,69,264,69,267,69,269,69,272,69,274,69,284,69,291,69,299,69,304,69,308,69,312,69,316,69,320,69,324,69,328,69,332,69,337,69,341,69],{"viewBox":259,"width":65,"role":66,"ariaLabel":260,"style":68},"0 0 760 216","Bar chart of main-thread time to send different payloads to a worker by clone and by transfer.",[71,262],{"className":263,"x":75,"y":75,"width":65,"height":65,"fill":76},[74],[78,265,266],{},"Main-thread cost of sending data to a worker (mid-tier phone)",[82,268,260],{},[71,270],{"x":86,"y":86,"width":87,"height":271,"rx":89,"fill":90,"stroke":91,"style":92},"214",[94,273,266],{"x":96,"y":97,"fill":91,"style":98},[275,276],"line",{"x1":277,"y1":278,"x2":277,"y2":279,"stroke":106,"strokeWidth":280,"strokeDashArray":281,"style":283},"580.1","72.0","194.0","1.5",[282,282],"4","stroke-opacity:0.9",[94,285,290],{"x":286,"y":287,"fill":91,"style":288,"textAnchor":289},"168.8","92.0","font-size:13px","end","4MB buffer, clone",[71,292],{"x":293,"y":294,"width":295,"height":296,"rx":297,"fill":145,"stroke":145,"style":298},"180.8","78.0","55.9","19","3","fill-opacity:0.55;stroke-opacity:0.9",[94,300,303],{"x":301,"y":287,"fill":91,"style":302},"242.7","font-size:12px;font-weight:600","7ms",[94,305,307],{"x":286,"y":306,"fill":91,"style":288,"textAnchor":289},"123.0","24MB buffer, clone",[71,309],{"x":293,"y":310,"width":311,"height":296,"rx":297,"fill":145,"stroke":145,"style":298},"109.0","287.6",[94,313,315],{"x":314,"y":306,"fill":91,"style":302},"474.3","36ms",[94,317,319],{"x":286,"y":318,"fill":91,"style":288,"textAnchor":289},"154.0","24MB buffer, transfer",[71,321],{"x":293,"y":322,"width":323,"height":296,"rx":297,"fill":145,"stroke":145,"style":298},"140.0","2.0",[94,325,327],{"x":326,"y":318,"fill":91,"style":302},"188.8","0.2ms",[94,329,331],{"x":286,"y":330,"fill":91,"style":288,"textAnchor":289},"185.0","50k JSON objects, clone",[71,333],{"x":293,"y":334,"width":335,"height":296,"rx":297,"fill":105,"stroke":106,"style":336},"171.0","487.2","fill-opacity:0.7;stroke-opacity:0.9",[94,338,340],{"x":339,"y":330,"fill":91,"style":302},"674.0","61ms",[94,342,347],{"x":277,"y":343,"fill":344,"style":345,"textAnchor":346},"68.0","#8a6608","font-size:12px;font-weight:700","middle","50ms long task",[173,349,351],{"id":350},"step-by-step-resolution","Step-by-Step Resolution",[353,354,356],"h3",{"id":355},"_1-transfer-buffers-with-raw-postmessage","1. Transfer buffers with raw postMessage",[358,359,364],"pre",{"className":360,"code":361,"language":362,"meta":363,"style":363},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","const pixels = ctx.getImageData(0, 0, w, h).data.buffer;    \u002F\u002F ArrayBuffer\nworker.postMessage({ type: 'blur', width: w, height: h, pixels }, [pixels]);\n\u002F\u002F pixels.byteLength === 0 now — the main thread no longer owns it.\n\u002F\u002F trade-off: after transfer the main thread cannot read the buffer. If you need\n\u002F\u002F to keep the original (for undo, say), copy it once deliberately with\n\u002F\u002F pixels.slice(0) and transfer the copy, accepting the copy cost explicitly.\n","javascript","",[30,365,366,405,423,429,435,441],{"__ignoreMap":363},[367,368,370,374,378,381,385,389,392,394,396,398,401],"span",{"class":275,"line":369},1,[367,371,373],{"class":372},"sPARh","const",[367,375,377],{"class":376},"sPXB4"," pixels",[367,379,380],{"class":372}," =",[367,382,384],{"class":383},"saISM"," ctx.",[367,386,388],{"class":387},"smZ65","getImageData",[367,390,391],{"class":383},"(",[367,393,75],{"class":376},[367,395,44],{"class":383},[367,397,75],{"class":376},[367,399,400],{"class":383},", w, h).data.buffer;    ",[367,402,404],{"class":403},"sjfSM","\u002F\u002F ArrayBuffer\n",[367,406,408,411,413,416,420],{"class":275,"line":407},2,[367,409,410],{"class":383},"worker.",[367,412,32],{"class":387},[367,414,415],{"class":383},"({ type: ",[367,417,419],{"class":418},"sZ8jY","'blur'",[367,421,422],{"class":383},", width: w, height: h, pixels }, [pixels]);\n",[367,424,426],{"class":275,"line":425},3,[367,427,428],{"class":403},"\u002F\u002F pixels.byteLength === 0 now — the main thread no longer owns it.\n",[367,430,432],{"class":275,"line":431},4,[367,433,434],{"class":403},"\u002F\u002F trade-off: after transfer the main thread cannot read the buffer. If you need\n",[367,436,438],{"class":275,"line":437},5,[367,439,440],{"class":403},"\u002F\u002F to keep the original (for undo, say), copy it once deliberately with\n",[367,442,444],{"class":275,"line":443},6,[367,445,446],{"class":403},"\u002F\u002F pixels.slice(0) and transfer the copy, accepting the copy cost explicitly.\n",[15,448,449],{},"Expected outcome: send cost drops to under a millisecond regardless of size.",[353,451,453],{"id":452},"_2-transfer-arguments-and-results-with-comlink","2. Transfer arguments and results with Comlink",[15,455,456,457,460],{},"Comlink exposes ",[30,458,459],{},"Comlink.transfer(value, transferables)"," for both directions.",[358,462,464],{"className":360,"code":463,"language":362,"meta":363,"style":363},"\u002F\u002F main.js\nimport * as Comlink from 'comlink';\nconst api = Comlink.wrap(new Worker(new URL('.\u002Fimage-worker.js', import.meta.url), { type: 'module' }));\nconst out = await api.blur(Comlink.transfer(pixels, [pixels]), w, h);\n\n\u002F\u002F image-worker.js\nComlink.expose({\n  blur(buffer, w, h) {\n    const result = applyBlur(new Uint8ClampedArray(buffer), w, h);\n    return Comlink.transfer(result.buffer, [result.buffer]);   \u002F\u002F zero-copy back\n  },\n});\n\u002F\u002F trade-off: Comlink.transfer marks a value for transfer on that single call.\n\u002F\u002F Forgetting it on the return value is the most common mistake — the result is\n\u002F\u002F silently cloned back onto the main thread.\n",[30,465,466,471,494,547,574,580,585,597,622,646,662,668,674,680,686],{"__ignoreMap":363},[367,467,468],{"class":275,"line":369},[367,469,470],{"class":403},"\u002F\u002F main.js\n",[367,472,473,476,479,482,485,488,491],{"class":275,"line":407},[367,474,475],{"class":372},"import",[367,477,478],{"class":376}," *",[367,480,481],{"class":372}," as",[367,483,484],{"class":383}," Comlink ",[367,486,487],{"class":372},"from",[367,489,490],{"class":418}," 'comlink'",[367,492,493],{"class":383},";\n",[367,495,496,498,501,503,506,509,511,514,517,519,521,524,526,529,531,533,535,538,541,544],{"class":275,"line":425},[367,497,373],{"class":372},[367,499,500],{"class":376}," api",[367,502,380],{"class":372},[367,504,505],{"class":383}," Comlink.",[367,507,508],{"class":387},"wrap",[367,510,391],{"class":383},[367,512,513],{"class":372},"new",[367,515,516],{"class":387}," Worker",[367,518,391],{"class":383},[367,520,513],{"class":372},[367,522,523],{"class":387}," URL",[367,525,391],{"class":383},[367,527,528],{"class":418},"'.\u002Fimage-worker.js'",[367,530,44],{"class":383},[367,532,475],{"class":372},[367,534,236],{"class":383},[367,536,537],{"class":376},"meta",[367,539,540],{"class":383},".url), { type: ",[367,542,543],{"class":418},"'module'",[367,545,546],{"class":383}," }));\n",[367,548,549,551,554,556,559,562,565,568,571],{"class":275,"line":431},[367,550,373],{"class":372},[367,552,553],{"class":376}," out",[367,555,380],{"class":372},[367,557,558],{"class":372}," await",[367,560,561],{"class":383}," api.",[367,563,564],{"class":387},"blur",[367,566,567],{"class":383},"(Comlink.",[367,569,570],{"class":387},"transfer",[367,572,573],{"class":383},"(pixels, [pixels]), w, h);\n",[367,575,576],{"class":275,"line":437},[367,577,579],{"emptyLinePlaceholder":578},true,"\n",[367,581,582],{"class":275,"line":443},[367,583,584],{"class":403},"\u002F\u002F image-worker.js\n",[367,586,588,591,594],{"class":275,"line":587},7,[367,589,590],{"class":383},"Comlink.",[367,592,593],{"class":387},"expose",[367,595,596],{"class":383},"({\n",[367,598,600,603,605,609,611,614,616,619],{"class":275,"line":599},8,[367,601,602],{"class":387},"  blur",[367,604,391],{"class":383},[367,606,608],{"class":607},"sQw3B","buffer",[367,610,44],{"class":383},[367,612,613],{"class":607},"w",[367,615,44],{"class":383},[367,617,618],{"class":607},"h",[367,620,621],{"class":383},") {\n",[367,623,625,628,631,633,636,638,640,643],{"class":275,"line":624},9,[367,626,627],{"class":372},"    const",[367,629,630],{"class":376}," result",[367,632,380],{"class":372},[367,634,635],{"class":387}," applyBlur",[367,637,391],{"class":383},[367,639,513],{"class":372},[367,641,642],{"class":387}," Uint8ClampedArray",[367,644,645],{"class":383},"(buffer), w, h);\n",[367,647,649,652,654,656,659],{"class":275,"line":648},10,[367,650,651],{"class":372},"    return",[367,653,505],{"class":383},[367,655,570],{"class":387},[367,657,658],{"class":383},"(result.buffer, [result.buffer]);   ",[367,660,661],{"class":403},"\u002F\u002F zero-copy back\n",[367,663,665],{"class":275,"line":664},11,[367,666,667],{"class":383},"  },\n",[367,669,671],{"class":275,"line":670},12,[367,672,673],{"class":383},"});\n",[367,675,677],{"class":275,"line":676},13,[367,678,679],{"class":403},"\u002F\u002F trade-off: Comlink.transfer marks a value for transfer on that single call.\n",[367,681,683],{"class":275,"line":682},14,[367,684,685],{"class":403},"\u002F\u002F Forgetting it on the return value is the most common mistake — the result is\n",[367,687,689],{"class":275,"line":688},15,[367,690,691],{"class":403},"\u002F\u002F silently cloned back onto the main thread.\n",[15,693,694],{},"Expected outcome: both directions are zero-copy; only small metadata is cloned.",[353,696,698],{"id":697},"_3-convert-object-arrays-into-columnar-typed-arrays","3. Convert object arrays into columnar typed arrays",[15,700,701],{},"Large arrays of records clone slowly. Store numeric fields in typed arrays (one per column) so they can be transferred.",[358,703,705],{"className":360,"code":704,"language":362,"meta":363,"style":363},"function toColumns(rows) {\n  const n = rows.length;\n  const price = new Float64Array(n), stock = new Int32Array(n);\n  rows.forEach((r, i) => { price[i] = r.price; stock[i] = r.stock; });\n  return { price, stock };\n}\nconst cols = toColumns(rows);\nworker.postMessage(cols, [cols.price.buffer, cols.stock.buffer]);\n\u002F\u002F trade-off: building columns costs a pass over the data on the main thread.\n\u002F\u002F Do it once when data arrives (or in the worker that fetched it), not on\n\u002F\u002F every interaction.\n",[30,706,707,722,740,771,810,818,823,837,846,851,856],{"__ignoreMap":363},[367,708,709,712,715,717,720],{"class":275,"line":369},[367,710,711],{"class":372},"function",[367,713,714],{"class":387}," toColumns",[367,716,391],{"class":383},[367,718,719],{"class":607},"rows",[367,721,621],{"class":383},[367,723,724,727,730,732,735,738],{"class":275,"line":407},[367,725,726],{"class":372},"  const",[367,728,729],{"class":376}," n",[367,731,380],{"class":372},[367,733,734],{"class":383}," rows.",[367,736,737],{"class":376},"length",[367,739,493],{"class":383},[367,741,742,744,747,749,752,755,758,761,763,765,768],{"class":275,"line":425},[367,743,726],{"class":372},[367,745,746],{"class":376}," price",[367,748,380],{"class":372},[367,750,751],{"class":372}," new",[367,753,754],{"class":387}," Float64Array",[367,756,757],{"class":383},"(n), ",[367,759,760],{"class":376},"stock",[367,762,380],{"class":372},[367,764,751],{"class":372},[367,766,767],{"class":387}," Int32Array",[367,769,770],{"class":383},"(n);\n",[367,772,773,776,779,782,785,787,790,793,796,799,802,805,807],{"class":275,"line":431},[367,774,775],{"class":383},"  rows.",[367,777,778],{"class":387},"forEach",[367,780,781],{"class":383},"((",[367,783,784],{"class":607},"r",[367,786,44],{"class":383},[367,788,789],{"class":607},"i",[367,791,792],{"class":383},") ",[367,794,795],{"class":372},"=>",[367,797,798],{"class":383}," { price[i] ",[367,800,801],{"class":372},"=",[367,803,804],{"class":383}," r.price; stock[i] ",[367,806,801],{"class":372},[367,808,809],{"class":383}," r.stock; });\n",[367,811,812,815],{"class":275,"line":437},[367,813,814],{"class":372},"  return",[367,816,817],{"class":383}," { price, stock };\n",[367,819,820],{"class":275,"line":443},[367,821,822],{"class":383},"}\n",[367,824,825,827,830,832,834],{"class":275,"line":587},[367,826,373],{"class":372},[367,828,829],{"class":376}," cols",[367,831,380],{"class":372},[367,833,714],{"class":387},[367,835,836],{"class":383},"(rows);\n",[367,838,839,841,843],{"class":275,"line":599},[367,840,410],{"class":383},[367,842,32],{"class":387},[367,844,845],{"class":383},"(cols, [cols.price.buffer, cols.stock.buffer]);\n",[367,847,848],{"class":275,"line":624},[367,849,850],{"class":403},"\u002F\u002F trade-off: building columns costs a pass over the data on the main thread.\n",[367,852,853],{"class":275,"line":648},[367,854,855],{"class":403},"\u002F\u002F Do it once when data arrives (or in the worker that fetched it), not on\n",[367,857,858],{"class":275,"line":664},[367,859,860],{"class":403},"\u002F\u002F every interaction.\n",[15,862,863],{},"Expected outcome: payloads that were tens of milliseconds to clone become effectively free to send.",[353,865,867],{"id":866},"_4-fetch-large-data-in-the-worker-directly","4. Fetch large data in the worker directly",[15,869,870,871,874],{},"The cheapest transfer is none. If the main thread only fetches data to hand it to a worker, let the worker ",[30,872,873],{},"fetch"," it instead; the response body never touches the main thread.",[15,876,877,878,236],{},"Expected outcome: no main-thread serialisation and no main-thread parsing — see ",[19,879,881],{"href":880},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fmoving-heavy-json-parsing-off-the-main-thread\u002F","moving heavy JSON parsing off the main thread",[15,883,884],{},[62,885,69,888,69,891,69,894,69,896,69,913,69,916,69,918,69,923,69,928,69,931,69,935,69,942,69,948,69,952,69,958,69,961,69,965,69,967,69,970,69,973,69,976,69,979,69,982,69,985,69,989,69,991,69,994,69,997,69,1000,69,1004,69,1007,69,1011,69,1015,69],{"viewBox":886,"width":65,"role":66,"ariaLabel":887,"style":68},"0 0 760 341","Decision sequence for choosing between structured clone, transfer and SharedArrayBuffer when sending data to a worker.",[71,889],{"className":890,"x":75,"y":75,"width":65,"height":65,"fill":76},[74],[78,892,893],{},"Clone, transfer or share?",[82,895,887],{},[897,898,899],"defs",{},[900,901,908],"marker",{"id":902,"viewBox":903,"refX":904,"refY":905,"markerWidth":906,"markerHeight":906,"orient":907},"fa8ce7ab96","0 0 10 10","9","5","7","auto-start-reverse",[909,910],"path",{"d":911,"fill":91,"style":912},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[71,914],{"x":86,"y":86,"width":87,"height":915,"rx":89,"fill":90,"stroke":91,"style":92},"339",[94,917,893],{"x":96,"y":97,"fill":91,"style":98},[71,919],{"x":96,"y":101,"width":920,"height":921,"rx":104,"fill":91,"stroke":91,"style":922},"320.0","35.0","fill-opacity:0.06;stroke-opacity:0.4",[94,924,927],{"x":925,"y":294,"fill":91,"style":926,"textAnchor":346},"188.0","font-size:13px;font-weight:700","Payload under ~100KB?",[71,929],{"x":930,"y":101,"width":920,"height":921,"rx":104,"fill":145,"stroke":145,"style":146},"412.0",[94,932,934],{"x":933,"y":294,"fill":91,"style":121,"textAnchor":346},"572.0","Clone — the cost is negligible",[275,936],{"x1":937,"y1":938,"x2":939,"y2":938,"stroke":91,"strokeWidth":280,"style":940,"markerEnd":941},"348.0","73.5","410.0","stroke-opacity:0.6","url(#fa8ce7ab96)",[94,943,947],{"x":944,"y":945,"fill":145,"style":946,"textAnchor":346},"380.0","66.5","font-size:11.5px;font-weight:700","yes",[275,949],{"x1":925,"y1":950,"x2":925,"y2":951,"stroke":91,"strokeWidth":280,"style":940,"markerEnd":941},"91.0","125.0",[94,953,957],{"x":954,"y":955,"fill":956,"style":946},"196.0","113.0","#51617a","no",[71,959],{"x":96,"y":960,"width":920,"height":921,"rx":104,"fill":91,"stroke":91,"style":922},"127.0",[94,962,964],{"x":925,"y":963,"fill":91,"style":926,"textAnchor":346},"149.0","Is it an ArrayBuffer or typed array?",[71,966],{"x":930,"y":960,"width":920,"height":921,"rx":104,"fill":145,"stroke":145,"style":146},[94,968,969],{"x":933,"y":963,"fill":91,"style":121,"textAnchor":346},"Transfer it in the transfer list",[275,971],{"x1":937,"y1":972,"x2":939,"y2":972,"stroke":91,"strokeWidth":280,"style":940,"markerEnd":941},"144.5",[94,974,947],{"x":944,"y":975,"fill":145,"style":946,"textAnchor":346},"137.5",[275,977],{"x1":925,"y1":978,"x2":925,"y2":954,"stroke":91,"strokeWidth":280,"style":940,"markerEnd":941},"162.0",[94,980,957],{"x":954,"y":981,"fill":956,"style":946},"184.0",[71,983],{"x":96,"y":984,"width":920,"height":921,"rx":104,"fill":91,"stroke":91,"style":922},"198.0",[94,986,988],{"x":925,"y":987,"fill":91,"style":926,"textAnchor":346},"220.0","Both threads need it at the same time?",[71,990],{"x":930,"y":984,"width":920,"height":921,"rx":104,"fill":145,"stroke":145,"style":146},[94,992,993],{"x":933,"y":987,"fill":91,"style":121,"textAnchor":346},"SharedArrayBuffer (needs cross-origin isolation)",[275,995],{"x1":937,"y1":996,"x2":939,"y2":996,"stroke":91,"strokeWidth":280,"style":940,"markerEnd":941},"215.5",[94,998,947],{"x":944,"y":999,"fill":145,"style":946,"textAnchor":346},"208.5",[275,1001],{"x1":925,"y1":1002,"x2":925,"y2":1003,"stroke":91,"strokeWidth":280,"style":940,"markerEnd":941},"233.0","267.0",[94,1005,957],{"x":954,"y":1006,"fill":956,"style":946},"255.0",[71,1008],{"x":96,"y":1009,"width":920,"height":1010,"rx":104,"fill":105,"stroke":106,"style":107},"269.0","50.0",[94,1012,1014],{"x":925,"y":1013,"fill":91,"style":121,"textAnchor":346},"290.5","Convert records to columnar typed arrays, then",[94,1016,570],{"x":925,"y":1017,"fill":91,"style":121,"textAnchor":346},"306.5",[173,1019,1021],{"id":1020},"verification","Verification",[15,1023,1024],{},"Record the interaction that sends data to the worker. The structured-clone block should be gone from the main-thread task, and the task should be short regardless of payload size. Add an assertion in development builds that detects accidental cloning of large data:",[358,1026,1028],{"className":360,"code":1027,"language":362,"meta":363,"style":363},"if (import.meta.env.DEV) {\n  const orig = Worker.prototype.postMessage;\n  Worker.prototype.postMessage = function (msg, transfer) {\n    const size = msg?.byteLength ?? msg?.buffer?.byteLength ?? 0;\n    if (size > 1_000_000 && !transfer?.length) console.warn('Large postMessage without transfer', size);\n    return orig.call(this, msg, transfer);\n  };\n}\n\u002F\u002F trade-off: this only inspects top-level buffers; nested ones inside objects\n\u002F\u002F slip through. It is a development tripwire, not a guarantee.\n",[30,1029,1030,1052,1071,1100,1125,1164,1182,1187,1191,1196],{"__ignoreMap":363},[367,1031,1032,1035,1038,1040,1042,1044,1047,1050],{"class":275,"line":369},[367,1033,1034],{"class":372},"if",[367,1036,1037],{"class":383}," (",[367,1039,475],{"class":372},[367,1041,236],{"class":383},[367,1043,537],{"class":376},[367,1045,1046],{"class":383},".env.",[367,1048,1049],{"class":376},"DEV",[367,1051,621],{"class":383},[367,1053,1054,1056,1059,1061,1063,1065,1068],{"class":275,"line":407},[367,1055,726],{"class":372},[367,1057,1058],{"class":376}," orig",[367,1060,380],{"class":372},[367,1062,516],{"class":376},[367,1064,236],{"class":383},[367,1066,1067],{"class":376},"prototype",[367,1069,1070],{"class":383},".postMessage;\n",[367,1072,1073,1076,1078,1080,1082,1084,1086,1089,1091,1094,1096,1098],{"class":275,"line":425},[367,1074,1075],{"class":376},"  Worker",[367,1077,236],{"class":383},[367,1079,1067],{"class":376},[367,1081,236],{"class":383},[367,1083,32],{"class":387},[367,1085,380],{"class":372},[367,1087,1088],{"class":372}," function",[367,1090,1037],{"class":383},[367,1092,1093],{"class":607},"msg",[367,1095,44],{"class":383},[367,1097,570],{"class":607},[367,1099,621],{"class":383},[367,1101,1102,1104,1107,1109,1112,1115,1118,1120,1123],{"class":275,"line":431},[367,1103,627],{"class":372},[367,1105,1106],{"class":376}," size",[367,1108,380],{"class":372},[367,1110,1111],{"class":383}," msg?.byteLength ",[367,1113,1114],{"class":372},"??",[367,1116,1117],{"class":383}," msg?.buffer?.byteLength ",[367,1119,1114],{"class":372},[367,1121,1122],{"class":376}," 0",[367,1124,493],{"class":383},[367,1126,1127,1130,1133,1136,1139,1142,1145,1148,1150,1153,1156,1158,1161],{"class":275,"line":437},[367,1128,1129],{"class":372},"    if",[367,1131,1132],{"class":383}," (size ",[367,1134,1135],{"class":372},">",[367,1137,1138],{"class":376}," 1_000_000",[367,1140,1141],{"class":372}," &&",[367,1143,1144],{"class":372}," !",[367,1146,1147],{"class":383},"transfer?.",[367,1149,737],{"class":376},[367,1151,1152],{"class":383},") console.",[367,1154,1155],{"class":387},"warn",[367,1157,391],{"class":383},[367,1159,1160],{"class":418},"'Large postMessage without transfer'",[367,1162,1163],{"class":383},", size);\n",[367,1165,1166,1168,1171,1174,1176,1179],{"class":275,"line":443},[367,1167,651],{"class":372},[367,1169,1170],{"class":383}," orig.",[367,1172,1173],{"class":387},"call",[367,1175,391],{"class":383},[367,1177,1178],{"class":376},"this",[367,1180,1181],{"class":383},", msg, transfer);\n",[367,1183,1184],{"class":275,"line":587},[367,1185,1186],{"class":383},"  };\n",[367,1188,1189],{"class":275,"line":599},[367,1190,822],{"class":383},[367,1192,1193],{"class":275,"line":624},[367,1194,1195],{"class":403},"\u002F\u002F trade-off: this only inspects top-level buffers; nested ones inside objects\n",[367,1197,1198],{"class":275,"line":648},[367,1199,1200],{"class":403},"\u002F\u002F slip through. It is a development tripwire, not a guarantee.\n",[173,1202,1204],{"id":1203},"sharedarraybuffer-when-transfer-is-not-enough","SharedArrayBuffer: When Transfer Is Not Enough",[15,1206,1207,1208,1211,1212,1215,1216,1219,1220,1223,1224,1227],{},"Transfer moves ownership: only one thread can use the buffer at a time. If both threads need to read and write the same memory concurrently — a worker filling a ring buffer of audio samples while the main thread draws them — ",[30,1209,1210],{},"SharedArrayBuffer"," provides genuinely shared memory, coordinated with ",[30,1213,1214],{},"Atomics",". It requires the page to be cross-origin isolated, which means serving ",[30,1217,1218],{},"Cross-Origin-Opener-Policy: same-origin"," and ",[30,1221,1222],{},"Cross-Origin-Embedder-Policy: require-corp"," (or ",[30,1225,1226],{},"credentialless","), and that can break third-party embeds that do not opt in. For most UI workloads, transferring buffers back and forth is simpler and sufficient; reach for shared memory only for continuous streaming workloads where the transfer round trip is itself the bottleneck.",[173,1229,1231],{"id":1230},"common-mistakes-with-transferables","Common Mistakes with Transferables",[178,1233,1234,1252,1262,1272],{},[181,1235,1236,1239,1240,1243,1244,1247,1248,1251],{},[184,1237,1238],{},"Transferring a buffer still referenced by a view you keep using."," Any ",[30,1241,1242],{},"Uint8Array"," or ",[30,1245,1246],{},"DataView"," over the transferred buffer becomes zero-length; reads return ",[30,1249,1250],{},"undefined"," and writes are silently dropped. Re-create views after receiving the buffer back.",[181,1253,1254,1257,1258,1261],{},[184,1255,1256],{},"Transferring the same buffer twice in one message."," Listing a buffer twice in the transfer array throws a ",[30,1259,1260],{},"DataCloneError",". Deduplicate the transfer list when building it from several views.",[181,1263,1264,1267,1268,1271],{},[184,1265,1266],{},"Transferring buffers owned by a library."," Some libraries keep internal references to buffers they hand you (canvas ",[30,1269,1270],{},"ImageData",", WebAssembly memory). Transferring those breaks the library; copy instead.",[181,1273,1274,1277],{},[184,1275,1276],{},"Forgetting the round trip."," For a pipeline where the worker returns modified data, transfer in both directions — otherwise half the benefit is lost on the way back.",[173,1279,1281],{"id":1280},"faq","FAQ",[1283,1284,1287,1291],"details",{"className":1285},[1286],"faq-item",[1288,1289,1290],"summary",{},"Can I transfer a typed array directly?",[15,1292,1293,1294,1037,1296,1299],{},"You transfer its underlying ",[30,1295,39],{},[30,1297,1298],{},"typedArray.buffer","). The typed array object itself is cloned (cheaply — it is a small view) and on the receiving side it refers to the transferred buffer. Be careful with views over part of a larger buffer: transferring the buffer moves all of it, including bytes other views depend on.",[1283,1301,1303,1306],{"className":1302},[1286],[1288,1304,1305],{},"Is transfer available everywhere?",[15,1307,1308,1309,1219,1311,1313,1314,1316],{},"Transferring ",[30,1310,39],{},[30,1312,43],{}," is supported in all modern browsers. Transferring streams, ",[30,1315,50],{}," and some other types has narrower support; feature-detect or fall back to cloning for those.",[1283,1318,1320,1323],{"className":1319},[1286],[1288,1321,1322],{},"Does Comlink proxy objects instead of copying them?",[15,1324,1325,1326,1329],{},"Only if you ask: ",[30,1327,1328],{},"Comlink.proxy(obj)"," sends a reference that turns method calls into messages. Proxied objects avoid copying but make every access a round trip, which is slow for data access. Use proxies for callbacks and transfer for bulk data.",[1283,1331,1333,1336],{"className":1332},[1286],[1288,1334,1335],{},"How do I know whether cloning is actually my bottleneck?",[15,1337,1338,1339,1341,1342,1345,1346,1349],{},"Time it directly: wrap the ",[30,1340,32],{}," call (or the Comlink call up to its first ",[30,1343,1344],{},"await",") in ",[30,1347,1348],{},"performance.now()"," measurements on a throttled device. If the call itself takes more than a few milliseconds, cloning is significant. Also check the receiving side: a long task at the start of the worker's message handler, before your code runs, is deserialisation. For payloads under roughly 100KB, cloning is rarely worth optimising.",[173,1351,1353],{"id":1352},"related","Related",[178,1355,1356,1363,1370],{},[181,1357,1358,1362],{},[19,1359,1361],{"href":1360},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F","Comlink vs raw postMessage for workers"," — the abstraction cost and when to skip it.",[181,1364,1365,1369],{},[19,1366,1368],{"href":1367},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker\u002F","Running client-side search in a Web Worker"," — a complete worker workload.",[181,1371,1372,1376],{},[19,1373,1375],{"href":1374},"\u002Fimage-media-optimization\u002Fimage-decoding-and-placeholders\u002Favoiding-main-thread-image-decode-jank\u002F","Avoiding main-thread image decode jank"," — moving image processing off-thread.",[1378,1379,1381],"script",{"type":1380},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Transfer ArrayBuffers to Web Workers Without Copying\",\n  \"description\": \"How structured cloning costs main-thread time, how transferable objects avoid it, and how to use them safely with Comlink.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Transfer buffers with raw postMessage\",\n      \"text\": \"Expected outcome: send cost drops to under a millisecond regardless of size.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Transfer arguments and results with Comlink\",\n      \"text\": \"Comlink exposes Comlink.transfer(value, transferables) for both directions.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Convert object arrays into columnar typed arrays\",\n      \"text\": \"Large arrays of records clone slowly.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Fetch large data in the worker directly\",\n      \"text\": \"The cheapest transfer is none.\"\n    }\n  ]\n}\n",[1378,1383,1384],{"type":1380},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Transfer ArrayBuffers to Web Workers Without Copying\",\n  \"description\": \"How structured cloning costs main-thread time, how transferable objects avoid it, and how to use them safely with Comlink.\",\n  \"datePublished\": \"2026-10-06\",\n  \"dateModified\": \"2026-10-06\",\n  \"author\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"publisher\": {\n    \"@type\": \"Organization\",\n    \"name\": \"frontend-performance.com\"\n  },\n  \"mainEntityOfPage\": {\n    \"@type\": \"WebPage\",\n    \"@id\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying\u002F\"\n  }\n}\n",[1378,1386,1387],{"type":1380},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 1,\n      \"name\": \"Home\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 2,\n      \"name\": \"Core Web Vitals & Measurement\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 3,\n      \"name\": \"Offloading Work to Web Workers with Comlink\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002F\"\n    },\n    {\n      \"@type\": \"ListItem\",\n      \"position\": 4,\n      \"name\": \"Transferring ArrayBuffers to Workers Without Copying\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying\u002F\"\n    }\n  ]\n}\n",[1389,1390,1391],"style",{},"html pre.shiki code .sPARh, html code.shiki .sPARh{--shiki-default:#A0111F;--shiki-dark:#FF9492;--shiki-light:#A0111F}html pre.shiki code .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}html pre.shiki code .saISM, html code.shiki .saISM{--shiki-default:#0E1116;--shiki-dark:#F0F3F6;--shiki-light:#0E1116}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}html pre.shiki code .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}",{"title":363,"searchDepth":407,"depth":407,"links":1393},[1394,1395,1396,1402,1403,1404,1405,1406],{"id":175,"depth":407,"text":176},{"id":219,"depth":407,"text":220},{"id":350,"depth":407,"text":351,"children":1397},[1398,1399,1400,1401],{"id":355,"depth":425,"text":356},{"id":452,"depth":425,"text":453},{"id":697,"depth":425,"text":698},{"id":866,"depth":425,"text":867},{"id":1020,"depth":407,"text":1021},{"id":1203,"depth":407,"text":1204},{"id":1230,"depth":407,"text":1231},{"id":1280,"depth":407,"text":1281},{"id":1352,"depth":407,"text":1353},"How structured cloning costs main-thread time, how transferable objects avoid it, and how to use them safely with Comlink.","md",{"slug":1410,"type":1411,"breadcrumb":1412,"datePublished":1420,"dateModified":1420},"transferring-arraybuffers-to-workers-without-copying","article",[1413,1416,1417,1418],{"name":1414,"url":1415},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1419},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying\u002F","2026-10-06","\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying",{"title":1423,"description":1424},"Transferring ArrayBuffers to Web Workers Without Copying","postMessage copies data with structured clone, blocking the main thread for tens of milliseconds. Use transferables and Comlink.transfer for zero-copy hand-off.","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying\u002Findex","JjJf1R5VBvtXJfRlTJzKe-y3expDmuLx1NqfLCI6kw0",[1428,1432],{"title":1429,"path":1430,"stem":1431,"children":-1},"Sharing One Worker Across Tabs with SharedWorker","\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker\u002Findex",{"title":1433,"path":1434,"stem":1435,"children":-1},"Optimizing First Input Delay (FID) and INP","\u002Fcore-web-vitals-measurement\u002Foptimizing-first-input-delay-fid","core-web-vitals-measurement\u002Foptimizing-first-input-delay-fid\u002Findex",1791308073873]