[{"data":1,"prerenderedAt":1296},["ShallowReactive",2],{"content:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fbundling-web-workers-with-vite-and-webpack":3,"surroundings:\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fbundling-web-workers-with-vite-and-webpack":1288},{"id":4,"title":5,"body":6,"description":1267,"extension":1268,"meta":1269,"navigation":1281,"path":1282,"seo":1283,"stem":1286,"__hash__":1287},"content\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fbundling-web-workers-with-vite-and-webpack\u002Findex.md","Bundling Web Workers with Vite and Webpack",{"type":7,"value":8,"toc":1252},"minimark",[9,14,29,45,193,198,237,241,251,261,267,284,346,350,355,474,481,485,623,626,630,633,725,733,737,740,897,900,1037,1041,1049,1109,1113,1131,1144,1150,1154,1161,1165,1188,1197,1210,1214,1237,1242,1245,1248],[10,11,13],"h1",{"id":12},"how-to-bundle-web-workers-with-vite-and-webpack","How to Bundle Web Workers with Vite and Webpack",[15,16,17,18,23,24,28],"p",{},"This guide covers the build side of ",[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 ",[19,25,27],{"href":26},"\u002Fcore-web-vitals-measurement\u002F","Core Web Vitals & Measurement",". A worker only helps INP if it starts quickly and does not drag a second copy of your application into the browser. Both depend on how it is bundled.",[15,30,31,32,36,37,40,41,44],{},"Older setups used ",[33,34,35],"code",{},"worker-loader",", string-concatenated blob URLs, or a hand-copied ",[33,38,39],{},"public\u002Fworker.js",". Modern bundlers recognise a single standard pattern — ",[33,42,43],{},"new Worker(new URL('.\u002Fworker.js', import.meta.url), { type: 'module' })"," — and emit the worker as its own hashed chunk with its own dependency graph. Getting the details right decides whether the worker chunk is 6KB or 300KB, whether it is cached across deploys, and whether it shares code with the main bundle or duplicates it.",[15,46,47],{},[48,49,55,56,55,63,55,67,55,70,55,88,55,96,55,102,55,110,55,117,55,122,55,126,55,129,55,133,55,136,55,139,55,143,55,146,55,149,55,152,55,156,55,159,55,162,55,165,55,169,55,172,55,181,55,185,55,189,55],"svg",{"viewBox":50,"width":51,"role":52,"ariaLabel":53,"style":54},"0 0 760 149","100%","img","Steps from the new URL worker pattern in source code to a separately hashed, cacheable worker chunk loaded at runtime.","height:auto;max-width:760px;display:block;margin:1.75rem auto;font-family:inherit;color:var(--fp-svg-ink)"," ",[57,58],"rect",{"className":59,"x":61,"y":61,"width":51,"height":51,"fill":62},[60],"svg-canvas","0","#ffffff",[64,65,66],"title",{},"How the bundler handles a worker reference",[68,69,53],"desc",{},[71,72,73],"defs",{},[74,75,82],"marker",{"id":76,"viewBox":77,"refX":78,"refY":79,"markerWidth":80,"markerHeight":80,"orient":81},"facc752ad8","0 0 10 10","9","5","7","auto-start-reverse",[83,84],"path",{"d":85,"fill":86,"style":87},"M0 0 L10 5 L0 10 z","currentColor","fill-opacity:0.7",[57,89],{"x":90,"y":90,"width":91,"height":92,"rx":93,"fill":94,"stroke":86,"style":95},"1","758","147","10","none","stroke-opacity:0.18",[97,98,66],"text",{"x":99,"y":100,"fill":86,"style":101},"28.0","34.0","font-size:16px;font-weight:700",[57,103],{"x":99,"y":104,"width":105,"height":106,"rx":107,"fill":108,"stroke":108,"style":109},"56.0","108.8","71.0","6","#0466c8","fill-opacity:0.14;stroke-opacity:0.9",[97,111,116],{"x":112,"y":113,"fill":86,"style":114,"textAnchor":115},"82.4","78.0","font-size:13px;font-weight:700","middle","Source",[97,118,121],{"x":112,"y":119,"fill":86,"style":120,"textAnchor":115},"96.0","font-size:12px","new URL('.\u002Fw.js',",[97,123,125],{"x":112,"y":124,"fill":86,"style":120,"textAnchor":115},"112.0","import.meta.url)",[57,127],{"x":128,"y":104,"width":105,"height":106,"rx":107,"fill":108,"stroke":108,"style":109},"176.8",[97,130,132],{"x":131,"y":113,"fill":86,"style":114,"textAnchor":115},"231.2","Detect",[97,134,135],{"x":131,"y":119,"fill":86,"style":120,"textAnchor":115},"worker entry point",[57,137],{"x":138,"y":104,"width":105,"height":106,"rx":107,"fill":108,"stroke":108,"style":109},"325.6",[97,140,142],{"x":141,"y":113,"fill":86,"style":114,"textAnchor":115},"380.0","Bundle",[97,144,145],{"x":141,"y":119,"fill":86,"style":120,"textAnchor":115},"own dependency",[97,147,148],{"x":141,"y":124,"fill":86,"style":120,"textAnchor":115},"graph",[57,150],{"x":151,"y":104,"width":105,"height":106,"rx":107,"fill":108,"stroke":108,"style":109},"474.4",[97,153,155],{"x":154,"y":113,"fill":86,"style":114,"textAnchor":115},"528.8","Emit",[97,157,158],{"x":154,"y":119,"fill":86,"style":120,"textAnchor":115},"w-3fa9c1.js",[97,160,161],{"x":154,"y":124,"fill":86,"style":120,"textAnchor":115},"(hashed)",[57,163],{"x":164,"y":104,"width":105,"height":106,"rx":107,"fill":108,"stroke":108,"style":109},"623.2",[97,166,168],{"x":167,"y":113,"fill":86,"style":114,"textAnchor":115},"677.6","Runtime",[97,170,171],{"x":167,"y":119,"fill":86,"style":120,"textAnchor":115},"new Worker(url)",[173,174],"line",{"x1":175,"y1":176,"x2":177,"y2":176,"stroke":86,"strokeWidth":178,"style":179,"markerEnd":180},"136.8","91.5","174.8","1.5","stroke-opacity:0.6","url(#facc752ad8)",[173,182],{"x1":183,"y1":176,"x2":184,"y2":176,"stroke":86,"strokeWidth":178,"style":179,"markerEnd":180},"285.6","323.6",[173,186],{"x1":187,"y1":176,"x2":188,"y2":176,"stroke":86,"strokeWidth":178,"style":179,"markerEnd":180},"434.4","472.4",[173,190],{"x1":191,"y1":176,"x2":192,"y2":176,"stroke":86,"strokeWidth":178,"style":179,"markerEnd":180},"583.2","621.2",[194,195,197],"h2",{"id":196},"rapid-diagnosis","Rapid Diagnosis",[199,200,201,209,215,221,231],"ul",{},[202,203,204,208],"li",{},[205,206,207],"strong",{},"Inspect the build output."," List emitted chunks; the worker should be its own file with a content hash. If it is missing, it was probably inlined as a blob or not bundled at all.",[202,210,211,214],{},[205,212,213],{},"Check the worker chunk size."," Open it in a bundle analyser. Large frameworks or UI libraries inside a worker chunk mean an import pulled in application code.",[202,216,217,220],{},[205,218,219],{},"Check for duplication."," If a large library (a parser, a search engine) appears in both the main and worker chunks, the user downloads it twice.",[202,222,223,226,227,230],{},[205,224,225],{},"Check headers."," The worker chunk should be served with the same long-lived ",[33,228,229],{},"Cache-Control: immutable"," policy as other hashed assets.",[202,232,233,236],{},[205,234,235],{},"Check start timing."," In the Network panel, note when the worker script is requested relative to LCP and first interaction.",[194,238,240],{"id":239},"root-cause-analysis","Root Cause Analysis",[15,242,243,246,247,250],{},[205,244,245],{},"1. Non-standard worker creation."," Building a worker from a string or a URL the bundler cannot analyse (",[33,248,249],{},"new Worker('\u002Fworkers\u002F' + name + '.js')",") bypasses bundling: no hashing, no tree-shaking, and often no transpilation.",[15,252,253,256,257,260],{},[205,254,255],{},"2. Shared modules importing heavy code."," The worker imports a ",[33,258,259],{},"utils"," module that imports the store, which imports the UI library. The worker bundle inherits the whole chain.",[15,262,263,266],{},[205,264,265],{},"3. Inlined workers."," Plugins that inline workers as base64 or blob URLs bloat the main bundle and defeat separate caching.",[15,268,269,55,272,275,276,279,280,283],{},[205,270,271],{},"4. Classic workers in a module codebase.",[33,273,274],{},"type: 'classic'"," workers cannot use ",[33,277,278],{},"import","; teams work around it with ",[33,281,282],{},"importScripts"," of unbundled files.",[15,285,286],{},[48,287,55,290,55,293,55,296,55,298,55,301,55,303,55,310,55,317,55,322,55,326,55,330,55,334,55,338,55,342,55],{"viewBox":288,"width":51,"role":52,"ariaLabel":289,"style":54},"0 0 760 163","Bar chart of the worker chunk size when it imports a shared barrel file, when it imports specific modules, and when dependencies are split into a shared chunk.",[57,291],{"className":292,"x":61,"y":61,"width":51,"height":51,"fill":62},[60],[64,294,295],{},"Worker chunk size by import hygiene",[68,297,289],{},[57,299],{"x":90,"y":90,"width":91,"height":300,"rx":93,"fill":94,"stroke":86,"style":95},"161",[97,302,295],{"x":99,"y":100,"fill":86,"style":101},[97,304,309],{"x":305,"y":306,"fill":86,"style":307,"textAnchor":308},"204.5","70.0","font-size:13px","end","Imports app barrel (utils\u002Findex)",[57,311],{"x":312,"y":104,"width":313,"height":314,"rx":315,"fill":108,"stroke":108,"style":316},"216.5","451.5","19","3","fill-opacity:0.55;stroke-opacity:0.9",[97,318,321],{"x":319,"y":306,"fill":86,"style":320},"674.0","font-size:12px;font-weight:600","312KB",[97,323,325],{"x":305,"y":324,"fill":86,"style":307,"textAnchor":308},"101.0","Imports specific modules only",[57,327],{"x":312,"y":328,"width":329,"height":314,"rx":315,"fill":108,"stroke":108,"style":316},"87.0","69.5",[97,331,333],{"x":332,"y":324,"fill":86,"style":320},"292.0","48KB",[97,335,337],{"x":305,"y":336,"fill":86,"style":307,"textAnchor":308},"132.0","+ parser in shared chunk",[57,339],{"x":312,"y":340,"width":341,"height":314,"rx":315,"fill":108,"stroke":108,"style":316},"118.0","13.0",[97,343,345],{"x":344,"y":336,"fill":86,"style":320},"235.5","9KB",[194,347,349],{"id":348},"step-by-step-resolution","Step-by-Step Resolution",[351,352,354],"h3",{"id":353},"_1-use-the-standard-worker-pattern","1. Use the standard worker pattern",[356,357,362],"pre",{"className":358,"code":359,"language":360,"meta":361,"style":361},"language-javascript shiki shiki-themes github-light-high-contrast github-dark-high-contrast github-light-high-contrast","\u002F\u002F Works in Vite, webpack 5, Rollup (with plugin), esbuild-based tools and Parcel.\nconst worker = new Worker(new URL('.\u002Fsearch-worker.js', import.meta.url), {\n  type: 'module',\n  name: 'search',          \u002F\u002F shows as the thread name in DevTools\n});\n\u002F\u002F trade-off: module workers are supported in all current browsers, but very old\n\u002F\u002F Safari versions lacked them. If you must support those, configure the bundler\n\u002F\u002F to emit a classic worker format (Vite: worker.format = 'iife').\n","javascript","",[33,363,364,372,423,435,450,456,462,468],{"__ignoreMap":361},[365,366,368],"span",{"class":173,"line":367},1,[365,369,371],{"class":370},"sjfSM","\u002F\u002F Works in Vite, webpack 5, Rollup (with plugin), esbuild-based tools and Parcel.\n",[365,373,375,379,383,386,389,393,397,400,403,405,409,412,414,417,420],{"class":173,"line":374},2,[365,376,378],{"class":377},"sPARh","const",[365,380,382],{"class":381},"sPXB4"," worker",[365,384,385],{"class":377}," =",[365,387,388],{"class":377}," new",[365,390,392],{"class":391},"smZ65"," Worker",[365,394,396],{"class":395},"saISM","(",[365,398,399],{"class":377},"new",[365,401,402],{"class":391}," URL",[365,404,396],{"class":395},[365,406,408],{"class":407},"sZ8jY","'.\u002Fsearch-worker.js'",[365,410,411],{"class":395},", ",[365,413,278],{"class":377},[365,415,416],{"class":395},".",[365,418,419],{"class":381},"meta",[365,421,422],{"class":395},".url), {\n",[365,424,426,429,432],{"class":173,"line":425},3,[365,427,428],{"class":395},"  type: ",[365,430,431],{"class":407},"'module'",[365,433,434],{"class":395},",\n",[365,436,438,441,444,447],{"class":173,"line":437},4,[365,439,440],{"class":395},"  name: ",[365,442,443],{"class":407},"'search'",[365,445,446],{"class":395},",          ",[365,448,449],{"class":370},"\u002F\u002F shows as the thread name in DevTools\n",[365,451,453],{"class":173,"line":452},5,[365,454,455],{"class":395},"});\n",[365,457,459],{"class":173,"line":458},6,[365,460,461],{"class":370},"\u002F\u002F trade-off: module workers are supported in all current browsers, but very old\n",[365,463,465],{"class":173,"line":464},7,[365,466,467],{"class":370},"\u002F\u002F Safari versions lacked them. If you must support those, configure the bundler\n",[365,469,471],{"class":173,"line":470},8,[365,472,473],{"class":370},"\u002F\u002F to emit a classic worker format (Vite: worker.format = 'iife').\n",[15,475,476,477,480],{},"Expected outcome: the bundler emits ",[33,478,479],{},"search-worker-[hash].js",", cached immutably and fetched only when the worker is created.",[351,482,484],{"id":483},"_2-configure-worker-output-explicitly","2. Configure worker output explicitly",[356,486,488],{"className":358,"code":487,"language":360,"meta":361,"style":361},"\u002F\u002F vite.config.js\nexport default {\n  worker: {\n    format: 'es',                                   \u002F\u002F module worker output\n    rollupOptions: { output: { entryFileNames: 'workers\u002F[name]-[hash].js' } },\n  },\n};\n\u002F\u002F webpack.config.js (webpack 5 detects the pattern natively)\nmodule.exports = {\n  output: { chunkFilename: '[name].[contenthash].js' },\n  optimization: { splitChunks: { chunks: 'all' } },  \u002F\u002F allows sharing with workers\n};\n\u002F\u002F trade-off: splitChunks 'all' can create many small shared chunks that the\n\u002F\u002F worker must fetch before starting. Check the request count for the worker's\n\u002F\u002F first load and set minSize if it fragments too much.\n",[33,489,490,495,507,512,526,537,544,552,557,572,584,599,605,611,617],{"__ignoreMap":361},[365,491,492],{"class":173,"line":367},[365,493,494],{"class":370},"\u002F\u002F vite.config.js\n",[365,496,497,500,503],{"class":173,"line":374},[365,498,499],{"class":377},"export",[365,501,502],{"class":377}," default",[365,504,506],{"class":505},"sQw3B"," {\n",[365,508,509],{"class":173,"line":425},[365,510,511],{"class":395},"  worker: {\n",[365,513,514,517,520,523],{"class":173,"line":437},[365,515,516],{"class":395},"    format: ",[365,518,519],{"class":407},"'es'",[365,521,522],{"class":395},",                                   ",[365,524,525],{"class":370},"\u002F\u002F module worker output\n",[365,527,528,531,534],{"class":173,"line":452},[365,529,530],{"class":395},"    rollupOptions: { output: { entryFileNames: ",[365,532,533],{"class":407},"'workers\u002F[name]-[hash].js'",[365,535,536],{"class":395}," } },\n",[365,538,539,542],{"class":173,"line":458},[365,540,541],{"class":395},"  }",[365,543,434],{"class":505},[365,545,546,549],{"class":173,"line":464},[365,547,548],{"class":505},"}",[365,550,551],{"class":395},";\n",[365,553,554],{"class":173,"line":470},[365,555,556],{"class":370},"\u002F\u002F webpack.config.js (webpack 5 detects the pattern natively)\n",[365,558,560,563,565,568,570],{"class":173,"line":559},9,[365,561,562],{"class":381},"module",[365,564,416],{"class":395},[365,566,567],{"class":381},"exports",[365,569,385],{"class":377},[365,571,506],{"class":395},[365,573,575,578,581],{"class":173,"line":574},10,[365,576,577],{"class":395},"  output: { chunkFilename: ",[365,579,580],{"class":407},"'[name].[contenthash].js'",[365,582,583],{"class":395}," },\n",[365,585,587,590,593,596],{"class":173,"line":586},11,[365,588,589],{"class":395},"  optimization: { splitChunks: { chunks: ",[365,591,592],{"class":407},"'all'",[365,594,595],{"class":395}," } },  ",[365,597,598],{"class":370},"\u002F\u002F allows sharing with workers\n",[365,600,602],{"class":173,"line":601},12,[365,603,604],{"class":395},"};\n",[365,606,608],{"class":173,"line":607},13,[365,609,610],{"class":370},"\u002F\u002F trade-off: splitChunks 'all' can create many small shared chunks that the\n",[365,612,614],{"class":173,"line":613},14,[365,615,616],{"class":370},"\u002F\u002F worker must fetch before starting. Check the request count for the worker's\n",[365,618,620],{"class":173,"line":619},15,[365,621,622],{"class":370},"\u002F\u002F first load and set minSize if it fragments too much.\n",[15,624,625],{},"Expected outcome: predictable, hashed worker files in a known location.",[351,627,629],{"id":628},"_3-keep-the-workers-import-graph-lean","3. Keep the worker's import graph lean",[15,631,632],{},"Import specific modules rather than barrels, and keep anything DOM- or UI-related out of code the worker imports. A lint rule can enforce the boundary.",[356,634,636],{"className":358,"code":635,"language":360,"meta":361,"style":361},"\u002F\u002F .eslintrc.cjs — forbid UI imports in worker code.\nmodule.exports = { overrides: [{\n  files: ['src\u002Fworkers\u002F**'],\n  rules: { 'no-restricted-imports': ['error', { patterns: ['react*', 'vue', '@\u002Fcomponents\u002F*', '@\u002Fstore\u002F*'] }] },\n}] };\n\u002F\u002F trade-off: lint rules catch direct imports only. A worker importing a module\n\u002F\u002F that transitively imports React is still possible; check the bundle analyser\n\u002F\u002F output in CI for framework code in worker chunks.\n",[33,637,638,643,656,667,705,710,715,720],{"__ignoreMap":361},[365,639,640],{"class":173,"line":367},[365,641,642],{"class":370},"\u002F\u002F .eslintrc.cjs — forbid UI imports in worker code.\n",[365,644,645,647,649,651,653],{"class":173,"line":374},[365,646,562],{"class":381},[365,648,416],{"class":395},[365,650,567],{"class":381},[365,652,385],{"class":377},[365,654,655],{"class":395}," { overrides: [{\n",[365,657,658,661,664],{"class":173,"line":425},[365,659,660],{"class":395},"  files: [",[365,662,663],{"class":407},"'src\u002Fworkers\u002F**'",[365,665,666],{"class":395},"],\n",[365,668,669,672,675,678,681,684,687,689,692,694,697,699,702],{"class":173,"line":437},[365,670,671],{"class":395},"  rules: { ",[365,673,674],{"class":407},"'no-restricted-imports'",[365,676,677],{"class":395},": [",[365,679,680],{"class":407},"'error'",[365,682,683],{"class":395},", { patterns: [",[365,685,686],{"class":407},"'react*'",[365,688,411],{"class":395},[365,690,691],{"class":407},"'vue'",[365,693,411],{"class":395},[365,695,696],{"class":407},"'@\u002Fcomponents\u002F*'",[365,698,411],{"class":395},[365,700,701],{"class":407},"'@\u002Fstore\u002F*'",[365,703,704],{"class":395},"] }] },\n",[365,706,707],{"class":173,"line":452},[365,708,709],{"class":395},"}] };\n",[365,711,712],{"class":173,"line":458},[365,713,714],{"class":370},"\u002F\u002F trade-off: lint rules catch direct imports only. A worker importing a module\n",[365,716,717],{"class":173,"line":464},[365,718,719],{"class":370},"\u002F\u002F that transitively imports React is still possible; check the bundle analyser\n",[365,721,722],{"class":173,"line":470},[365,723,724],{"class":370},"\u002F\u002F output in CI for framework code in worker chunks.\n",[15,726,727,728,732],{},"Expected outcome: worker chunks contain only the computation they exist for — typically tens of kilobytes. ",[19,729,731],{"href":730},"\u002Fjavascript-bundle-optimization-code-splitting\u002Ftree-shaking-and-dead-code-elimination\u002Fwhy-barrel-files-break-tree-shaking\u002F","Why barrel files break tree shaking"," explains the underlying problem.",[351,734,736],{"id":735},"_4-start-the-worker-at-the-right-moment","4. Start the worker at the right moment",[15,738,739],{},"Create the worker after LCP and before the first interaction that needs it — on idle, on hover or focus of the triggering control, or after load.",[356,741,743],{"className":358,"code":742,"language":360,"meta":361,"style":361},"let workerPromise;\nexport function getWorker() {\n  workerPromise ??= Promise.resolve(new Worker(new URL('.\u002Fsearch-worker.js', import.meta.url), { type: 'module' }));\n  return workerPromise;\n}\naddEventListener('load', () => requestIdleCallback(() => getWorker()));\nsearchInput.addEventListener('focus', () => getWorker(), { once: true });\n\u002F\u002F trade-off: creating on idle wastes a download for users who never search.\n\u002F\u002F If search usage is rare, create on focus only and accept a short delay on\n\u002F\u002F the first query.\n",[33,744,745,753,766,814,821,826,855,882,887,892],{"__ignoreMap":361},[365,746,747,750],{"class":173,"line":367},[365,748,749],{"class":377},"let",[365,751,752],{"class":395}," workerPromise;\n",[365,754,755,757,760,763],{"class":173,"line":374},[365,756,499],{"class":377},[365,758,759],{"class":377}," function",[365,761,762],{"class":391}," getWorker",[365,764,765],{"class":395},"() {\n",[365,767,768,771,774,777,779,782,784,786,788,790,792,794,796,798,800,802,804,806,809,811],{"class":173,"line":425},[365,769,770],{"class":395},"  workerPromise ",[365,772,773],{"class":377},"??=",[365,775,776],{"class":381}," Promise",[365,778,416],{"class":395},[365,780,781],{"class":391},"resolve",[365,783,396],{"class":395},[365,785,399],{"class":377},[365,787,392],{"class":391},[365,789,396],{"class":395},[365,791,399],{"class":377},[365,793,402],{"class":391},[365,795,396],{"class":395},[365,797,408],{"class":407},[365,799,411],{"class":395},[365,801,278],{"class":377},[365,803,416],{"class":395},[365,805,419],{"class":381},[365,807,808],{"class":395},".url), { type: ",[365,810,431],{"class":407},[365,812,813],{"class":395}," }));\n",[365,815,816,819],{"class":173,"line":437},[365,817,818],{"class":377},"  return",[365,820,752],{"class":395},[365,822,823],{"class":173,"line":452},[365,824,825],{"class":395},"}\n",[365,827,828,831,833,836,839,842,845,848,850,852],{"class":173,"line":458},[365,829,830],{"class":391},"addEventListener",[365,832,396],{"class":395},[365,834,835],{"class":407},"'load'",[365,837,838],{"class":395},", () ",[365,840,841],{"class":377},"=>",[365,843,844],{"class":391}," requestIdleCallback",[365,846,847],{"class":395},"(() ",[365,849,841],{"class":377},[365,851,762],{"class":391},[365,853,854],{"class":395},"()));\n",[365,856,857,860,862,864,867,869,871,873,876,879],{"class":173,"line":464},[365,858,859],{"class":395},"searchInput.",[365,861,830],{"class":391},[365,863,396],{"class":395},[365,865,866],{"class":407},"'focus'",[365,868,838],{"class":395},[365,870,841],{"class":377},[365,872,762],{"class":391},[365,874,875],{"class":395},"(), { once: ",[365,877,878],{"class":381},"true",[365,880,881],{"class":395}," });\n",[365,883,884],{"class":173,"line":470},[365,885,886],{"class":370},"\u002F\u002F trade-off: creating on idle wastes a download for users who never search.\n",[365,888,889],{"class":173,"line":559},[365,890,891],{"class":370},"\u002F\u002F If search usage is rare, create on focus only and accept a short delay on\n",[365,893,894],{"class":173,"line":574},[365,895,896],{"class":370},"\u002F\u002F the first query.\n",[15,898,899],{},"Expected outcome: the worker is warm when needed, without competing with LCP resources.",[15,901,902],{},[48,903,55,906,55,909,55,912,55,914,55,917,55,919,55,924,55,931,55,935,55,939,55,942,55,946,55,949,55,953,55,957,55,961,55,963,55,966,55,968,55,970,55,972,55,974,55,977,55,981,55,983,55,985,55,987,55,989,55,991,55,994,55,997,55,1001,55,1006,55,1009,55,1011,55,1013,55,1015,55,1017,55,1020,55,1024,55,1026,55,1029,55,1031,55,1033,55,1035,55],{"viewBox":904,"width":51,"role":52,"ariaLabel":905,"style":54},"0 0 760 228","Comparison of ways to create a Web Worker by whether the bundler can hash, tree-shake and cache the worker code.",[57,907],{"className":908,"x":61,"y":61,"width":51,"height":51,"fill":62},[60],[64,910,911],{},"Worker creation patterns compared",[68,913,905],{},[57,915],{"x":90,"y":90,"width":91,"height":916,"rx":93,"fill":94,"stroke":86,"style":95},"226",[97,918,911],{"x":99,"y":100,"fill":86,"style":101},[57,920],{"x":99,"y":104,"width":921,"height":922,"rx":61,"fill":86,"stroke":86,"style":923},"210.0","30.0","fill-opacity:0.06;stroke-opacity:0.4",[97,925,930],{"x":926,"y":927,"fill":86,"style":928,"textAnchor":929},"38.0","75.5","font-size:12.5px;font-weight:700","start","Pattern",[57,932],{"x":933,"y":104,"width":934,"height":922,"rx":61,"fill":86,"stroke":86,"style":923},"238.0","164.7",[97,936,938],{"x":937,"y":927,"fill":86,"style":928,"textAnchor":115},"320.3","Hashed + cached",[57,940],{"x":941,"y":104,"width":934,"height":922,"rx":61,"fill":86,"stroke":86,"style":923},"402.7",[97,943,945],{"x":944,"y":927,"fill":86,"style":928,"textAnchor":115},"485.0","Tree-shaken",[57,947],{"x":948,"y":104,"width":934,"height":922,"rx":61,"fill":86,"stroke":86,"style":923},"567.3",[97,950,952],{"x":951,"y":927,"fill":86,"style":928,"textAnchor":115},"649.7","Recommended",[57,954],{"x":99,"y":955,"width":921,"height":922,"rx":61,"fill":94,"stroke":86,"style":956},"86.0","stroke-opacity:0.35",[97,958,960],{"x":926,"y":959,"fill":86,"style":928,"textAnchor":929},"105.5","new URL('.\u002Fw.js', import.meta.url)",[57,962],{"x":933,"y":955,"width":934,"height":922,"rx":61,"fill":108,"stroke":108,"style":109},[97,964,965],{"x":937,"y":959,"fill":86,"style":120,"textAnchor":115},"yes",[57,967],{"x":941,"y":955,"width":934,"height":922,"rx":61,"fill":108,"stroke":108,"style":109},[97,969,965],{"x":944,"y":959,"fill":86,"style":120,"textAnchor":115},[57,971],{"x":948,"y":955,"width":934,"height":922,"rx":61,"fill":108,"stroke":108,"style":109},[97,973,965],{"x":951,"y":959,"fill":86,"style":120,"textAnchor":115},[57,975],{"x":99,"y":976,"width":921,"height":922,"rx":61,"fill":94,"stroke":86,"style":956},"116.0",[97,978,980],{"x":926,"y":979,"fill":86,"style":928,"textAnchor":929},"135.5","Vite ?worker import",[57,982],{"x":933,"y":976,"width":934,"height":922,"rx":61,"fill":108,"stroke":108,"style":109},[97,984,965],{"x":937,"y":979,"fill":86,"style":120,"textAnchor":115},[57,986],{"x":941,"y":976,"width":934,"height":922,"rx":61,"fill":108,"stroke":108,"style":109},[97,988,965],{"x":944,"y":979,"fill":86,"style":120,"textAnchor":115},[57,990],{"x":948,"y":976,"width":934,"height":922,"rx":61,"fill":86,"stroke":86,"style":923},[97,992,993],{"x":951,"y":979,"fill":86,"style":120,"textAnchor":115},"Vite only",[57,995],{"x":99,"y":996,"width":921,"height":922,"rx":61,"fill":94,"stroke":86,"style":956},"146.0",[97,998,1000],{"x":926,"y":999,"fill":86,"style":928,"textAnchor":929},"165.5","Blob URL from string",[57,1002],{"x":933,"y":996,"width":934,"height":922,"rx":61,"fill":1003,"stroke":1004,"style":1005},"#ffc300","#b8860b","fill-opacity:0.24;stroke-opacity:0.9",[97,1007,1008],{"x":937,"y":999,"fill":86,"style":120,"textAnchor":115},"no",[57,1010],{"x":941,"y":996,"width":934,"height":922,"rx":61,"fill":1003,"stroke":1004,"style":1005},[97,1012,1008],{"x":944,"y":999,"fill":86,"style":120,"textAnchor":115},[57,1014],{"x":948,"y":996,"width":934,"height":922,"rx":61,"fill":1003,"stroke":1004,"style":1005},[97,1016,1008],{"x":951,"y":999,"fill":86,"style":120,"textAnchor":115},[57,1018],{"x":99,"y":1019,"width":921,"height":922,"rx":61,"fill":94,"stroke":86,"style":956},"176.0",[97,1021,1023],{"x":926,"y":1022,"fill":86,"style":928,"textAnchor":929},"195.5","Hand-copied public\u002Fworker.js",[57,1025],{"x":933,"y":1019,"width":934,"height":922,"rx":61,"fill":1003,"stroke":1004,"style":1005},[97,1027,1028],{"x":937,"y":1022,"fill":86,"style":120,"textAnchor":115},"manual",[57,1030],{"x":941,"y":1019,"width":934,"height":922,"rx":61,"fill":1003,"stroke":1004,"style":1005},[97,1032,1008],{"x":944,"y":1022,"fill":86,"style":120,"textAnchor":115},[57,1034],{"x":948,"y":1019,"width":934,"height":922,"rx":61,"fill":1003,"stroke":1004,"style":1005},[97,1036,1008],{"x":951,"y":1022,"fill":86,"style":120,"textAnchor":115},[194,1038,1040],{"id":1039},"verification","Verification",[15,1042,1043,1044,1048],{},"Build and inspect: the worker chunk is separate, hashed, small, and free of framework code. In DevTools, the worker appears as a named thread in the Performance panel, and its script is fetched after LCP. Add a CI check on worker chunk size with ",[19,1045,1047],{"href":1046},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fjavascript-performance-budgets\u002Fenforcing-bundle-budgets-with-size-limit\u002F","size-limit"," so an innocent import does not quietly add 200KB.",[356,1050,1052],{"className":358,"code":1051,"language":360,"meta":361,"style":361},"\u002F\u002F .size-limit.cjs\nmodule.exports = [\n  { name: 'search worker', path: 'dist\u002Fworkers\u002Fsearch-worker-*.js', limit: '20 KB' },\n];\n\u002F\u002F trade-off: a byte limit catches accidental imports but not slow code; pair it\n\u002F\u002F with a timing check of the worker's startup in a throttled lab run.\n",[33,1053,1054,1059,1072,1094,1099,1104],{"__ignoreMap":361},[365,1055,1056],{"class":173,"line":367},[365,1057,1058],{"class":370},"\u002F\u002F .size-limit.cjs\n",[365,1060,1061,1063,1065,1067,1069],{"class":173,"line":374},[365,1062,562],{"class":381},[365,1064,416],{"class":395},[365,1066,567],{"class":381},[365,1068,385],{"class":377},[365,1070,1071],{"class":395}," [\n",[365,1073,1074,1077,1080,1083,1086,1089,1092],{"class":173,"line":425},[365,1075,1076],{"class":395},"  { name: ",[365,1078,1079],{"class":407},"'search worker'",[365,1081,1082],{"class":395},", path: ",[365,1084,1085],{"class":407},"'dist\u002Fworkers\u002Fsearch-worker-*.js'",[365,1087,1088],{"class":395},", limit: ",[365,1090,1091],{"class":407},"'20 KB'",[365,1093,583],{"class":395},[365,1095,1096],{"class":173,"line":437},[365,1097,1098],{"class":395},"];\n",[365,1100,1101],{"class":173,"line":452},[365,1102,1103],{"class":370},"\u002F\u002F trade-off: a byte limit catches accidental imports but not slow code; pair it\n",[365,1105,1106],{"class":173,"line":458},[365,1107,1108],{"class":370},"\u002F\u002F with a timing check of the worker's startup in a throttled lab run.\n",[194,1110,1112],{"id":1111},"deployment-pitfalls","Deployment Pitfalls",[15,1114,1115,1118,1119,1122,1123,1126,1127,1130],{},[205,1116,1117],{},"Content Security Policy."," A ",[33,1120,1121],{},"worker-src"," (or fallback ",[33,1124,1125],{},"script-src",") directive must allow the origin serving worker chunks. Blob-URL workers additionally need ",[33,1128,1129],{},"blob:"," in the policy, which weakens it — another reason to prefer bundled worker files.",[15,1132,1133,1136,1137,1139,1140,1143],{},[205,1134,1135],{},"Cross-origin CDNs."," Workers must be same-origin with the page. If your static assets live on a separate CDN hostname, ",[33,1138,171],{}," with a cross-origin URL fails. Serve worker chunks from the page's origin (a CDN path rule) or bootstrap with a small same-origin script that ",[33,1141,1142],{},"import()","s the cross-origin module, which requires CORS headers.",[15,1145,1146,1149],{},[205,1147,1148],{},"Stale workers after deploy."," A long-lived tab holds a worker from the previous release; if the main thread updates (via a service worker or a soft reload of modules) and the protocol between them changed, messages break. Version your worker message protocol and recreate the worker when versions differ.",[194,1151,1153],{"id":1152},"testing-workers-in-ci","Testing Workers in CI",[15,1155,1156,1157,1160],{},"Unit tests for worker logic are easiest when the computation lives in plain modules the worker imports: test those modules directly in Node, and keep the worker file itself a thin ",[33,1158,1159],{},"Comlink.expose"," wrapper. For integration, run a headless browser test that creates the real bundled worker, sends a representative request and asserts the response and its timing. This catches the bundling-specific failures — wrong output format, CSP blocking the worker URL, a missing chunk — that unit tests never exercise.",[194,1162,1164],{"id":1163},"faq","FAQ",[1166,1167,1170,1174],"details",{"className":1168},[1169],"faq-item",[1171,1172,1173],"summary",{},"Does Vite's ?worker import suffix still matter?",[15,1175,1176,1179,1180,1183,1184,1187],{},[33,1177,1178],{},"import MyWorker from '.\u002Fw.js?worker'"," is a Vite-specific shortcut that produces a constructor. It works, but the ",[33,1181,1182],{},"new URL(..., import.meta.url)"," pattern is portable across bundlers and is what Vite recommends for most cases. Use ",[33,1185,1186],{},"?worker&inline"," only when you genuinely need a single-file build.",[1166,1189,1191,1194],{"className":1190},[1169],[1171,1192,1193],{},"Can a worker share a chunk with the main thread?",[15,1195,1196],{},"Yes, with module workers both can import the same hashed chunk, and the browser's HTTP cache serves it to the second requester. They still each evaluate it separately — there is no shared module instance across threads — so sharing saves bytes, not CPU.",[1166,1198,1200,1203],{"className":1199},[1169],[1171,1201,1202],{},"How do I debug a bundled worker?",[15,1204,1205,1206,1209],{},"Source maps work for workers when the bundler emits them; in DevTools, worker sources appear under their own thread in the Sources panel. Naming the worker via the ",[33,1207,1208],{},"name"," option makes the thread easy to find in both Sources and Performance.",[194,1211,1213],{"id":1212},"related","Related",[199,1215,1216,1223,1230],{},[202,1217,1218,1222],{},[19,1219,1221],{"href":1220},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002F","Comlink vs raw postMessage for workers"," — the messaging layer on top of the bundled worker.",[202,1224,1225,1229],{},[19,1226,1228],{"href":1227},"\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 worker built with this setup.",[202,1231,1232,1236],{},[19,1233,1235],{"href":1234},"\u002Fjavascript-bundle-optimization-code-splitting\u002Fvite-and-rollup-build-optimization\u002Fconfiguring-manualchunks-in-vite\u002F","Configuring manualChunks in Vite"," — controlling how shared code is split.",[1238,1239,1241],"script",{"type":1240},"application\u002Fld+json","\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"HowTo\",\n  \"name\": \"How to Bundle Web Workers with Vite and Webpack\",\n  \"description\": \"The bundler-native way to build workers, how to keep their dependencies small and shared, and how to load them without hurting startup.\",\n  \"step\": [\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 1,\n      \"name\": \"Use the standard worker pattern\",\n      \"text\": \"Expected outcome: the bundler emits search-worker-[hash].js, cached immutably and fetched only when the worker is created.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 2,\n      \"name\": \"Configure worker output explicitly\",\n      \"text\": \"Expected outcome: predictable, hashed worker files in a known location.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 3,\n      \"name\": \"Keep the worker's import graph lean\",\n      \"text\": \"Import specific modules rather than barrels, and keep anything DOM- or UI-related out of code the worker imports.\"\n    },\n    {\n      \"@type\": \"HowToStep\",\n      \"position\": 4,\n      \"name\": \"Start the worker at the right moment\",\n      \"text\": \"Create the worker after LCP and before the first interaction that needs it — on idle, on hover or focus of the triggering control, or after load.\"\n    }\n  ]\n}\n",[1238,1243,1244],{"type":1240},"\n{\n  \"@context\": \"https:\u002F\u002Fschema.org\",\n  \"@type\": \"TechArticle\",\n  \"headline\": \"How to Bundle Web Workers with Vite and Webpack\",\n  \"description\": \"The bundler-native way to build workers, how to keep their dependencies small and shared, and how to load them without hurting startup.\",\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\u002Fbundling-web-workers-with-vite-and-webpack\u002F\"\n  }\n}\n",[1238,1246,1247],{"type":1240},"\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\": \"Bundling Web Workers with Vite and Webpack\",\n      \"item\": \"https:\u002F\u002Ffrontend-performance.com\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fbundling-web-workers-with-vite-and-webpack\u002F\"\n    }\n  ]\n}\n",[1249,1250,1251],"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 .smZ65, html code.shiki .smZ65{--shiki-default:#622CBC;--shiki-dark:#DBB7FF;--shiki-light:#622CBC}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 .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html pre.shiki code .sQw3B, html code.shiki .sQw3B{--shiki-default:#702C00;--shiki-dark:#FFB757;--shiki-light:#702C00}",{"title":361,"searchDepth":374,"depth":374,"links":1253},[1254,1255,1256,1262,1263,1264,1265,1266],{"id":196,"depth":374,"text":197},{"id":239,"depth":374,"text":240},{"id":348,"depth":374,"text":349,"children":1257},[1258,1259,1260,1261],{"id":353,"depth":425,"text":354},{"id":483,"depth":425,"text":484},{"id":628,"depth":425,"text":629},{"id":735,"depth":425,"text":736},{"id":1039,"depth":374,"text":1040},{"id":1111,"depth":374,"text":1112},{"id":1152,"depth":374,"text":1153},{"id":1163,"depth":374,"text":1164},{"id":1212,"depth":374,"text":1213},"The bundler-native way to build workers, how to keep their dependencies small and shared, and how to load them without hurting startup.","md",{"slug":1270,"type":1271,"breadcrumb":1272,"datePublished":1280,"dateModified":1280},"bundling-web-workers-with-vite-and-webpack","article",[1273,1276,1277,1278],{"name":1274,"url":1275},"Home","\u002F",{"name":27,"url":26},{"name":22,"url":21},{"name":5,"url":1279},"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fbundling-web-workers-with-vite-and-webpack\u002F","2026-10-06",true,"\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fbundling-web-workers-with-vite-and-webpack",{"title":1284,"description":1285},"Bundling Web Workers with Vite and Webpack 5","Ship Web Workers as hashed module chunks with new URL(import.meta.url) in Vite and webpack 5, avoid duplicated dependencies and start workers at the right time.","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fbundling-web-workers-with-vite-and-webpack\u002Findex","W94F1fkWtZT6foRvfXZ0dRDV7DJi_ZxvZw_xhm6LqRI",[1289,1292],{"title":22,"path":1290,"stem":1291,"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":1293,"path":1294,"stem":1295,"children":-1},"Comlink vs Raw postMessage for Workers","\u002Fcore-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers","core-web-vitals-measurement\u002Foffloading-work-to-web-workers-with-comlink\u002Fcomlink-vs-raw-postmessage-for-workers\u002Findex",1791308073855]