[{"data":1,"prerenderedAt":1183},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker":1174},{"id":4,"title":5,"body":6,"description":1155,"extension":1156,"meta":1157,"navigation":524,"path":1169,"seo":1170,"stem":1172,"__hash__":1173},"content\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker\u002Findex.md","Sharing One Worker Across Tabs with SharedWorker",{"type":7,"value":8,"toc":1140},"minimark",[9,14,29,41,154,159,194,198,204,210,216,230,360,364,369,620,623,627,793,796,800,937,940,944,947,950,1012,1016,1019,1023,1037,1041,1051,1055,1067,1080,1089,1098,1102,1125,1130,1133,1136],[10,11,13],"h1",{"id":12},"how-to-share-one-worker-across-tabs-with-sharedworker","How to Share One Worker Across Tabs with SharedWorker",[15,16,17,18,23,24,28],"p",{},"This guide extends ",[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"," to multi-tab applications. Dashboards, email clients, trading tools and admin panels are often open in several tabs at once. If each tab starts its own worker to maintain a WebSocket, build a search index or cache API data, the device does that work once per tab — multiplying CPU, memory and network use.",[15,30,31,32,36,37,40],{},"A ",[33,34,35],"code",{},"SharedWorker"," is created once per origin and script URL; every tab that constructs it connects to the same instance through a ",[33,38,39],{},"MessagePort",". Expensive state — a large index, a live connection, a decoded dataset — exists once. New tabs connect to an already-warm worker, which can turn a 600ms startup into an instant one for every tab after the first.",[15,42,43],{},[44,45,51,52,51,59,51,63,51,66,51,75,51,81,51,90,51,96,51,101,51,106,51,109,51,112,51,115,51,118,51,121,51,124,51,129,51,133,51,135,51,139,51,141,51,144,51,146,51,149,51,151,51],"svg",{"viewBox":46,"width":47,"role":48,"ariaLabel":49,"style":50},"0 0 760 228","100%","img","Comparison of three tabs each running a dedicated worker with three tabs connected to a single shared worker.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[53,54],"rect",{"className":55,"x":57,"y":57,"width":47,"height":47,"fill":58},[56],"svg-canvas","0","#ffffff",[60,61,62],"title",{},"Dedicated workers vs one shared worker",[64,65,49],"desc",{},[53,67],{"x":68,"y":68,"width":69,"height":70,"rx":71,"fill":72,"stroke":73,"style":74},"1","758","226","10","none","currentColor","stroke-opacity:0.18",[76,77,62],"text",{"x":78,"y":79,"fill":73,"style":80},"28.0","34.0","font-size:16px;font-weight:700",[53,82],{"x":78,"y":83,"width":84,"height":85,"rx":86,"fill":87,"stroke":88,"style":89},"56.0","340.0","150.0","6","#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[76,91,95],{"x":92,"y":93,"fill":73,"style":94},"42.0","82.0","font-size:14px;font-weight:700","Dedicated worker per tab",[76,97,100],{"x":92,"y":98,"fill":73,"style":99},"108.0","font-size:12.5px;font-weight:700","•",[76,102,105],{"x":83,"y":98,"fill":73,"style":103,"textAnchor":104},"font-size:12.5px","start","3 tabs = 3 workers, 3 indexes",[76,107,100],{"x":92,"y":108,"fill":73,"style":99},"132.0",[76,110,111],{"x":83,"y":108,"fill":73,"style":103,"textAnchor":104},"3 WebSocket connections",[76,113,100],{"x":92,"y":114,"fill":73,"style":99},"156.0",[76,116,117],{"x":83,"y":114,"fill":73,"style":103,"textAnchor":104},"Each new tab pays full startup",[76,119,100],{"x":92,"y":120,"fill":73,"style":99},"180.0",[76,122,123],{"x":83,"y":120,"fill":73,"style":103,"textAnchor":104},"Memory scales with tab count",[53,125],{"x":126,"y":83,"width":84,"height":85,"rx":86,"fill":127,"stroke":127,"style":128},"392.0","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[76,130,132],{"x":131,"y":93,"fill":73,"style":94},"406.0","One SharedWorker",[76,134,100],{"x":131,"y":98,"fill":73,"style":99},[76,136,138],{"x":137,"y":98,"fill":73,"style":103,"textAnchor":104},"420.0","3 tabs share 1 worker",[76,140,100],{"x":131,"y":108,"fill":73,"style":99},[76,142,143],{"x":137,"y":108,"fill":73,"style":103,"textAnchor":104},"1 connection fanned out to all tabs",[76,145,100],{"x":131,"y":114,"fill":73,"style":99},[76,147,148],{"x":137,"y":114,"fill":73,"style":103,"textAnchor":104},"Later tabs connect to a warm worker",[76,150,100],{"x":131,"y":120,"fill":73,"style":99},[76,152,153],{"x":137,"y":120,"fill":73,"style":103,"textAnchor":104},"Memory roughly constant",[155,156,158],"h2",{"id":157},"rapid-diagnosis","Rapid Diagnosis",[160,161,162,170,176,182,188],"ul",{},[163,164,165,169],"li",{},[166,167,168],"strong",{},"Count tabs per user."," If your analytics shows many sessions with multiple concurrent tabs (common in B2B tools), the duplication is real.",[163,171,172,175],{},[166,173,174],{},"Measure per-tab startup work."," In a trace of opening a second tab, look for the same worker startup, fetch and indexing as in the first.",[163,177,178,181],{},[166,179,180],{},"Check memory."," In Chrome's Task Manager, compare memory with one tab and with three tabs open. Linear growth in worker memory points to duplicated state.",[163,183,184,187],{},[166,185,186],{},"Check connection counts."," Multiple identical WebSocket or SSE connections from one browser are a strong signal.",[163,189,190,193],{},[166,191,192],{},"Check browser support for your audience."," SharedWorker is supported in Chromium desktop, Firefox and Safari 16+, but not in Chrome for Android. Mobile Chromium users need a fallback.",[155,195,197],{"id":196},"root-cause-analysis","Root Cause Analysis",[15,199,200,203],{},[166,201,202],{},"1. Per-tab ownership of shared state."," Each tab independently fetches and processes the same data because nothing coordinates them.",[15,205,206,209],{},[166,207,208],{},"2. Per-tab real-time connections."," Each tab opens its own socket, multiplying server load and client CPU for message parsing.",[15,211,212,215],{},[166,213,214],{},"3. Cold start on every tab."," Every new tab pays worker script download (usually cached), evaluation and state construction.",[15,217,218,221,222,225,226,229],{},[166,219,220],{},"4. Cross-tab sync through storage events."," Tabs coordinate by writing to ",[33,223,224],{},"localStorage"," and listening for ",[33,227,228],{},"storage"," events, which is synchronous on the main thread and slow for large values.",[15,231,232],{},[44,233,51,236,51,239,51,242,51,244,51,261,51,264,51,266,51,270,51,277,51,280,51,283,51,286,51,290,51,293,51,297,51,306,51,309,51,312,51,315,51,321,51,327,51,331,51,335,51,338,51,343,51,348,51,352,51,357,51],{"viewBox":234,"width":47,"role":48,"ariaLabel":235,"style":50},"0 0 760 330","Sequence showing the first tab starting the shared worker and building state, and a second tab connecting to the already-warm worker.",[53,237],{"className":238,"x":57,"y":57,"width":47,"height":47,"fill":58},[56],[60,240,241],{},"Two tabs connecting to one SharedWorker",[64,243,235],{},[245,246,247],"defs",{},[248,249,256],"marker",{"id":250,"viewBox":251,"refX":252,"refY":253,"markerWidth":254,"markerHeight":254,"orient":255},"faa0d85885","0 0 10 10","9","5","7","auto-start-reverse",[257,258],"path",{"d":259,"fill":73,"style":260},"M0 0 L10 5 L0 10 z","fill-opacity:0.7",[53,262],{"x":68,"y":68,"width":69,"height":263,"rx":71,"fill":72,"stroke":73,"style":74},"328",[76,265,241],{"x":78,"y":79,"fill":73,"style":80},[53,267],{"x":268,"y":83,"width":85,"height":79,"rx":86,"fill":73,"stroke":73,"style":269},"41.0","fill-opacity:0.06;stroke-opacity:0.4",[76,271,276],{"x":272,"y":273,"fill":73,"style":274,"textAnchor":275},"116.0","78.0","font-size:13px;font-weight:700","middle","Tab A",[53,278],{"x":279,"y":83,"width":85,"height":79,"rx":86,"fill":73,"stroke":73,"style":269},"217.0",[76,281,35],{"x":282,"y":273,"fill":73,"style":274,"textAnchor":275},"292.0",[53,284],{"x":285,"y":83,"width":85,"height":79,"rx":86,"fill":73,"stroke":73,"style":269},"393.0",[76,287,289],{"x":288,"y":273,"fill":73,"style":274,"textAnchor":275},"468.0","Tab B",[53,291],{"x":292,"y":83,"width":85,"height":79,"rx":86,"fill":73,"stroke":73,"style":269},"569.0",[76,294,296],{"x":295,"y":273,"fill":73,"style":274,"textAnchor":275},"644.0","Server",[298,299],"line",{"x1":272,"y1":300,"x2":272,"y2":301,"stroke":73,"strokeWidth":302,"strokeDashArray":303,"style":305},"90.0","308.0","1.5",[304,304],"4","stroke-opacity:0.35",[298,307],{"x1":282,"y1":300,"x2":282,"y2":301,"stroke":73,"strokeWidth":302,"strokeDashArray":308,"style":305},[304,304],[298,310],{"x1":288,"y1":300,"x2":288,"y2":301,"stroke":73,"strokeWidth":302,"strokeDashArray":311,"style":305},[304,304],[298,313],{"x1":295,"y1":300,"x2":295,"y2":301,"stroke":73,"strokeWidth":302,"strokeDashArray":314,"style":305},[304,304],[298,316],{"x1":272,"y1":317,"x2":318,"y2":317,"stroke":73,"strokeWidth":302,"style":319,"markerEnd":320},"124.0","290.0","stroke-opacity:0.6","url(#faa0d85885)",[76,322,326],{"x":323,"y":324,"fill":73,"style":325,"textAnchor":275},"204.0","117.0","font-size:12px","connect (cold start)",[298,328],{"x1":282,"y1":329,"x2":330,"y2":329,"stroke":73,"strokeWidth":302,"style":319,"markerEnd":320},"164.0","642.0",[76,332,334],{"x":288,"y":333,"fill":73,"style":325,"textAnchor":275},"157.0","one WebSocket",[298,336],{"x1":288,"y1":323,"x2":337,"y2":323,"stroke":73,"strokeWidth":302,"style":319,"markerEnd":320},"294.0",[76,339,342],{"x":340,"y":341,"fill":73,"style":325,"textAnchor":275},"380.0","197.0","connect (warm)",[298,344],{"x1":282,"y1":345,"x2":346,"y2":345,"stroke":73,"strokeWidth":302,"strokeDashArray":347,"style":319,"markerEnd":320},"244.0","118.0",[253,304],[76,349,351],{"x":323,"y":350,"fill":73,"style":325,"textAnchor":275},"237.0","update",[298,353],{"x1":282,"y1":354,"x2":355,"y2":354,"stroke":73,"strokeWidth":302,"strokeDashArray":356,"style":319,"markerEnd":320},"284.0","466.0",[253,304],[76,358,351],{"x":340,"y":359,"fill":73,"style":325,"textAnchor":275},"277.0",[155,361,363],{"id":362},"step-by-step-resolution","Step-by-Step Resolution",[365,366,368],"h3",{"id":367},"_1-create-the-shared-worker-with-comlink","1. Create the shared worker with Comlink",[370,371,376],"pre",{"className":372,"code":373,"language":374,"meta":375,"style":375},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F main.js\nimport * as Comlink from 'comlink';\nconst shared = new SharedWorker(new URL('.\u002Fdata-worker.js', import.meta.url), { type: 'module', name: 'data' });\nconst api = Comlink.wrap(shared.port);\nconst results = await api.search('invoice 2041');\n\n\u002F\u002F data-worker.js\nimport * as Comlink from 'comlink';\nconst state = createState();                          \u002F\u002F built once for all tabs\nonconnect = (e) => Comlink.expose(state.api, e.ports[0]);\n\u002F\u002F trade-off: every tab shares one thread. A long task in the shared worker\n\u002F\u002F delays responses to ALL tabs; keep its work chunked just as you would on\n\u002F\u002F the main thread, especially if tabs issue concurrent requests.\n","javascript","",[33,377,378,386,414,473,492,519,526,532,549,568,602,608,614],{"__ignoreMap":375},[379,380,382],"span",{"class":298,"line":381},1,[379,383,385],{"class":384},"sjfSM","\u002F\u002F main.js\n",[379,387,389,393,397,400,404,407,411],{"class":298,"line":388},2,[379,390,392],{"class":391},"sPARh","import",[379,394,396],{"class":395},"sPXB4"," *",[379,398,399],{"class":391}," as",[379,401,403],{"class":402},"saISM"," Comlink ",[379,405,406],{"class":391},"from",[379,408,410],{"class":409},"sZ8jY"," 'comlink'",[379,412,413],{"class":402},";\n",[379,415,417,420,423,426,429,433,436,439,442,444,447,450,452,455,458,461,464,467,470],{"class":298,"line":416},3,[379,418,419],{"class":391},"const",[379,421,422],{"class":395}," shared",[379,424,425],{"class":391}," =",[379,427,428],{"class":391}," new",[379,430,432],{"class":431},"smZ65"," SharedWorker",[379,434,435],{"class":402},"(",[379,437,438],{"class":391},"new",[379,440,441],{"class":431}," URL",[379,443,435],{"class":402},[379,445,446],{"class":409},"'.\u002Fdata-worker.js'",[379,448,449],{"class":402},", ",[379,451,392],{"class":391},[379,453,454],{"class":402},".",[379,456,457],{"class":395},"meta",[379,459,460],{"class":402},".url), { type: ",[379,462,463],{"class":409},"'module'",[379,465,466],{"class":402},", name: ",[379,468,469],{"class":409},"'data'",[379,471,472],{"class":402}," });\n",[379,474,476,478,481,483,486,489],{"class":298,"line":475},4,[379,477,419],{"class":391},[379,479,480],{"class":395}," api",[379,482,425],{"class":391},[379,484,485],{"class":402}," Comlink.",[379,487,488],{"class":431},"wrap",[379,490,491],{"class":402},"(shared.port);\n",[379,493,495,497,500,502,505,508,511,513,516],{"class":298,"line":494},5,[379,496,419],{"class":391},[379,498,499],{"class":395}," results",[379,501,425],{"class":391},[379,503,504],{"class":391}," await",[379,506,507],{"class":402}," api.",[379,509,510],{"class":431},"search",[379,512,435],{"class":402},[379,514,515],{"class":409},"'invoice 2041'",[379,517,518],{"class":402},");\n",[379,520,522],{"class":298,"line":521},6,[379,523,525],{"emptyLinePlaceholder":524},true,"\n",[379,527,529],{"class":298,"line":528},7,[379,530,531],{"class":384},"\u002F\u002F data-worker.js\n",[379,533,535,537,539,541,543,545,547],{"class":298,"line":534},8,[379,536,392],{"class":391},[379,538,396],{"class":395},[379,540,399],{"class":391},[379,542,403],{"class":402},[379,544,406],{"class":391},[379,546,410],{"class":409},[379,548,413],{"class":402},[379,550,552,554,557,559,562,565],{"class":298,"line":551},9,[379,553,419],{"class":391},[379,555,556],{"class":395}," state",[379,558,425],{"class":391},[379,560,561],{"class":431}," createState",[379,563,564],{"class":402},"();                          ",[379,566,567],{"class":384},"\u002F\u002F built once for all tabs\n",[379,569,571,574,576,579,583,586,589,591,594,597,599],{"class":298,"line":570},10,[379,572,573],{"class":431},"onconnect",[379,575,425],{"class":391},[379,577,578],{"class":402}," (",[379,580,582],{"class":581},"sQw3B","e",[379,584,585],{"class":402},") ",[379,587,588],{"class":391},"=>",[379,590,485],{"class":402},[379,592,593],{"class":431},"expose",[379,595,596],{"class":402},"(state.api, e.ports[",[379,598,57],{"class":395},[379,600,601],{"class":402},"]);\n",[379,603,605],{"class":298,"line":604},11,[379,606,607],{"class":384},"\u002F\u002F trade-off: every tab shares one thread. A long task in the shared worker\n",[379,609,611],{"class":298,"line":610},12,[379,612,613],{"class":384},"\u002F\u002F delays responses to ALL tabs; keep its work chunked just as you would on\n",[379,615,617],{"class":298,"line":616},13,[379,618,619],{"class":384},"\u002F\u002F the main thread, especially if tabs issue concurrent requests.\n",[15,621,622],{},"Expected outcome: the second and later tabs get results without rebuilding state.",[365,624,626],{"id":625},"_2-fan-out-real-time-data-from-a-single-connection","2. Fan out real-time data from a single connection",[370,628,630],{"className":372,"code":629,"language":374,"meta":375,"style":375},"\u002F\u002F data-worker.js\nconst ports = new Set();\nconst ws = new WebSocket('wss:\u002F\u002Fapi.example.com\u002Fstream');\nws.onmessage = (m) => { const update = JSON.parse(m.data); for (const p of ports) p.postMessage(update); };\nonconnect = (e) => { const port = e.ports[0]; ports.add(port); port.start(); };\n\u002F\u002F trade-off: ports of closed tabs are not reliably removed — there is no\n\u002F\u002F 'close' event on a MessagePort. Have tabs send a 'bye' on pagehide and prune\n\u002F\u002F ports that fail to respond to a periodic ping.\n",[33,631,632,636,653,674,736,778,783,788],{"__ignoreMap":375},[379,633,634],{"class":298,"line":381},[379,635,531],{"class":384},[379,637,638,640,643,645,647,650],{"class":298,"line":388},[379,639,419],{"class":391},[379,641,642],{"class":395}," ports",[379,644,425],{"class":391},[379,646,428],{"class":391},[379,648,649],{"class":431}," Set",[379,651,652],{"class":402},"();\n",[379,654,655,657,660,662,664,667,669,672],{"class":298,"line":416},[379,656,419],{"class":391},[379,658,659],{"class":395}," ws",[379,661,425],{"class":391},[379,663,428],{"class":391},[379,665,666],{"class":431}," WebSocket",[379,668,435],{"class":402},[379,670,671],{"class":409},"'wss:\u002F\u002Fapi.example.com\u002Fstream'",[379,673,518],{"class":402},[379,675,676,679,682,684,686,689,691,693,696,698,701,703,706,708,711,714,717,719,721,724,727,730,733],{"class":298,"line":475},[379,677,678],{"class":402},"ws.",[379,680,681],{"class":431},"onmessage",[379,683,425],{"class":391},[379,685,578],{"class":402},[379,687,688],{"class":581},"m",[379,690,585],{"class":402},[379,692,588],{"class":391},[379,694,695],{"class":402}," { ",[379,697,419],{"class":391},[379,699,700],{"class":395}," update",[379,702,425],{"class":391},[379,704,705],{"class":395}," JSON",[379,707,454],{"class":402},[379,709,710],{"class":431},"parse",[379,712,713],{"class":402},"(m.data); ",[379,715,716],{"class":391},"for",[379,718,578],{"class":402},[379,720,419],{"class":391},[379,722,723],{"class":395}," p",[379,725,726],{"class":391}," of",[379,728,729],{"class":402}," ports) p.",[379,731,732],{"class":431},"postMessage",[379,734,735],{"class":402},"(update); };\n",[379,737,738,740,742,744,746,748,750,752,754,757,759,762,764,767,770,773,775],{"class":298,"line":494},[379,739,573],{"class":431},[379,741,425],{"class":391},[379,743,578],{"class":402},[379,745,582],{"class":581},[379,747,585],{"class":402},[379,749,588],{"class":391},[379,751,695],{"class":402},[379,753,419],{"class":391},[379,755,756],{"class":395}," port",[379,758,425],{"class":391},[379,760,761],{"class":402}," e.ports[",[379,763,57],{"class":395},[379,765,766],{"class":402},"]; ports.",[379,768,769],{"class":431},"add",[379,771,772],{"class":402},"(port); port.",[379,774,104],{"class":431},[379,776,777],{"class":402},"(); };\n",[379,779,780],{"class":298,"line":521},[379,781,782],{"class":384},"\u002F\u002F trade-off: ports of closed tabs are not reliably removed — there is no\n",[379,784,785],{"class":298,"line":528},[379,786,787],{"class":384},"\u002F\u002F 'close' event on a MessagePort. Have tabs send a 'bye' on pagehide and prune\n",[379,789,790],{"class":298,"line":534},[379,791,792],{"class":384},"\u002F\u002F ports that fail to respond to a periodic ping.\n",[15,794,795],{},"Expected outcome: one socket per browser regardless of tab count; JSON parsing of stream messages happens once, off every main thread.",[365,797,799],{"id":798},"_3-provide-a-fallback-where-sharedworker-is-missing","3. Provide a fallback where SharedWorker is missing",[370,801,803],{"className":372,"code":802,"language":374,"meta":375,"style":375},"function createDataWorker() {\n  if ('SharedWorker' in globalThis) {\n    const sw = new SharedWorker(new URL('.\u002Fdata-worker.js', import.meta.url), { type: 'module' });\n    return sw.port;\n  }\n  return new Worker(new URL('.\u002Fdata-worker.js', import.meta.url), { type: 'module' });\n}\n\u002F\u002F The worker script handles both: in a dedicated worker, treat `self` as the single port.\n\u002F\u002F trade-off: the fallback loses deduplication on Chrome for Android. That is\n\u002F\u002F acceptable — mobile users rarely keep many tabs of a dashboard open.\n",[33,804,805,816,832,870,878,883,917,922,927,932],{"__ignoreMap":375},[379,806,807,810,813],{"class":298,"line":381},[379,808,809],{"class":391},"function",[379,811,812],{"class":431}," createDataWorker",[379,814,815],{"class":402},"() {\n",[379,817,818,821,823,826,829],{"class":298,"line":388},[379,819,820],{"class":391},"  if",[379,822,578],{"class":402},[379,824,825],{"class":409},"'SharedWorker'",[379,827,828],{"class":391}," in",[379,830,831],{"class":402}," globalThis) {\n",[379,833,834,837,840,842,844,846,848,850,852,854,856,858,860,862,864,866,868],{"class":298,"line":416},[379,835,836],{"class":391},"    const",[379,838,839],{"class":395}," sw",[379,841,425],{"class":391},[379,843,428],{"class":391},[379,845,432],{"class":431},[379,847,435],{"class":402},[379,849,438],{"class":391},[379,851,441],{"class":431},[379,853,435],{"class":402},[379,855,446],{"class":409},[379,857,449],{"class":402},[379,859,392],{"class":391},[379,861,454],{"class":402},[379,863,457],{"class":395},[379,865,460],{"class":402},[379,867,463],{"class":409},[379,869,472],{"class":402},[379,871,872,875],{"class":298,"line":475},[379,873,874],{"class":391},"    return",[379,876,877],{"class":402}," sw.port;\n",[379,879,880],{"class":298,"line":494},[379,881,882],{"class":402},"  }\n",[379,884,885,888,890,893,895,897,899,901,903,905,907,909,911,913,915],{"class":298,"line":521},[379,886,887],{"class":391},"  return",[379,889,428],{"class":391},[379,891,892],{"class":431}," Worker",[379,894,435],{"class":402},[379,896,438],{"class":391},[379,898,441],{"class":431},[379,900,435],{"class":402},[379,902,446],{"class":409},[379,904,449],{"class":402},[379,906,392],{"class":391},[379,908,454],{"class":402},[379,910,457],{"class":395},[379,912,460],{"class":402},[379,914,463],{"class":409},[379,916,472],{"class":402},[379,918,919],{"class":298,"line":528},[379,920,921],{"class":402},"}\n",[379,923,924],{"class":298,"line":534},[379,925,926],{"class":384},"\u002F\u002F The worker script handles both: in a dedicated worker, treat `self` as the single port.\n",[379,928,929],{"class":298,"line":551},[379,930,931],{"class":384},"\u002F\u002F trade-off: the fallback loses deduplication on Chrome for Android. That is\n",[379,933,934],{"class":298,"line":570},[379,935,936],{"class":384},"\u002F\u002F acceptable — mobile users rarely keep many tabs of a dashboard open.\n",[15,938,939],{},"Expected outcome: one code path that degrades to a per-tab worker where needed.",[365,941,943],{"id":942},"_4-keep-state-warm-across-tab-closures","4. Keep state warm across tab closures",[15,945,946],{},"A SharedWorker is terminated when its last connected tab closes. If users close and reopen the app frequently, consider persisting expensive derived state (a serialised index) to IndexedDB from the worker so a cold start restores rather than rebuilds.",[15,948,949],{},"Expected outcome: even cold starts become fast after the first visit.",[15,951,952],{},[44,953,51,956,51,959,51,962,51,964,51,967,51,969,51,976,51,983,51,988,51,992,51,997,51,1001,51,1004,51,1008,51],{"viewBox":954,"width":47,"role":48,"ariaLabel":955,"style":50},"0 0 760 163","Bar chart comparing memory used by data workers with three tabs open, using dedicated workers versus one shared worker.",[53,957],{"className":958,"x":57,"y":57,"width":47,"height":47,"fill":58},[56],[60,960,961],{},"Resource use with three open tabs",[64,963,955],{},[53,965],{"x":68,"y":68,"width":69,"height":966,"rx":71,"fill":72,"stroke":73,"style":74},"161",[76,968,961],{"x":78,"y":79,"fill":73,"style":80},[76,970,975],{"x":971,"y":972,"fill":73,"style":973,"textAnchor":974},"183.6","70.0","font-size:13px","end","Dedicated workers (3 tabs)",[53,977],{"x":978,"y":83,"width":979,"height":980,"rx":981,"fill":87,"stroke":88,"style":982},"195.6","472.4","19","3","fill-opacity:0.7;stroke-opacity:0.9",[76,984,987],{"x":985,"y":972,"fill":73,"style":986},"674.0","font-size:12px;font-weight:600","186MB",[76,989,991],{"x":971,"y":990,"fill":73,"style":973,"textAnchor":974},"101.0","SharedWorker (3 tabs)",[53,993],{"x":978,"y":994,"width":995,"height":980,"rx":981,"fill":127,"stroke":127,"style":996},"87.0","172.7","fill-opacity:0.55;stroke-opacity:0.9",[76,998,1000],{"x":999,"y":990,"fill":73,"style":986},"374.3","68MB",[76,1002,1003],{"x":971,"y":108,"fill":73,"style":973,"textAnchor":974},"SharedWorker (1 tab)",[53,1005],{"x":978,"y":346,"width":1006,"height":980,"rx":981,"fill":73,"stroke":73,"style":1007},"157.5","fill-opacity:0.7;stroke-opacity:0.4",[76,1009,1011],{"x":1010,"y":108,"fill":73,"style":986},"359.1","62MB",[155,1013,1015],{"id":1014},"verification","Verification",[15,1017,1018],{},"Open three tabs and compare against the dedicated-worker version: worker count in Task Manager, WebSocket connections in the Network panel, and startup trace of the third tab. The third tab should show no state construction. Measure INP in each tab during bursts of real-time updates; it should be unaffected because parsing happens in the shared worker.",[155,1020,1022],{"id":1021},"sharedworker-vs-service-worker-vs-broadcastchannel","SharedWorker vs Service Worker vs BroadcastChannel",[15,1024,1025,1026,1029,1030,1033,1034,1036],{},"These three are often confused. A ",[166,1027,1028],{},"service worker"," intercepts network requests and can outlive all tabs, but it is event-driven and can be stopped by the browser at any time when idle — unsuitable for holding long-lived state or connections. A ",[166,1031,1032],{},"BroadcastChannel"," lets tabs message each other but runs no code of its own; every tab still does its own work, and leader election (choosing one tab to own the connection) is your responsibility. A ",[166,1035,35],{}," is a real, persistent thread shared by connected tabs, which is what you need for deduplicating computation or connections. For many apps, a SharedWorker for state and connections plus a service worker for caching is the cleanest division of labour.",[155,1038,1040],{"id":1039},"handling-errors-and-restarts","Handling Errors and Restarts",[15,1042,1043,1044,1047,1048,1050],{},"A crash in a dedicated worker affects one tab; a crash in a shared worker affects every connected tab at once. Wrap message handlers in try\u002Fcatch and report errors back to the requesting port rather than letting them become unhandled. Listen for the ",[33,1045,1046],{},"error"," event on the ",[33,1049,35],{}," object in each tab to detect a worker that failed to start. And because a shared worker can be terminated when the last tab closes and recreated when one opens, never assume in-memory state survives between visits — persist anything expensive to rebuild, and design the startup path so that rebuilding is correct, merely slower.",[155,1052,1054],{"id":1053},"faq","FAQ",[1056,1057,1060,1064],"details",{"className":1058},[1059],"faq-item",[1061,1062,1063],"summary",{},"Does a SharedWorker help single-tab users?",[15,1065,1066],{},"No. With one tab it behaves like a dedicated worker. Its benefit is entirely about deduplication across tabs (and iframes of the same origin), so measure how often your users actually have multiple tabs open before adopting it.",[1056,1068,1070,1073],{"className":1069},[1059],[1061,1071,1072],{},"Can I debug a SharedWorker in DevTools?",[15,1074,1075,1076,1079],{},"Yes: ",[33,1077,1078],{},"chrome:\u002F\u002Finspect\u002F#workers"," lists shared workers with an \"inspect\" link that opens dedicated DevTools for the worker thread. Console output from the worker appears there, not in the tabs' consoles.",[1056,1081,1083,1086],{"className":1082},[1059],[1061,1084,1085],{},"What happens to the shared worker on a deploy?",[15,1087,1088],{},"Tabs loaded before the deploy are connected to the old worker script; new tabs loading a different (hashed) script URL create a separate shared worker instance. Both run side by side until the old tabs close. Version your message protocol so old tabs and new workers never talk to each other by accident.",[1056,1090,1092,1095],{"className":1091},[1059],[1061,1093,1094],{},"Can iframes connect to the same SharedWorker?",[15,1096,1097],{},"Yes, if they are same-origin with the worker script. Embedded widgets of your own origin can share state with the host page this way. Cross-origin iframes cannot connect, and in some browsers storage partitioning means a third-party context gets a separate instance even for the same script URL.",[155,1099,1101],{"id":1100},"related","Related",[160,1103,1104,1111,1118],{},[163,1105,1106,1110],{},[19,1107,1109],{"href":1108},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fbundling-web-workers-with-vite-and-webpack\u002F","Bundling Web Workers with Vite and Webpack"," — emitting the shared worker script correctly.",[163,1112,1113,1117],{},[19,1114,1116],{"href":1115},"\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 workload that benefits from sharing.",[163,1119,1120,1124],{},[19,1121,1123],{"href":1122},"\u002Fadvanced-caching-strategies-cdn-architecture\u002Fservice-worker-caching-strategies\u002F","Service Worker caching strategies"," — the other kind of worker, for the network layer.",[1126,1127,1129],"script",{"type":1128},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Share One Worker Across Tabs with SharedWorker\",\n  \"description\": \"How SharedWorker deduplicates work across tabs, how to talk to it with Comlink, and the support and lifecycle caveats that decide whether to use it.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Create the shared worker with Comlink\",\n      \"text\": \"\u002F\u002F data-worker.js import * as Comlink from 'comlink'; const state = createState(); \u002F\u002F built once for all tabs onconnect = (e) => Comlink.expose(state.api, e.ports[0]); \u002F\u002F trade-off: every tab shares one thread.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Fan out real-time data from a single connection\",\n      \"text\": \"Expected outcome: one socket per browser regardless of tab count; JSON parsing of stream messages happens once, off every main thread.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Provide a fallback where SharedWorker is missing\",\n      \"text\": \"Expected outcome: one code path that degrades to a per-tab worker where needed.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Keep state warm across tab closures\",\n      \"text\": \"A SharedWorker is terminated when its last connected tab closes.\"\n    }\n  ]\n}\n",[1126,1131,1132],{"type":1128},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Share One Worker Across Tabs with SharedWorker\",\n  \"description\": \"How SharedWorker deduplicates work across tabs, how to talk to it with Comlink, and the support and lifecycle caveats that decide whether to use it.\",\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\u002Fsharing-one-worker-across-tabs-with-sharedworker\u002F\"\n  }\n}\n",[1126,1134,1135],{"type":1128},"\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\": \"Sharing One Worker Across Tabs with SharedWorker\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker\u002F\"\n    }\n  ]\n}\n",[1137,1138,1139],"style",{},"html pre.shiki code .sjfSM, html code.shiki .sjfSM{--shiki-default:#66707B;--shiki-dark:#BDC4CC;--shiki-light:#66707B}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 .sZ8jY, html code.shiki .sZ8jY{--shiki-default:#032563;--shiki-dark:#ADDCFF;--shiki-light:#032563}html pre.shiki code .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--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":375,"searchDepth":388,"depth":388,"links":1141},[1142,1143,1144,1150,1151,1152,1153,1154],{"id":157,"depth":388,"text":158},{"id":196,"depth":388,"text":197},{"id":362,"depth":388,"text":363,"children":1145},[1146,1147,1148,1149],{"id":367,"depth":416,"text":368},{"id":625,"depth":416,"text":626},{"id":798,"depth":416,"text":799},{"id":942,"depth":416,"text":943},{"id":1014,"depth":388,"text":1015},{"id":1021,"depth":388,"text":1022},{"id":1039,"depth":388,"text":1040},{"id":1053,"depth":388,"text":1054},{"id":1100,"depth":388,"text":1101},"How SharedWorker deduplicates work across tabs, how to talk to it with Comlink, and the support and lifecycle caveats that decide whether to use it.","md",{"slug":1158,"type":1159,"breadcrumb":1160,"datePublished":1168,"dateModified":1168},"sharing-one-worker-across-tabs-with-sharedworker","article",[1161,1164,1165,1166],{"name":1162,"url":1163},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1167},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker\u002F","2026-10-06","\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker",{"title":5,"description":1171},"A SharedWorker runs once per origin and serves every open tab, deduplicating caches, connections and indexes. When it helps performance and when it does not.","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker\u002Findex","nS8i3rv4ThOSOCDjmQYP_MVL74cUCR2WKwI1Kj8Ius0",[1175,1179],{"title":1176,"path":1177,"stem":1178},"Running Client-Side Search in a Web Worker","\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker\u002Findex",{"title":1180,"path":1181,"stem":1182},"Transferring ArrayBuffers to Workers Without Copying","\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying\u002Findex",1791308078484]