[{"data":1,"prerenderedAt":1338},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker":1329},{"id":4,"title":5,"body":6,"description":1310,"extension":1311,"meta":1312,"navigation":406,"path":1324,"seo":1325,"stem":1327,"__hash__":1328},"content\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker\u002Findex.md","Running Client-Side Search in a Web Worker",{"type":7,"value":8,"toc":1295},"minimark",[9,14,29,32,181,186,215,219,225,231,237,243,332,336,341,680,683,687,885,888,892,895,898,902,909,1054,1057,1162,1166,1173,1177,1189,1193,1203,1207,1219,1228,1237,1253,1257,1280,1285,1288,1291],[10,11,13],"h1",{"id":12},"how-to-run-client-side-search-in-a-web-worker","How to Run Client-Side Search in a Web Worker",[15,16,17,18,23,24,28],"p",{},"This guide applies ",[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"," to one of the most common heavy client-side workloads, within ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",": full-text or fuzzy search over a dataset shipped to the browser — documentation sites, product catalogues, command palettes, contact lists.",[15,30,31],{},"Client-side search has two expensive phases. Building the index (tokenising thousands of documents, computing term frequencies) can take hundreds of milliseconds and usually runs at startup, inflating Total Blocking Time and blocking early interactions. Querying (fuzzy matching against the index, scoring, sorting) runs on every keystroke and directly determines INP for typing. Both are pure computation with small inputs and outputs — the ideal shape for a worker.",[15,33,34,170,174,175,42],{},[35,36,42,43,42,50,42,54,42,57,42,75,42,83,42,89,42,96,42,103,42,106,42,110,42,113,42,117,42,126,42,129,42,132,42,138,42,144,42,149,42,153,42,157,42,162,42,167],"svg",{"viewBox":37,"width":38,"role":39,"ariaLabel":40,"style":41},"0 0 760 316","100%","img","Sequence of the main thread posting the document set once to a worker to build the index, then sending queries and receiving lists of result IDs.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[44,45],"rect",{"className":46,"x":48,"y":48,"width":38,"height":38,"fill":49},[47],"svg-canvas","0","#ffffff",[51,52,53],"title",{},"Search with the index in a worker",[55,56,40],"desc",{},[58,59,60],"defs",{},[61,62,69],"marker",{"id":63,"viewBox":64,"refX":65,"refY":66,"markerWidth":67,"markerHeight":67,"orient":68},"fa8edf7418","0 0 10 10","9","5","7","auto-start-reverse",[70,71],"path",{"d":72,"fill":73,"style":74},"M0 0 L10 5 L0 10 z","currentColor","fill-opacity:0.7",[44,76],{"x":77,"y":77,"width":78,"height":79,"rx":80,"fill":81,"stroke":73,"style":82},"1","758","314","10","none","stroke-opacity:0.18",[84,85,53],"text",{"x":86,"y":87,"fill":73,"style":88},"28.0","34.0","font-size:16px;font-weight:700",[44,90],{"x":91,"y":92,"width":93,"height":87,"rx":94,"fill":73,"stroke":73,"style":95},"70.3","56.0","150.0","6","fill-opacity:0.06;stroke-opacity:0.4",[84,97,102],{"x":98,"y":99,"fill":73,"style":100,"textAnchor":101},"145.3","78.0","font-size:13px;font-weight:700","middle","Main thread",[44,104],{"x":105,"y":92,"width":93,"height":87,"rx":94,"fill":73,"stroke":73,"style":95},"305.0",[84,107,109],{"x":108,"y":99,"fill":73,"style":100,"textAnchor":101},"380.0","Worker",[44,111],{"x":112,"y":92,"width":93,"height":87,"rx":94,"fill":73,"stroke":73,"style":95},"539.7",[84,114,116],{"x":115,"y":99,"fill":73,"style":100,"textAnchor":101},"614.7","Network",[118,119],"line",{"x1":98,"y1":120,"x2":98,"y2":121,"stroke":73,"strokeWidth":122,"strokeDashArray":123,"style":125},"90.0","268.0","1.5",[124,124],"4","stroke-opacity:0.35",[118,127],{"x1":108,"y1":120,"x2":108,"y2":121,"stroke":73,"strokeWidth":122,"strokeDashArray":128,"style":125},[124,124],[118,130],{"x1":115,"y1":120,"x2":115,"y2":121,"stroke":73,"strokeWidth":122,"strokeDashArray":131,"style":125},[124,124],[118,133],{"x1":108,"y1":134,"x2":135,"y2":134,"stroke":73,"strokeWidth":122,"style":136,"markerEnd":137},"124.0","612.7","stroke-opacity:0.6","url(#fa8edf7418)",[84,139,143],{"x":140,"y":141,"fill":73,"style":142,"textAnchor":101},"497.3","117.0","font-size:12px","fetch docs.json",[118,145],{"x1":115,"y1":146,"x2":147,"y2":146,"stroke":73,"strokeWidth":122,"strokeDashArray":148,"style":136,"markerEnd":137},"164.0","382.0",[66,124],[84,150,152],{"x":140,"y":151,"fill":73,"style":142,"textAnchor":101},"157.0","3.2MB JSON",[118,154],{"x1":98,"y1":155,"x2":156,"y2":155,"stroke":73,"strokeWidth":122,"style":136,"markerEnd":137},"204.0","378.0",[84,158,161],{"x":159,"y":160,"fill":73,"style":142,"textAnchor":101},"262.7","197.0","query \"cach\"",[118,163],{"x1":108,"y1":164,"x2":165,"y2":164,"stroke":73,"strokeWidth":122,"strokeDashArray":166,"style":136,"markerEnd":137},"244.0","147.3",[66,124],[84,168],{"x":159,"y":169,"fill":73,"style":142,"textAnchor":101},"237.0",[171,172,173],"span",{},"ids"," (top 20) ",[84,176,180],{"x":86,"y":177,"fill":178,"style":179},"294.0","#51617a","font-size:12.5px","The worker fetches and indexes the documents itself; the main thread only sends query strings and receives small arrays of IDs.",[182,183,185],"h2",{"id":184},"rapid-diagnosis","Rapid Diagnosis",[187,188,189,197,203,209],"ul",{},[190,191,192,196],"li",{},[193,194,195],"strong",{},"Profile startup."," In a throttled trace, look for a long task building the search index (calls into Lunr, FlexSearch, MiniSearch, Fuse.js or your own code) during or shortly after load.",[190,198,199,202],{},[193,200,201],{},"Profile typing."," Type a query; each keystroke's processing time should be dominated by the search call if this guide applies.",[190,204,205,208],{},[193,206,207],{},"Check dataset size."," Documents × average length. Above a few thousand documents, main-thread indexing becomes a noticeable long task on mobile.",[190,210,211,214],{},[193,212,213],{},"Check result rendering."," If rendering results costs more than searching, fix rendering (cap results, virtualise) first; a worker will not help that part.",[182,216,218],{"id":217},"root-cause-analysis","Root Cause Analysis",[15,220,221,224],{},[193,222,223],{},"1. Index built on the main thread at startup."," Libraries are typically initialised in application code with the full document set, producing one long task.",[15,226,227,230],{},[193,228,229],{},"2. Fuzzy matching per keystroke."," Fuzzy algorithms are expensive; scoring every document on every keystroke scales linearly with the dataset.",[15,232,233,236],{},[193,234,235],{},"3. Large data cloned to the worker."," Teams move search to a worker but post the full document array from the main thread, paying a large structured-clone cost there instead.",[15,238,239,242],{},[193,240,241],{},"4. Stale query results."," Responses for earlier queries arrive after later ones and overwrite fresher results, causing flicker and wasted rendering.",[15,244,245],{},[35,246,42,249,42,252,42,255,42,257,42,260,42,262,42,270,42,277,42,285,42,290,42,294,42,300,42,304,42,308,42,312,42,316,42,320,42,323,42,326,42],{"viewBox":247,"width":38,"role":39,"ariaLabel":248,"style":41},"0 0 760 216","Bar chart comparing main-thread time for index build and per-keystroke query when search runs on the main thread versus in a worker.",[44,250],{"className":251,"x":48,"y":48,"width":38,"height":38,"fill":49},[47],[51,253,254],{},"Main-thread cost: in-page search vs worker search",[55,256,248],{},[44,258],{"x":77,"y":77,"width":78,"height":259,"rx":80,"fill":81,"stroke":73,"style":82},"214",[84,261,254],{"x":86,"y":87,"fill":73,"style":88},[118,263],{"x1":264,"y1":265,"x2":264,"y2":266,"stroke":267,"strokeWidth":122,"strokeDashArray":268,"style":269},"256.2","72.0","194.0","#b8860b",[124,124],"stroke-opacity:0.9",[84,271,276],{"x":272,"y":273,"fill":73,"style":274,"textAnchor":275},"188.5","92.0","font-size:13px","end","Index build (main thread)",[44,278],{"x":279,"y":99,"width":280,"height":281,"rx":282,"fill":283,"stroke":267,"style":284},"200.5","467.5","19","3","#ffc300","fill-opacity:0.7;stroke-opacity:0.9",[84,286,289],{"x":287,"y":273,"fill":73,"style":288},"674.0","font-size:12px;font-weight:600","420ms",[84,291,293],{"x":272,"y":292,"fill":73,"style":274,"textAnchor":275},"123.0","Index build (worker)",[44,295],{"x":279,"y":296,"width":297,"height":281,"rx":282,"fill":298,"stroke":298,"style":299},"109.0","2.0","#0466c8","fill-opacity:0.55;stroke-opacity:0.9",[84,301,303],{"x":302,"y":292,"fill":73,"style":288},"208.5","0.5ms",[84,305,307],{"x":272,"y":306,"fill":73,"style":274,"textAnchor":275},"154.0","Query per key (main thread)",[44,309],{"x":279,"y":310,"width":311,"height":281,"rx":282,"fill":283,"stroke":267,"style":284},"140.0","94.6",[84,313,315],{"x":314,"y":306,"fill":73,"style":288},"301.1","85ms",[84,317,319],{"x":272,"y":318,"fill":73,"style":274,"textAnchor":275},"185.0","Query per key (worker)",[44,321],{"x":279,"y":322,"width":297,"height":281,"rx":282,"fill":298,"stroke":298,"style":299},"171.0",[84,324,325],{"x":302,"y":318,"fill":73,"style":288},"1.5ms",[84,327,331],{"x":264,"y":328,"fill":329,"style":330,"textAnchor":101},"68.0","#8a6608","font-size:12px;font-weight:700","50ms",[182,333,335],{"id":334},"step-by-step-resolution","Step-by-Step Resolution",[337,338,340],"h3",{"id":339},"_1-fetch-and-index-inside-the-worker","1. Fetch and index inside the worker",[342,343,348],"pre",{"className":344,"code":345,"language":346,"meta":347,"style":347},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F search-worker.js\nimport MiniSearch from 'minisearch';\nimport * as Comlink from 'comlink';\n\nlet index;\nconst ready = (async () => {\n  const docs = await (await fetch('\u002Fsearch\u002Fdocs.json')).json();   \u002F\u002F never touches main thread\n  index = new MiniSearch({ fields: ['title', 'body'], storeFields: ['id'], searchOptions: { prefix: true, fuzzy: 0.2 } });\n  index.addAll(docs);\n})();\n\nComlink.expose({\n  async search(q, limit = 20) {\n    await ready;\n    return index.search(q).slice(0, limit).map((r) => r.id);      \u002F\u002F small result\n  },\n});\n\u002F\u002F trade-off: building in the worker still costs CPU and battery, just not main-\n\u002F\u002F thread time. For very large datasets, prebuild the index at deploy time and\n\u002F\u002F load the serialised index instead (MiniSearch.loadJSON) to skip indexing.\n","javascript","",[349,350,351,358,379,401,408,417,444,485,533,545,551,556,568,596,605,650,656,662,668,674],"code",{"__ignoreMap":347},[171,352,354],{"class":118,"line":353},1,[171,355,357],{"class":356},"sjfSM","\u002F\u002F search-worker.js\n",[171,359,361,365,369,372,376],{"class":118,"line":360},2,[171,362,364],{"class":363},"sPARh","import",[171,366,368],{"class":367},"saISM"," MiniSearch ",[171,370,371],{"class":363},"from",[171,373,375],{"class":374},"sZ8jY"," 'minisearch'",[171,377,378],{"class":367},";\n",[171,380,382,384,388,391,394,396,399],{"class":118,"line":381},3,[171,383,364],{"class":363},[171,385,387],{"class":386},"sPXB4"," *",[171,389,390],{"class":363}," as",[171,392,393],{"class":367}," Comlink ",[171,395,371],{"class":363},[171,397,398],{"class":374}," 'comlink'",[171,400,378],{"class":367},[171,402,404],{"class":118,"line":403},4,[171,405,407],{"emptyLinePlaceholder":406},true,"\n",[171,409,411,414],{"class":118,"line":410},5,[171,412,413],{"class":363},"let",[171,415,416],{"class":367}," index;\n",[171,418,420,423,426,429,432,435,438,441],{"class":118,"line":419},6,[171,421,422],{"class":363},"const",[171,424,425],{"class":386}," ready",[171,427,428],{"class":363}," =",[171,430,431],{"class":367}," (",[171,433,434],{"class":363},"async",[171,436,437],{"class":367}," () ",[171,439,440],{"class":363},"=>",[171,442,443],{"class":367}," {\n",[171,445,447,450,453,455,458,460,463,467,470,473,476,479,482],{"class":118,"line":446},7,[171,448,449],{"class":363},"  const",[171,451,452],{"class":386}," docs",[171,454,428],{"class":363},[171,456,457],{"class":363}," await",[171,459,431],{"class":367},[171,461,462],{"class":363},"await",[171,464,466],{"class":465},"smZ65"," fetch",[171,468,469],{"class":367},"(",[171,471,472],{"class":374},"'\u002Fsearch\u002Fdocs.json'",[171,474,475],{"class":367},")).",[171,477,478],{"class":465},"json",[171,480,481],{"class":367},"();   ",[171,483,484],{"class":356},"\u002F\u002F never touches main thread\n",[171,486,488,491,494,497,500,503,506,509,512,515,518,521,524,527,530],{"class":118,"line":487},8,[171,489,490],{"class":367},"  index ",[171,492,493],{"class":363},"=",[171,495,496],{"class":363}," new",[171,498,499],{"class":465}," MiniSearch",[171,501,502],{"class":367},"({ fields: [",[171,504,505],{"class":374},"'title'",[171,507,508],{"class":367},", ",[171,510,511],{"class":374},"'body'",[171,513,514],{"class":367},"], storeFields: [",[171,516,517],{"class":374},"'id'",[171,519,520],{"class":367},"], searchOptions: { prefix: ",[171,522,523],{"class":386},"true",[171,525,526],{"class":367},", fuzzy: ",[171,528,529],{"class":386},"0.2",[171,531,532],{"class":367}," } });\n",[171,534,536,539,542],{"class":118,"line":535},9,[171,537,538],{"class":367},"  index.",[171,540,541],{"class":465},"addAll",[171,543,544],{"class":367},"(docs);\n",[171,546,548],{"class":118,"line":547},10,[171,549,550],{"class":367},"})();\n",[171,552,554],{"class":118,"line":553},11,[171,555,407],{"emptyLinePlaceholder":406},[171,557,559,562,565],{"class":118,"line":558},12,[171,560,561],{"class":367},"Comlink.",[171,563,564],{"class":465},"expose",[171,566,567],{"class":367},"({\n",[171,569,571,574,577,579,583,585,588,590,593],{"class":118,"line":570},13,[171,572,573],{"class":363},"  async",[171,575,576],{"class":465}," search",[171,578,469],{"class":367},[171,580,582],{"class":581},"sQw3B","q",[171,584,508],{"class":367},[171,586,587],{"class":581},"limit",[171,589,428],{"class":363},[171,591,592],{"class":386}," 20",[171,594,595],{"class":367},") {\n",[171,597,599,602],{"class":118,"line":598},14,[171,600,601],{"class":363},"    await",[171,603,604],{"class":367}," ready;\n",[171,606,608,611,614,617,620,623,625,627,630,633,636,639,642,644,647],{"class":118,"line":607},15,[171,609,610],{"class":363},"    return",[171,612,613],{"class":367}," index.",[171,615,616],{"class":465},"search",[171,618,619],{"class":367},"(q).",[171,621,622],{"class":465},"slice",[171,624,469],{"class":367},[171,626,48],{"class":386},[171,628,629],{"class":367},", limit).",[171,631,632],{"class":465},"map",[171,634,635],{"class":367},"((",[171,637,638],{"class":581},"r",[171,640,641],{"class":367},") ",[171,643,440],{"class":363},[171,645,646],{"class":367}," r.id);      ",[171,648,649],{"class":356},"\u002F\u002F small result\n",[171,651,653],{"class":118,"line":652},16,[171,654,655],{"class":367},"  },\n",[171,657,659],{"class":118,"line":658},17,[171,660,661],{"class":367},"});\n",[171,663,665],{"class":118,"line":664},18,[171,666,667],{"class":356},"\u002F\u002F trade-off: building in the worker still costs CPU and battery, just not main-\n",[171,669,671],{"class":118,"line":670},19,[171,672,673],{"class":356},"\u002F\u002F thread time. For very large datasets, prebuild the index at deploy time and\n",[171,675,677],{"class":118,"line":676},20,[171,678,679],{"class":356},"\u002F\u002F load the serialised index instead (MiniSearch.loadJSON) to skip indexing.\n",[15,681,682],{},"Expected outcome: no index-building long task on the main thread; startup TBT drops by the full indexing time.",[337,684,686],{"id":685},"_2-query-from-the-main-thread-with-stale-result-protection","2. Query from the main thread with stale-result protection",[342,688,690],{"className":344,"code":689,"language":346,"meta":347,"style":347},"\u002F\u002F search-client.js\nimport * as Comlink from 'comlink';\nconst api = Comlink.wrap(new Worker(new URL('.\u002Fsearch-worker.js', import.meta.url), { type: 'module' }));\nlet seq = 0;\nexport async function search(q) {\n  const mine = ++seq;\n  const ids = await api.search(q);\n  return mine === seq ? ids : null;              \u002F\u002F drop responses for superseded queries\n}\n\u002F\u002F trade-off: dropping stale responses means a very fast typist sees results\n\u002F\u002F only when they pause. That is usually what users want; if not, render stale\n\u002F\u002F results with a subdued style until the fresh ones arrive.\n",[349,691,692,697,713,767,781,800,815,834,865,870,875,880],{"__ignoreMap":347},[171,693,694],{"class":118,"line":353},[171,695,696],{"class":356},"\u002F\u002F search-client.js\n",[171,698,699,701,703,705,707,709,711],{"class":118,"line":360},[171,700,364],{"class":363},[171,702,387],{"class":386},[171,704,390],{"class":363},[171,706,393],{"class":367},[171,708,371],{"class":363},[171,710,398],{"class":374},[171,712,378],{"class":367},[171,714,715,717,720,722,725,728,730,733,736,738,740,743,745,748,750,752,755,758,761,764],{"class":118,"line":381},[171,716,422],{"class":363},[171,718,719],{"class":386}," api",[171,721,428],{"class":363},[171,723,724],{"class":367}," Comlink.",[171,726,727],{"class":465},"wrap",[171,729,469],{"class":367},[171,731,732],{"class":363},"new",[171,734,735],{"class":465}," Worker",[171,737,469],{"class":367},[171,739,732],{"class":363},[171,741,742],{"class":465}," URL",[171,744,469],{"class":367},[171,746,747],{"class":374},"'.\u002Fsearch-worker.js'",[171,749,508],{"class":367},[171,751,364],{"class":363},[171,753,754],{"class":367},".",[171,756,757],{"class":386},"meta",[171,759,760],{"class":367},".url), { type: ",[171,762,763],{"class":374},"'module'",[171,765,766],{"class":367}," }));\n",[171,768,769,771,774,776,779],{"class":118,"line":403},[171,770,413],{"class":363},[171,772,773],{"class":367}," seq ",[171,775,493],{"class":363},[171,777,778],{"class":386}," 0",[171,780,378],{"class":367},[171,782,783,786,789,792,794,796,798],{"class":118,"line":410},[171,784,785],{"class":363},"export",[171,787,788],{"class":363}," async",[171,790,791],{"class":363}," function",[171,793,576],{"class":465},[171,795,469],{"class":367},[171,797,582],{"class":581},[171,799,595],{"class":367},[171,801,802,804,807,809,812],{"class":118,"line":419},[171,803,449],{"class":363},[171,805,806],{"class":386}," mine",[171,808,428],{"class":363},[171,810,811],{"class":363}," ++",[171,813,814],{"class":367},"seq;\n",[171,816,817,819,822,824,826,829,831],{"class":118,"line":446},[171,818,449],{"class":363},[171,820,821],{"class":386}," ids",[171,823,428],{"class":363},[171,825,457],{"class":363},[171,827,828],{"class":367}," api.",[171,830,616],{"class":465},[171,832,833],{"class":367},"(q);\n",[171,835,836,839,842,845,847,850,853,856,859,862],{"class":118,"line":487},[171,837,838],{"class":363},"  return",[171,840,841],{"class":367}," mine ",[171,843,844],{"class":363},"===",[171,846,773],{"class":367},[171,848,849],{"class":363},"?",[171,851,852],{"class":367}," ids ",[171,854,855],{"class":363},":",[171,857,858],{"class":386}," null",[171,860,861],{"class":367},";              ",[171,863,864],{"class":356},"\u002F\u002F drop responses for superseded queries\n",[171,866,867],{"class":118,"line":535},[171,868,869],{"class":367},"}\n",[171,871,872],{"class":118,"line":547},[171,873,874],{"class":356},"\u002F\u002F trade-off: dropping stale responses means a very fast typist sees results\n",[171,876,877],{"class":118,"line":553},[171,878,879],{"class":356},"\u002F\u002F only when they pause. That is usually what users want; if not, render stale\n",[171,881,882],{"class":118,"line":558},[171,883,884],{"class":356},"\u002F\u002F results with a subdued style until the fresh ones arrive.\n",[15,886,887],{},"Expected outcome: each keystroke costs the main thread only a message post and, later, rendering a small result list.",[337,889,891],{"id":890},"_3-return-ids-render-from-main-thread-data","3. Return IDs, render from main-thread data",[15,893,894],{},"Return IDs (or minimal display fields) and look up display data the main thread already holds — or a compact summary map — rather than returning full documents.",[15,896,897],{},"Expected outcome: responses stay a few hundred bytes; no deserialisation cost on arrival.",[337,899,901],{"id":900},"_4-prebuild-the-index-for-large-corpora","4. Prebuild the index for large corpora",[15,903,904,905,908],{},"For corpora above tens of thousands of documents, build the index at deploy time and ship it serialised. The worker loads it with a single ",[349,906,907],{},"JSON.parse"," (still in the worker) instead of indexing.",[342,910,912],{"className":344,"code":911,"language":346,"meta":347,"style":347},"\u002F\u002F build-index.mjs (run at build time)\nimport MiniSearch from 'minisearch';\nimport { readFileSync, writeFileSync } from 'node:fs';\nconst docs = JSON.parse(readFileSync('docs.json', 'utf8'));\nconst ms = new MiniSearch({ fields: ['title', 'body'], storeFields: ['id'] });\nms.addAll(docs);\nwriteFileSync('public\u002Fsearch\u002Findex.json', JSON.stringify(ms));\n\u002F\u002F trade-off: a serialised index is often larger than the raw documents. Check\n\u002F\u002F the compressed size; for some corpora, indexing in the worker is the better\n\u002F\u002F trade between bytes and CPU.\n",[349,913,914,919,931,945,979,1007,1016,1039,1044,1049],{"__ignoreMap":347},[171,915,916],{"class":118,"line":353},[171,917,918],{"class":356},"\u002F\u002F build-index.mjs (run at build time)\n",[171,920,921,923,925,927,929],{"class":118,"line":360},[171,922,364],{"class":363},[171,924,368],{"class":367},[171,926,371],{"class":363},[171,928,375],{"class":374},[171,930,378],{"class":367},[171,932,933,935,938,940,943],{"class":118,"line":381},[171,934,364],{"class":363},[171,936,937],{"class":367}," { readFileSync, writeFileSync } ",[171,939,371],{"class":363},[171,941,942],{"class":374}," 'node:fs'",[171,944,378],{"class":367},[171,946,947,949,951,953,956,958,961,963,966,968,971,973,976],{"class":118,"line":403},[171,948,422],{"class":363},[171,950,452],{"class":386},[171,952,428],{"class":363},[171,954,955],{"class":386}," JSON",[171,957,754],{"class":367},[171,959,960],{"class":465},"parse",[171,962,469],{"class":367},[171,964,965],{"class":465},"readFileSync",[171,967,469],{"class":367},[171,969,970],{"class":374},"'docs.json'",[171,972,508],{"class":367},[171,974,975],{"class":374},"'utf8'",[171,977,978],{"class":367},"));\n",[171,980,981,983,986,988,990,992,994,996,998,1000,1002,1004],{"class":118,"line":410},[171,982,422],{"class":363},[171,984,985],{"class":386}," ms",[171,987,428],{"class":363},[171,989,496],{"class":363},[171,991,499],{"class":465},[171,993,502],{"class":367},[171,995,505],{"class":374},[171,997,508],{"class":367},[171,999,511],{"class":374},[171,1001,514],{"class":367},[171,1003,517],{"class":374},[171,1005,1006],{"class":367},"] });\n",[171,1008,1009,1012,1014],{"class":118,"line":419},[171,1010,1011],{"class":367},"ms.",[171,1013,541],{"class":465},[171,1015,544],{"class":367},[171,1017,1018,1021,1023,1026,1028,1031,1033,1036],{"class":118,"line":446},[171,1019,1020],{"class":465},"writeFileSync",[171,1022,469],{"class":367},[171,1024,1025],{"class":374},"'public\u002Fsearch\u002Findex.json'",[171,1027,508],{"class":367},[171,1029,1030],{"class":386},"JSON",[171,1032,754],{"class":367},[171,1034,1035],{"class":465},"stringify",[171,1037,1038],{"class":367},"(ms));\n",[171,1040,1041],{"class":118,"line":487},[171,1042,1043],{"class":356},"\u002F\u002F trade-off: a serialised index is often larger than the raw documents. Check\n",[171,1045,1046],{"class":118,"line":535},[171,1047,1048],{"class":356},"\u002F\u002F the compressed size; for some corpora, indexing in the worker is the better\n",[171,1050,1051],{"class":118,"line":547},[171,1052,1053],{"class":356},"\u002F\u002F trade between bytes and CPU.\n",[15,1055,1056],{},"Expected outcome: search ready within a few hundred milliseconds of the worker starting, even on large sites.",[15,1058,1059],{},[35,1060,42,1063,42,1066,42,1069,42,1071,42,1074,42,1076,42,1081,42,1087,42,1091,42,1094,42,1097,42,1100,42,1103,42,1107,42,1111,42,1114,42,1118,42,1122,42,1128,42,1132,42,1136,42,1141,42,1143,42,1146,42,1150,42,1153,42,1156,42,1159,42],{"viewBox":1061,"width":38,"role":39,"ariaLabel":1062,"style":41},"0 0 760 318","Four steps to relocate client-side search into a worker, from fetching inside the worker to prebuilding the index.",[44,1064],{"className":1065,"x":48,"y":48,"width":38,"height":38,"fill":49},[47],[51,1067,1068],{},"Moving search off the main thread",[55,1070,1062],{},[44,1072],{"x":77,"y":77,"width":78,"height":1073,"rx":80,"fill":81,"stroke":73,"style":82},"316",[84,1075,1068],{"x":86,"y":87,"fill":73,"style":88},[44,1077],{"x":265,"y":92,"width":1078,"height":1079,"rx":94,"fill":298,"stroke":298,"style":1080},"660.0","51.0","fill-opacity:0.14;stroke-opacity:0.9",[84,1082,1086],{"x":1083,"y":1084,"fill":73,"style":100,"textAnchor":1085},"86.0","77.0","start","Fetch and index in the worker",[84,1088,1090],{"x":1083,"y":1089,"fill":73,"style":142,"textAnchor":1085},"94.0","The document set never crosses the main thread",[44,1092],{"x":265,"y":1093,"width":1078,"height":1079,"rx":94,"fill":298,"stroke":298,"style":1080},"119.0",[84,1095,1096],{"x":1083,"y":310,"fill":73,"style":100,"textAnchor":1085},"Query with sequence numbers",[84,1098,1099],{"x":1083,"y":151,"fill":73,"style":142,"textAnchor":1085},"Drop responses for superseded queries",[44,1101],{"x":265,"y":1102,"width":1078,"height":1079,"rx":94,"fill":298,"stroke":298,"style":1080},"182.0",[84,1104,1106],{"x":1083,"y":1105,"fill":73,"style":100,"textAnchor":1085},"203.0","Return IDs only",[84,1108,1110],{"x":1083,"y":1109,"fill":73,"style":142,"textAnchor":1085},"220.0","Render from data the main thread already holds",[44,1112],{"x":265,"y":1113,"width":1078,"height":1079,"rx":94,"fill":298,"stroke":298,"style":1080},"245.0",[84,1115,1117],{"x":1083,"y":1116,"fill":73,"style":100,"textAnchor":1085},"266.0","Prebuild large indexes at deploy time",[84,1119,1121],{"x":1083,"y":1120,"fill":73,"style":142,"textAnchor":1085},"283.0","Load serialised JSON instead of indexing",[118,1123],{"x1":1124,"y1":1125,"x2":1124,"y2":1126,"stroke":73,"strokeWidth":122,"style":1127},"43.0","95.5","130.5","stroke-opacity:0.3",[118,1129],{"x1":1124,"y1":1130,"x2":1124,"y2":1131,"stroke":73,"strokeWidth":122,"style":1127},"158.5","193.5",[118,1133],{"x1":1124,"y1":1134,"x2":1124,"y2":1135,"stroke":73,"strokeWidth":122,"style":1127},"221.5","256.5",[1137,1138],"circle",{"cx":1124,"cy":1139,"r":1140,"fill":298},"81.5","13",[84,1142,77],{"x":1124,"y":1083,"fill":49,"style":100,"textAnchor":101},[1137,1144],{"cx":1124,"cy":1145,"r":1140,"fill":298},"144.5",[84,1147,1149],{"x":1124,"y":1148,"fill":49,"style":100,"textAnchor":101},"149.0","2",[1137,1151],{"cx":1124,"cy":1152,"r":1140,"fill":298},"207.5",[84,1154,282],{"x":1124,"y":1155,"fill":49,"style":100,"textAnchor":101},"212.0",[1137,1157],{"cx":1124,"cy":1158,"r":1140,"fill":298},"270.5",[84,1160,124],{"x":1124,"y":1161,"fill":49,"style":100,"textAnchor":101},"275.0",[182,1163,1165],{"id":1164},"verification","Verification",[15,1167,1168,1169,1172],{},"In a throttled trace, startup should show no indexing task on the main thread, and typing should produce interactions well under ",[349,1170,1171],{},"100ms",", dominated by rendering. Use the Performance panel's thread selector to confirm the work now appears on the worker's thread. In RUM, compare INP for keyboard interactions on the search input before and after.",[182,1174,1176],{"id":1175},"lifecycle-when-to-start-the-worker","Lifecycle: When to Start the Worker",[15,1178,1179,1180,1183,1184,1188],{},"Starting a worker costs a script download, evaluation and (here) a data fetch. For a search box in the header of every page, start it on idle after load so it is ready when the user first focuses the field; for a command palette opened with a keyboard shortcut, start it on the first ",[349,1181,1182],{},"keydown"," of the modifier key, or on focus of the trigger. Do not start it during the critical rendering path — workers run on other threads, but their script and data downloads compete for bandwidth with LCP resources. A shared search worker across tabs is possible with a SharedWorker, covered in ",[19,1185,1187],{"href":1186},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fsharing-one-worker-across-tabs-with-sharedworker\u002F","sharing one worker across tabs with SharedWorker",", though for most sites a per-tab worker is simpler and fast enough.",[182,1190,1192],{"id":1191},"keeping-the-index-fresh","Keeping the Index Fresh",[15,1194,1195,1196,508,1199,1202],{},"Client-side indexes go stale when content changes. Version the document set (a hash in its URL, emitted by your build) and let the worker fetch the current version on startup; with immutable caching, unchanged versions cost nothing on repeat visits. For data that changes during a session — a user's own records in a CRM — apply incremental updates in the worker (",[349,1197,1198],{},"index.add",[349,1200,1201],{},"index.discard",") from the same messages that update the UI, rather than rebuilding. Rebuilds are the expensive path; reserve them for structural changes such as a new field becoming searchable.",[182,1204,1206],{"id":1205},"faq","FAQ",[1208,1209,1212,1216],"details",{"className":1210},[1211],"faq-item",[1213,1214,1215],"summary",{},"Which search library works best in a worker?",[15,1217,1218],{},"Any library without DOM dependencies works. MiniSearch and FlexSearch are compact and fast to query; Fuse.js is simple but slower for large datasets because it scans rather than indexes. Choose based on query features (prefix, fuzzy, field boosting) and serialised index size, then measure query time on a throttled device.",[1208,1220,1222,1225],{"className":1221},[1211],[1213,1223,1224],{},"Should highlighting matches happen in the worker?",[15,1226,1227],{},"Computing match positions can be done in the worker and returned with the IDs; applying highlights is DOM work and belongs on the main thread. Keep the returned data small — positions for the visible results only.",[1208,1229,1231,1234],{"className":1230},[1211],[1213,1232,1233],{},"Is a worker worth it for a few hundred documents?",[15,1235,1236],{},"Usually not. Querying a few hundred short documents takes a millisecond or two on the main thread. Workers add complexity and startup cost; reserve them for datasets where indexing or querying measurably exceeds the long-task threshold on your target devices.",[1208,1238,1240,1243],{"className":1239},[1211],[1213,1241,1242],{},"What happens if the user searches before the index is ready?",[15,1244,1245,1246,1248,1249,1252],{},"The worker's ",[349,1247,616],{}," awaits the ",[349,1250,1251],{},"ready"," promise, so the first query simply resolves later. Show a \"Loading search…\" state in the results area rather than an empty list, so users do not conclude there are no matches. If early searches are common, start the worker earlier or ship a prebuilt index that loads faster than building one.",[182,1254,1256],{"id":1255},"related","Related",[187,1258,1259,1266,1273],{},[190,1260,1261,1265],{},[19,1262,1264],{"href":1263},"\u002Fcore-web-vitals-measurement\u002Fprofiling-event-handlers-for-inp\u002Ffixing-slow-inp-on-text-inputs-and-typeahead\u002F","Fixing slow INP on text inputs and typeahead"," — the main-thread side of the same interaction.",[190,1267,1268,1272],{},[19,1269,1271],{"href":1270},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Ftransferring-arraybuffers-to-workers-without-copying\u002F","Transferring ArrayBuffers to workers without copying"," — when results are large.",[190,1274,1275,1279],{},[19,1276,1278],{"href":1277},"\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"," — shipping the worker script correctly.",[1281,1282,1284],"script",{"type":1283},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Run Client-Side Search in a Web Worker\",\n  \"description\": \"Move index construction and querying for client-side search into a worker, keeping every keystroke cheap on the main thread.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Fetch and index inside the worker\",\n      \"text\": \"let index; const ready = (async () => { const docs = await (await fetch('\u002Fsearch\u002Fdocs.json')).json(); \u002F\u002F never touches main thread index = new MiniSearch({ fields: ['title', 'body'], storeFields: ['id'], searchOptions: { prefix: true, fuzzy: 0.2 } }); index.addAll(docs); })();\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Query from the main thread with stale-result protection\",\n      \"text\": \"Expected outcome: each keystroke costs the main thread only a message post and, later, rendering a small result list.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Return IDs, render from main-thread data\",\n      \"text\": \"Return IDs (or minimal display fields) and look up display data the main thread already holds — or a compact summary map — rather than returning full documents.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Prebuild the index for large corpora\",\n      \"text\": \"For corpora above tens of thousands of documents, build the index at deploy time and ship it serialised.\"\n    }\n  ]\n}\n",[1281,1286,1287],{"type":1283},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Run Client-Side Search in a Web Worker\",\n  \"description\": \"Move index construction and querying for client-side search into a worker, keeping every keystroke cheap on the main thread.\",\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\u002Frunning-client-side-search-in-a-web-worker\u002F\"\n  }\n}\n",[1281,1289,1290],{"type":1283},"\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\": \"Running Client-Side Search in a Web Worker\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker\u002F\"\n    }\n  ]\n}\n",[1292,1293,1294],"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 .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 .sPXB4, html code.shiki .sPXB4{--shiki-default:#023B95;--shiki-dark:#91CBFF;--shiki-light:#023B95}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":347,"searchDepth":360,"depth":360,"links":1296},[1297,1298,1299,1305,1306,1307,1308,1309],{"id":184,"depth":360,"text":185},{"id":217,"depth":360,"text":218},{"id":334,"depth":360,"text":335,"children":1300},[1301,1302,1303,1304],{"id":339,"depth":381,"text":340},{"id":685,"depth":381,"text":686},{"id":890,"depth":381,"text":891},{"id":900,"depth":381,"text":901},{"id":1164,"depth":360,"text":1165},{"id":1175,"depth":360,"text":1176},{"id":1191,"depth":360,"text":1192},{"id":1205,"depth":360,"text":1206},{"id":1255,"depth":360,"text":1256},"Move index construction and querying for client-side search into a worker, keeping every keystroke cheap on the main thread.","md",{"slug":1313,"type":1314,"breadcrumb":1315,"datePublished":1323,"dateModified":1323},"running-client-side-search-in-a-web-worker","article",[1316,1319,1320,1321],{"name":1317,"url":1318},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1322},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker\u002F","2026-10-06","\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker",{"title":5,"description":1326},"Build and query a client-side search index in a Web Worker so typing stays under 200ms INP. Index off-thread, return IDs, and cancel stale queries.","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Frunning-client-side-search-in-a-web-worker\u002Findex","K52jbiAqm5GzuTt_XtuCkTuE5AFXQfHSIpQekEo9OA8",[1330,1334],{"title":1331,"path":1332,"stem":1333},"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",{"title":1335,"path":1336,"stem":1337},"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",1791308078809]