[{"data":1,"prerenderedAt":1181},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F":1173},{"id":4,"title":5,"body":6,"description":1152,"extension":1153,"meta":1154,"navigation":1166,"path":1167,"seo":1168,"stem":1171,"__hash__":1172},"content\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002Findex.md","Comlink vs Raw postMessage for Workers",{"type":7,"value":8,"toc":1140},"minimark",[9,14,34,52,153,158,317,321,324,467,714,717,728,732,757,762,765,904,910,914,934,948,965,969,979,989,1009,1013,1036,1089,1092,1096,1125,1130,1133,1136],[10,11,13],"h1",{"id":12},"comlink-vs-raw-postmessage-for-web-workers-which-abstraction-to-pick","Comlink vs Raw postMessage for Web Workers: Which Abstraction to Pick",[15,16,17,18,23,24,28,29,33],"p",{},"This comparison sits under ",[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",", part of the interactivity work in ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",": once you have decided to move heavy work off the main thread to protect INP under ",[30,31,32],"code",{},"200ms",", you still have to choose how the main thread talks to the worker.",[15,35,36,37,43,44,47,48,51],{},"Both options run identical CPU off the main thread, so neither is \"faster\" at the work itself — the choice is about the communication layer. ",[19,38,42],{"href":39,"rel":40},"https:\u002F\u002Fgithub.com\u002FGoogleChromeLabs\u002Fcomlink",[41],"nofollow","Comlink"," wraps the worker in a Proxy so ",[30,45,46],{},"await worker.parseJSON(text)"," reads like a local async call. Raw ",[30,49,50],{},"postMessage"," is the platform primitive: you send a message, the worker replies with another message, and you wire up the correlation yourself. The decision turns on call complexity, bundle budget, per-call overhead, and how much debugging friction you can absorb.",[15,53,54],{},[55,56,63,64,63,68,63,72,63,82,63,89,63,97,63,103,63,108,63,112,63,117,63,121,63,125,63,129,63,133,63,136,63,139,63,142,63,145,63,148,63],"svg",{"xmlns":57,"viewBox":58,"width":59,"role":60,"ariaLabel":61,"style":62},"http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","0 0 760 290","100%","img","Comlink wraps the worker as proxied async methods, while raw postMessage requires a hand-rolled message protocol with id correlation","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:#001d3d"," ",[65,66,67],"title",{},"Comlink proxy vs raw postMessage protocol",[69,70,71],"desc",{},"Comlink exposes an object of methods called as async functions; raw postMessage needs a manual id-correlated message switch.",[73,74],"rect",{"x":75,"y":75,"width":76,"height":77,"rx":78,"fill":79,"stroke":80,"style":81},"1","758","288","10","none","currentColor","stroke-opacity:0.18",[83,84,88],"text",{"x":85,"y":86,"fill":80,"style":87},"24","38","font-size:18px;font-weight:700","Two ways across the worker boundary",[73,90],{"x":85,"y":91,"width":92,"height":93,"rx":94,"fill":95,"stroke":95,"style":96},"60","340","200","8","#0466c8","fill-opacity:0.08",[73,98],{"x":99,"y":91,"width":92,"height":93,"rx":94,"fill":100,"stroke":101,"style":102},"396","#ffc300","#b8860b","fill-opacity:0.10",[83,104,42],{"x":105,"y":106,"fill":80,"style":107},"194","86","font-size:14px;font-weight:700;text-anchor:middle",[83,109,111],{"x":110,"y":106,"fill":80,"style":107},"566","Raw postMessage",[83,113,116],{"x":105,"y":114,"fill":80,"style":115},"120","font-size:13px;text-anchor:middle","await api.parse(text)",[83,118,120],{"x":105,"y":119,"fill":80,"style":115},"148","methods read as local",[83,122,124],{"x":105,"y":123,"fill":80,"style":115},"176","+1.1KB, 1 async hop",[83,126,128],{"x":105,"y":127,"fill":80,"style":115},"204","errors reject promise",[83,130,132],{"x":105,"y":131,"fill":80,"style":115},"232","typed proxy",[83,134,135],{"x":110,"y":114,"fill":80,"style":115},"postMessage + onmessage",[83,137,138],{"x":110,"y":119,"fill":80,"style":115},"manual id correlation",[83,140,141],{"x":110,"y":123,"fill":80,"style":115},"0 bytes, full control",[83,143,144],{"x":110,"y":127,"fill":80,"style":115},"hand-rolled errors",[83,146,147],{"x":110,"y":131,"fill":80,"style":115},"untyped payloads",[83,149,152],{"x":85,"y":150,"fill":80,"style":151},"280","font-size:13px","Same off-thread compute and clone cost; only the communication layer differs.",[154,155,157],"h2",{"id":156},"decision-matrix","Decision Matrix",[159,160,161,175],"table",{},[162,163,164],"thead",{},[165,166,167,171,173],"tr",{},[168,169,170],"th",{},"Dimension",[168,172,42],{},[168,174,111],{},[176,177,178,193,206,222,239,258,274,287,304],"tbody",{},[165,179,180,187,190],{},[181,182,183],"td",{},[184,185,186],"strong",{},"Ergonomics",[181,188,189],{},"High — proxied async methods, return values, thrown errors propagate as rejections",[181,191,192],{},"Low — manual message protocol, request\u002Fresponse correlation, hand-rolled error handling",[165,194,195,200,203],{},[181,196,197],{},[184,198,199],{},"Bundle size",[181,201,202],{},"+~1.1KB gzipped (both threads)",[181,204,205],{},"0 bytes — platform built-in",[165,207,208,213,216],{},[181,209,210],{},[184,211,212],{},"Per-call overhead",[181,214,215],{},"One Proxy hop + an internal message-id round-trip on top of the post",[181,217,218,219,221],{},"Bare ",[30,220,50],{}," cost only",[165,223,224,229,232],{},[181,225,226],{},[184,227,228],{},"Multiple methods",[181,230,231],{},"Trivial — expose an object of functions",[181,233,234,235,238],{},"Verbose — a ",[30,236,237],{},"type"," switch grows with every method",[165,240,241,246,252],{},[181,242,243],{},[184,244,245],{},"Transferables",[181,247,248,249],{},"Supported via ",[30,250,251],{},"Comlink.transfer(value, [buf])",[181,253,254,255],{},"Native — second arg to ",[30,256,257],{},"postMessage(msg, [buf])",[165,259,260,265,271],{},[181,261,262],{},[184,263,264],{},"Callbacks \u002F progress",[181,266,267,270],{},[30,268,269],{},"Comlink.proxy(fn)"," proxies a callback into the worker",[181,272,273],{},"Manual back-messages with your own correlation",[165,275,276,281,284],{},[181,277,278],{},[184,279,280],{},"Debugging",[181,282,283],{},"One extra abstraction in stack traces; needs worker source maps",[181,285,286],{},"Transparent — messages are visible and inspectable",[165,288,289,294,301],{},[181,290,291],{},[184,292,293],{},"Type safety",[181,295,296,297,300],{},"Strong with TypeScript — ",[30,298,299],{},"Comlink.Remote\u003CT>"," types the proxy",[181,302,303],{},"Weak — message payloads are untyped unless you hand-write guards",[165,305,306,311,314],{},[181,307,308],{},[184,309,310],{},"Best fit",[181,312,313],{},"Rich, multi-method worker APIs called from app code",[181,315,316],{},"One hot function, or a tight high-frequency message loop",[154,318,320],{"id":319},"ergonomics-where-comlink-earns-its-place","Ergonomics: Where Comlink Earns Its Place",[15,322,323],{},"The gap is starkest with more than one method. Compare a two-method worker.",[325,326,331],"pre",{"className":327,"code":328,"language":329,"meta":330,"style":330},"language-javascript shiki shiki-themes github-light-high-contrast github-light-high-contrast github-light-high-contrast","\u002F\u002F Comlink: the worker API is just an object; the main thread awaits methods.\nimport * as Comlink from 'comlink';\nComlink.expose({\n  parse: (text) => JSON.parse(text),\n  hash: (buf) => crypto.subtle.digest('SHA-256', buf),\n});\n\u002F\u002F main thread:  const api = Comlink.wrap(worker);  await api.parse(text);\n\u002F\u002F trade-off: the Proxy hides the boundary so well that engineers forget every call is\n\u002F\u002F async + cloned, and start calling it in tight loops — keep calls coarse-grained.\n","javascript","",[30,332,333,342,370,383,413,443,449,455,461],{"__ignoreMap":330},[334,335,338],"span",{"class":336,"line":337},"line",1,[334,339,341],{"class":340},"sIIH1","\u002F\u002F Comlink: the worker API is just an object; the main thread awaits methods.\n",[334,343,345,349,353,356,360,363,367],{"class":336,"line":344},2,[334,346,348],{"class":347},"sP5qI","import",[334,350,352],{"class":351},"sf6mN"," *",[334,354,355],{"class":347}," as",[334,357,359],{"class":358},"syybb"," Comlink ",[334,361,362],{"class":347},"from",[334,364,366],{"class":365},"s-_DF"," 'comlink'",[334,368,369],{"class":358},";\n",[334,371,373,376,380],{"class":336,"line":372},3,[334,374,375],{"class":358},"Comlink.",[334,377,379],{"class":378},"ssM3C","expose",[334,381,382],{"class":358},"({\n",[334,384,386,389,392,395,398,401,404,407,410],{"class":336,"line":385},4,[334,387,388],{"class":378},"  parse",[334,390,391],{"class":358},": (",[334,393,83],{"class":394},"seIZK",[334,396,397],{"class":358},") ",[334,399,400],{"class":347},"=>",[334,402,403],{"class":351}," JSON",[334,405,406],{"class":358},".",[334,408,409],{"class":378},"parse",[334,411,412],{"class":358},"(text),\n",[334,414,416,419,421,424,426,428,431,434,437,440],{"class":336,"line":415},5,[334,417,418],{"class":378},"  hash",[334,420,391],{"class":358},[334,422,423],{"class":394},"buf",[334,425,397],{"class":358},[334,427,400],{"class":347},[334,429,430],{"class":358}," crypto.subtle.",[334,432,433],{"class":378},"digest",[334,435,436],{"class":358},"(",[334,438,439],{"class":365},"'SHA-256'",[334,441,442],{"class":358},", buf),\n",[334,444,446],{"class":336,"line":445},6,[334,447,448],{"class":358},"});\n",[334,450,452],{"class":336,"line":451},7,[334,453,454],{"class":340},"\u002F\u002F main thread:  const api = Comlink.wrap(worker);  await api.parse(text);\n",[334,456,458],{"class":336,"line":457},8,[334,459,460],{"class":340},"\u002F\u002F trade-off: the Proxy hides the boundary so well that engineers forget every call is\n",[334,462,464],{"class":336,"line":463},9,[334,465,466],{"class":340},"\u002F\u002F async + cloned, and start calling it in tight loops — keep calls coarse-grained.\n",[325,468,470],{"className":327,"code":469,"language":329,"meta":330,"style":330},"\u002F\u002F Raw postMessage: you own the protocol, the id correlation, and the error path.\nconst pending = new Map();\nlet seq = 0;\nfunction call(method, args, transfer = []) {\n  const id = seq++;\n  return new Promise((resolve, reject) => {\n    pending.set(id, { resolve, reject });\n    worker.postMessage({ id, method, args }, transfer);\n  });\n}\nworker.onmessage = ({ data }) => {\n  const p = pending.get(data.id); pending.delete(data.id);\n  data.error ? p.reject(data.error) : p.resolve(data.result);\n};\n\u002F\u002F trade-off: this is ~30 lines you now own and test forever; it's the right cost only\n\u002F\u002F when you have ONE worker call and don't want a dependency, or you need full control.\n",[30,471,472,477,497,513,542,560,588,599,609,614,620,644,669,696,702,708],{"__ignoreMap":330},[334,473,474],{"class":336,"line":337},[334,475,476],{"class":340},"\u002F\u002F Raw postMessage: you own the protocol, the id correlation, and the error path.\n",[334,478,479,482,485,488,491,494],{"class":336,"line":344},[334,480,481],{"class":347},"const",[334,483,484],{"class":351}," pending",[334,486,487],{"class":347}," =",[334,489,490],{"class":347}," new",[334,492,493],{"class":378}," Map",[334,495,496],{"class":358},"();\n",[334,498,499,502,505,508,511],{"class":336,"line":372},[334,500,501],{"class":347},"let",[334,503,504],{"class":358}," seq ",[334,506,507],{"class":347},"=",[334,509,510],{"class":351}," 0",[334,512,369],{"class":358},[334,514,515,518,521,523,526,529,532,534,537,539],{"class":336,"line":385},[334,516,517],{"class":347},"function",[334,519,520],{"class":378}," call",[334,522,436],{"class":358},[334,524,525],{"class":394},"method",[334,527,528],{"class":358},", ",[334,530,531],{"class":394},"args",[334,533,528],{"class":358},[334,535,536],{"class":394},"transfer",[334,538,487],{"class":347},[334,540,541],{"class":358}," []) {\n",[334,543,544,547,550,552,555,558],{"class":336,"line":415},[334,545,546],{"class":347},"  const",[334,548,549],{"class":351}," id",[334,551,487],{"class":347},[334,553,554],{"class":358}," seq",[334,556,557],{"class":347},"++",[334,559,369],{"class":358},[334,561,562,565,567,570,573,576,578,581,583,585],{"class":336,"line":445},[334,563,564],{"class":347},"  return",[334,566,490],{"class":347},[334,568,569],{"class":351}," Promise",[334,571,572],{"class":358},"((",[334,574,575],{"class":394},"resolve",[334,577,528],{"class":358},[334,579,580],{"class":394},"reject",[334,582,397],{"class":358},[334,584,400],{"class":347},[334,586,587],{"class":358}," {\n",[334,589,590,593,596],{"class":336,"line":451},[334,591,592],{"class":358},"    pending.",[334,594,595],{"class":378},"set",[334,597,598],{"class":358},"(id, { resolve, reject });\n",[334,600,601,604,606],{"class":336,"line":457},[334,602,603],{"class":358},"    worker.",[334,605,50],{"class":378},[334,607,608],{"class":358},"({ id, method, args }, transfer);\n",[334,610,611],{"class":336,"line":463},[334,612,613],{"class":358},"  });\n",[334,615,617],{"class":336,"line":616},10,[334,618,619],{"class":358},"}\n",[334,621,623,626,629,631,634,637,640,642],{"class":336,"line":622},11,[334,624,625],{"class":358},"worker.",[334,627,628],{"class":378},"onmessage",[334,630,487],{"class":347},[334,632,633],{"class":358}," ({ ",[334,635,636],{"class":394},"data",[334,638,639],{"class":358}," }) ",[334,641,400],{"class":347},[334,643,587],{"class":358},[334,645,647,649,652,654,657,660,663,666],{"class":336,"line":646},12,[334,648,546],{"class":347},[334,650,651],{"class":351}," p",[334,653,487],{"class":347},[334,655,656],{"class":358}," pending.",[334,658,659],{"class":378},"get",[334,661,662],{"class":358},"(data.id); pending.",[334,664,665],{"class":378},"delete",[334,667,668],{"class":358},"(data.id);\n",[334,670,672,675,678,681,683,686,689,691,693],{"class":336,"line":671},13,[334,673,674],{"class":358},"  data.error ",[334,676,677],{"class":347},"?",[334,679,680],{"class":358}," p.",[334,682,580],{"class":378},[334,684,685],{"class":358},"(data.error) ",[334,687,688],{"class":347},":",[334,690,680],{"class":358},[334,692,575],{"class":378},[334,694,695],{"class":358},"(data.result);\n",[334,697,699],{"class":336,"line":698},14,[334,700,701],{"class":358},"};\n",[334,703,705],{"class":336,"line":704},15,[334,706,707],{"class":340},"\u002F\u002F trade-off: this is ~30 lines you now own and test forever; it's the right cost only\n",[334,709,711],{"class":336,"line":710},16,[334,712,713],{"class":340},"\u002F\u002F when you have ONE worker call and don't want a dependency, or you need full control.\n",[15,715,716],{},"The raw version is essentially a hand-rolled subset of what Comlink ships in 1.1KB. The moment a worker grows past one or two methods, the maintenance cost of the manual protocol exceeds the bundle cost of Comlink.",[15,718,719,720,723,724,727],{},"The hidden cost of the raw protocol is correctness, not just keystrokes. The ",[30,721,722],{},"pending"," map, the sequence counter, and the error-path branch are each places a real bug lives: a leaked map entry when a worker never replies, a swallowed rejection when ",[30,725,726],{},"data.error"," is set but falsy, an off-by-one if two callers race the counter. These are the bugs that ship quietly and surface as a stuck spinner in production. Comlink has paid down that surface once, with tests, for every consumer. Writing it again per project means re-testing it per project — and most teams do not, which is how a worker layer accumulates intermittent hangs nobody can reproduce.",[154,729,731],{"id":730},"overhead-when-the-proxy-hop-matters","Overhead: When the Proxy Hop Matters",[15,733,734,735,737,738,741,742,745,746,748,749,752,753,756],{},"Comlink's per-call cost is the Proxy trap plus its internal message-id bookkeeping layered on the same structured-clone ",[30,736,50],{}," that raw code uses. For coarse-grained calls — one ",[30,739,740],{},"await api.parse(bigText)"," that runs ",[30,743,744],{},"300ms"," off-thread — the overhead is unmeasurable noise. It only matters in a high-frequency loop: thousands of tiny round-trips per second, such as a per-frame physics step or a streaming protocol pushing many small messages. There, the abstraction's fixed per-message cost compounds and a hand-tuned raw ",[30,747,50],{}," (or a ",[30,750,751],{},"MessageChannel"," with a binary protocol) wins. Both abstractions still pay the same structured-clone cost for non-transferable payloads, so the marshalling discipline from the ",[19,754,755],{"href":21},"parent guide on transferables"," applies either way.",[758,759,761],"h3",{"id":760},"measuring-the-dispatch-cost-yourself","Measuring the dispatch cost yourself",[15,763,764],{},"If you suspect the message layer is hot, measure it rather than guess. Time a no-op round-trip on both abstractions over many iterations and compare against the cost of the actual work.",[325,766,768],{"className":327,"code":767,"language":329,"meta":330,"style":330},"async function benchRoundTrip(callFn, n = 1000) {\n  const start = performance.now();\n  for (let i = 0; i \u003C n; i++) await callFn(); \u002F\u002F a no-op worker method\n  return (performance.now() - start) \u002F n;     \u002F\u002F mean ms per round-trip\n  \u002F\u002F trade-off: this measures dispatch with an empty payload, so it ISOLATES the proxy\n  \u002F\u002F overhead but ignores clone cost — re-run with a realistic payload before deciding,\n  \u002F\u002F because for real work the clone usually dwarfs the proxy hop on both abstractions.\n}\n",[30,769,770,799,816,858,885,890,895,900],{"__ignoreMap":330},[334,771,772,775,778,781,783,786,788,791,793,796],{"class":336,"line":337},[334,773,774],{"class":347},"async",[334,776,777],{"class":347}," function",[334,779,780],{"class":378}," benchRoundTrip",[334,782,436],{"class":358},[334,784,785],{"class":394},"callFn",[334,787,528],{"class":358},[334,789,790],{"class":394},"n",[334,792,487],{"class":347},[334,794,795],{"class":351}," 1000",[334,797,798],{"class":358},") {\n",[334,800,801,803,806,808,811,814],{"class":336,"line":344},[334,802,546],{"class":347},[334,804,805],{"class":351}," start",[334,807,487],{"class":347},[334,809,810],{"class":358}," performance.",[334,812,813],{"class":378},"now",[334,815,496],{"class":358},[334,817,818,821,824,826,829,831,833,836,839,842,844,846,849,852,855],{"class":336,"line":372},[334,819,820],{"class":347},"  for",[334,822,823],{"class":358}," (",[334,825,501],{"class":347},[334,827,828],{"class":358}," i ",[334,830,507],{"class":347},[334,832,510],{"class":351},[334,834,835],{"class":358},"; i ",[334,837,838],{"class":347},"\u003C",[334,840,841],{"class":358}," n; i",[334,843,557],{"class":347},[334,845,397],{"class":358},[334,847,848],{"class":347},"await",[334,850,851],{"class":378}," callFn",[334,853,854],{"class":358},"(); ",[334,856,857],{"class":340},"\u002F\u002F a no-op worker method\n",[334,859,860,862,865,867,870,873,876,879,882],{"class":336,"line":385},[334,861,564],{"class":347},[334,863,864],{"class":358}," (performance.",[334,866,813],{"class":378},[334,868,869],{"class":358},"() ",[334,871,872],{"class":347},"-",[334,874,875],{"class":358}," start) ",[334,877,878],{"class":347},"\u002F",[334,880,881],{"class":358}," n;     ",[334,883,884],{"class":340},"\u002F\u002F mean ms per round-trip\n",[334,886,887],{"class":336,"line":415},[334,888,889],{"class":340},"  \u002F\u002F trade-off: this measures dispatch with an empty payload, so it ISOLATES the proxy\n",[334,891,892],{"class":336,"line":445},[334,893,894],{"class":340},"  \u002F\u002F overhead but ignores clone cost — re-run with a realistic payload before deciding,\n",[334,896,897],{"class":336,"line":451},[334,898,899],{"class":340},"  \u002F\u002F because for real work the clone usually dwarfs the proxy hop on both abstractions.\n",[334,901,902],{"class":336,"line":457},[334,903,619],{"class":358},[15,905,906,907,909],{},"In almost all product workloads the per-call mean is a fraction of a millisecond on both, and the ",[30,908,744],{}," of actual compute makes the difference irrelevant. The dispatch cost only becomes the deciding factor at thousands of calls per second — at which point you should also ask whether batching many calls into one would beat both abstractions.",[154,911,913],{"id":912},"debugging-and-transferables","Debugging and Transferables",[15,915,916,917,919,920,922,923,925,926,929,930,933],{},"Raw ",[30,918,50],{}," is more transparent to debug: every message is a plain object you can log at both ends, and a stack trace points directly at your ",[30,921,628],{}," handler. Comlink adds a Proxy layer, so a thrown worker error surfaces as a rejected promise with a stack that runs through Comlink's internals — workable, but you must ship source maps for the worker chunk to read it. On transferables the two are equivalent in capability: raw code passes the transfer list as the second ",[30,924,50],{}," argument; Comlink wraps the value in ",[30,927,928],{},"Comlink.transfer(value, [buffer])",". Neither can transfer a plain string or object — those are always cloned — so for binary-heavy work both require you to think in ",[30,931,932],{},"ArrayBuffer","s regardless of abstraction.",[15,935,936,937,941,942,944,945,947],{},"There is one capability gap worth knowing: Comlink can proxy a ",[938,939,940],"em",{},"callback"," into the worker with ",[30,943,269],{},", so the worker can invoke a main-thread function for progress events without you writing any back-channel protocol. Reproducing that with raw ",[30,946,50],{}," means inventing a second message type for progress, correlating it to the originating request, and routing it back to the right caller — exactly the kind of bookkeeping Comlink exists to remove. If your worker needs to stream progress, Comlink's ergonomic lead widens considerably; if it is strictly request\u002Fresponse, the two converge.",[15,949,950,951,954,955,957,958,960,961,964],{},"Type safety is the other axis where the abstractions diverge in a TypeScript codebase. ",[30,952,953],{},"Comlink.wrap\u003CT>()"," returns a ",[30,956,299],{}," that types every proxied method, its arguments, and its return value as a promise, so a signature change in the worker surfaces as a compile error at the call site. Raw ",[30,959,50],{}," payloads are ",[30,962,963],{},"any"," unless you hand-write discriminated-union message types and runtime guards, and a drifted message shape fails silently at runtime instead of at build time. For a long-lived worker API that multiple teams call, that static guarantee is often the deciding factor on its own.",[154,966,968],{"id":967},"when-to-pick-which","When to Pick Which",[15,970,971,974,975,406],{},[184,972,973],{},"Pick Comlink when"," the worker exposes several methods, you call it from application code that benefits from reading like normal async functions, you use TypeScript and want the proxy typed, or you need to proxy callbacks for progress. This covers the large majority of product work — a JSON-parsing worker, an image-processing service, a search-index builder. The 1.1KB is trivial against the maintainability it buys, and the bundling story is identical to the manual approach since you wire the worker the same way described in ",[19,976,978],{"href":977},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fdynamic-imports-and-route-based-splitting\u002F","dynamic imports and route-based splitting",[15,980,981,984,985,988],{},[184,982,983],{},"Pick raw postMessage when"," you have exactly one fire-and-forget function and refuse a dependency, when you are in a hot path doing thousands of tiny messages per second where the proxy overhead compounds, or when you need a custom wire protocol (binary framing, a ",[30,986,987],{},"SharedArrayBuffer"," ring buffer) that does not map to method-call semantics at all. It is also the right call when total transparency for debugging outweighs ergonomics — low-level infrastructure where you want to see every byte on the wire.",[15,990,991,992,995,996,998,999,1003,1004,1008],{},"A practical hybrid: start with Comlink for the developer experience, and if profiling later shows the message layer itself is hot — which is rare, because the ",[938,993,994],{},"work"," dominates, not the dispatch — drop to raw ",[30,997,50],{}," for just that one hot path. Decide based on a real trace, not a guess; the ",[19,1000,1002],{"href":1001},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002F","profiling event handlers for INP"," workflow shows how to confirm whether the dispatch or the compute is actually the cost. For borderline cases where the work might not need a worker at all, re-check whether ",[19,1005,1007],{"href":1006},"\u002Fcore-web-vitals-measurement\u002Foptimizing-inp-with-scheduler-yield\u002F","scheduler.yield() on the main thread"," already solves it before adopting either abstraction.",[758,1010,1012],{"id":1011},"migrating-from-raw-postmessage-to-comlink","Migrating from raw postMessage to Comlink",[15,1014,1015,1016,1018,1019,1021,1022,1025,1026,1029,1030,1032,1033,406],{},"If you started with raw ",[30,1017,50],{}," and the protocol has grown unwieldy, the migration is mechanical and low-risk because the worker still runs identical compute — only the wire wrapper changes. Replace the worker's ",[30,1020,628],{}," switch with a ",[30,1023,1024],{},"Comlink.expose({ ... })"," object of the same functions, and replace the main thread's hand-rolled ",[30,1027,1028],{},"call()"," helper and ",[30,1031,722],{}," map with ",[30,1034,1035],{},"Comlink.wrap(worker)",[325,1037,1039],{"className":327,"code":1038,"language":329,"meta":330,"style":330},"\u002F\u002F Before (worker): const handlers = { parse, hash }; onmessage = e => { ... dispatch ... }\n\u002F\u002F After (worker):\nimport * as Comlink from 'comlink';\nComlink.expose({ parse, hash }); \u002F\u002F the same two functions, no protocol code\n\u002F\u002F trade-off: migrating mid-project means both styles coexist briefly; do it per-worker\n\u002F\u002F in one commit rather than half-converting a single worker, or message routing breaks.\n",[30,1040,1041,1046,1051,1067,1079,1084],{"__ignoreMap":330},[334,1042,1043],{"class":336,"line":337},[334,1044,1045],{"class":340},"\u002F\u002F Before (worker): const handlers = { parse, hash }; onmessage = e => { ... dispatch ... }\n",[334,1047,1048],{"class":336,"line":344},[334,1049,1050],{"class":340},"\u002F\u002F After (worker):\n",[334,1052,1053,1055,1057,1059,1061,1063,1065],{"class":336,"line":372},[334,1054,348],{"class":347},[334,1056,352],{"class":351},[334,1058,355],{"class":347},[334,1060,359],{"class":358},[334,1062,362],{"class":347},[334,1064,366],{"class":365},[334,1066,369],{"class":358},[334,1068,1069,1071,1073,1076],{"class":336,"line":385},[334,1070,375],{"class":358},[334,1072,379],{"class":378},[334,1074,1075],{"class":358},"({ parse, hash }); ",[334,1077,1078],{"class":340},"\u002F\u002F the same two functions, no protocol code\n",[334,1080,1081],{"class":336,"line":415},[334,1082,1083],{"class":340},"\u002F\u002F trade-off: migrating mid-project means both styles coexist briefly; do it per-worker\n",[334,1085,1086],{"class":336,"line":445},[334,1087,1088],{"class":340},"\u002F\u002F in one commit rather than half-converting a single worker, or message routing breaks.\n",[15,1090,1091],{},"The reverse migration — Comlink to raw — is only worth doing for a measured hot path, and you typically extract just that one method to a dedicated raw channel while leaving the rest on Comlink. Wholesale removal of Comlink to \"save 1.1KB\" is rarely justified against the protocol code you take back on.",[154,1093,1095],{"id":1094},"related","Related",[1097,1098,1099,1106,1113,1119],"ul",{},[1100,1101,1102,1105],"li",{},[19,1103,1104],{"href":21},"Offloading work to web workers with Comlink"," — the full setup, transferables, lifecycle, and pooling behind both options here.",[1100,1107,1108,1112],{},[19,1109,1111],{"href":1110},"\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"," — a concrete worker the matrix above applies to directly.",[1100,1114,1115,1118],{},[19,1116,1117],{"href":1006},"Optimizing INP with scheduler.yield()"," — the on-thread alternative to check before reaching for any worker.",[1100,1120,1121,1124],{},[19,1122,1123],{"href":1001},"Profiling event handlers for INP"," — proving whether dispatch or compute is the real cost before optimizing the message layer.",[1126,1127,1129],"script",{"type":1128},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"Choosing Between Comlink and Raw postMessage for Web Workers\",\n  \"description\": \"Compare Comlink RPC and raw postMessage across ergonomics, bundle size, overhead, debugging, and transferables to choose a worker communication layer.\",\n  \"step\": [\n    { \"@type\": \"HowToStep\", \"name\": \"Weigh ergonomics against bundle cost\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F#ergonomics-where-comlink-earns-its-place\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Assess per-call overhead for your call frequency\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F#overhead-when-the-proxy-hop-matters\" },\n    { \"@type\": \"HowToStep\", \"name\": \"Apply the when-to-pick-which decision\", \"url\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F#when-to-pick-which\" }\n  ]\n}\n",[1126,1131,1132],{"type":1128},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"Comlink vs Raw postMessage for Web Workers: Which Abstraction to Pick\",\n  \"description\": \"A decision matrix comparing Comlink RPC and raw postMessage for Web Worker communication, with when-to-pick-which guidance.\",\n  \"datePublished\": \"2026-06-18\",\n  \"dateModified\": \"2026-06-18\",\n  \"mainEntityOfPage\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F\"\n}\n",[1126,1134,1135],{"type":1128},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"BreadcrumbList\",\n  \"itemListElement\": [\n    { \"@type\": \"ListItem\", \"position\": 1, \"name\": \"Home\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 2, \"name\": \"Core Web Vitals & Measurement\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 3, \"name\": \"Offloading Work to Web Workers with Comlink\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002F\" },\n    { \"@type\": \"ListItem\", \"position\": 4, \"name\": \"Comlink vs Raw postMessage for Workers\", \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F\" }\n  ]\n}\n",[1137,1138,1139],"style",{},"html pre.shiki code .sIIH1, html code.shiki .sIIH1{--shiki-default:#66707B;--shiki-dark:#66707B;--shiki-light:#66707B}html pre.shiki code .sP5qI, html code.shiki .sP5qI{--shiki-default:#A0111F;--shiki-dark:#A0111F;--shiki-light:#A0111F}html pre.shiki code .sf6mN, html code.shiki .sf6mN{--shiki-default:#023B95;--shiki-dark:#023B95;--shiki-light:#023B95}html pre.shiki code .syybb, html code.shiki .syybb{--shiki-default:#0E1116;--shiki-dark:#0E1116;--shiki-light:#0E1116}html pre.shiki code .s-_DF, html code.shiki .s-_DF{--shiki-default:#032563;--shiki-dark:#032563;--shiki-light:#032563}html pre.shiki code .ssM3C, html code.shiki .ssM3C{--shiki-default:#622CBC;--shiki-dark:#622CBC;--shiki-light:#622CBC}html pre.shiki code .seIZK, html code.shiki .seIZK{--shiki-default:#702C00;--shiki-dark:#702C00;--shiki-light:#702C00}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);}",{"title":330,"searchDepth":344,"depth":344,"links":1141},[1142,1143,1144,1147,1148,1151],{"id":156,"depth":344,"text":157},{"id":319,"depth":344,"text":320},{"id":730,"depth":344,"text":731,"children":1145},[1146],{"id":760,"depth":372,"text":761},{"id":912,"depth":344,"text":913},{"id":967,"depth":344,"text":968,"children":1149},[1150],{"id":1011,"depth":372,"text":1012},{"id":1094,"depth":344,"text":1095},"Decision matrix comparing Comlink RPC and raw postMessage for Web Workers, with when-to-pick-which guidance.","md",{"slug":1155,"type":1156,"breadcrumb":1157,"datePublished":1165,"dateModified":1165},"comlink-vs-raw-postmessage-for-workers","long_tail",[1158,1160,1161,1163],{"name":1159,"url":878},"Home",{"name":27,"url":26},{"name":1162,"url":21},"Offloading Work to Web Workers with Comlink",{"name":5,"url":1164},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F","2026-06-18",true,"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers",{"title":1169,"description":1170},"Comlink vs Raw postMessage for Web Workers","A decision matrix for Comlink vs raw postMessage: ergonomics, bundle size, per-call overhead, debugging, and transferables — with clear guidance on when to pick which.","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002Findex","j_urHTiOfFJPTKEUypzVrCaMuXScHuRpIRX-_wU_5vY",[1174,1177],{"title":1162,"path":1175,"stem":1176,"children":-1},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Findex",{"title":1178,"path":1179,"stem":1180,"children":-1},"Moving Heavy JSON Parsing Off the Main Thread","\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fmoving-heavy-json-parsing-off-the-main-thread","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fmoving-heavy-json-parsing-off-the-main-thread\u002Findex",1782237171390]