docs/web-workers.md
FuseWorker distributes search across multiple Web Workers, searching dataset shards in parallel. The UI stays completely responsive — no jank, no frozen frames — even on 100K+ item datasets.
Click Search on each side and watch the balls. The FPS counter tells the story.
<WorkerDemo /><sub>Dataset: randomly generated people with name, email, company, and description fields — searched across all 4 keys.</sub>
The standard Fuse class searches synchronously on the main thread. For most datasets (hundreds to a few thousand items), this is fast enough. But with 10K+ items, search can take hundreds of milliseconds or more, visibly freezing the UI.
FuseWorker is designed for these cases:
If your dataset is small enough that Fuse feels instant, stick with Fuse. Simpler, synchronous, no cleanup needed.
You can — it's not too complicated. But FuseWorker handles the parts that are tedious to get right: splitting the dataset into shards, spawning and managing worker lifecycle, dispatching queries to all workers in parallel, and merging results back in sorted order. It's boilerplate you'd write every time, bundled into a drop-in class with the same API you already know.
import { FuseWorker } from 'fuse.js/worker'
const fuse = new FuseWorker(docs, {
keys: ['title', 'author', 'description'],
threshold: 0.4,
includeScore: true
})
const results = await fuse.search('javascript')
// Clean up when done
fuse.terminate()
Almost the same API as Fuse, with a few caveats covered in Differences from Fuse below — the headline one is that search() returns a Promise.
search() call.Fuse instance with its shard.Main Thread Worker Threads
─────────── ──────────────
FuseWorker.search('query')
├── postMessage ──────────────────▶ Worker 1: Fuse.search('query') on shard 1
├── postMessage ──────────────────▶ Worker 2: Fuse.search('query') on shard 2
├── postMessage ──────────────────▶ Worker 3: Fuse.search('query') on shard 3
└── postMessage ──────────────────▶ Worker 4: Fuse.search('query') on shard 4
│
merge + sort ◀────────────────────────┘
const fuse = new FuseWorker(docs, options?, workerOptions?)
docs — Array of documents to search (same as Fuse)options — Fuse.js options (same as Fuse: keys, threshold, includeScore, etc.)workerOptions — Worker-specific options:| Option | Type | Default | Description |
|---|---|---|---|
numWorkers | number | navigator.hardwareConcurrency (max 8) | Number of parallel workers |
workerUrl | string | URL | Auto-resolved | Custom path to the worker script |
search(query, options?)const results = await fuse.search('query')
const limited = await fuse.search('query', { limit: 10 })
Returns Promise<FuseResult[]>. Supports the same query types as Fuse: strings, and expression objects (logical search).
add(doc)await fuse.add({ title: 'New Item', author: 'Jane' })
Adds a document to one of the worker shards (round-robin distribution).
setCollection(docs)await fuse.setCollection(newDocs)
Replaces the entire dataset. Redistributes across workers and rebuilds indexes. Use this instead of remove() — filter your array, then call setCollection.
terminate()fuse.terminate()
Terminates all workers and cleans up resources. Always call this when you're done searching.
Benchmarked in Chrome on 100,000 documents with 4 searchable keys:
| Method | Time | Speedup |
|---|---|---|
| Fuse (single thread) | 6,276ms | 1.00x |
| FuseWorker (2 workers) | 3,514ms | 1.79x |
| FuseWorker (4 workers) | 1,909ms | 3.29x |
| FuseWorker (8 workers) | 1,338ms | 4.69x |
Scaling is near-linear. The sweet spot is typically 4 workers — beyond that, the marginal gains diminish and memory usage increases (each worker holds a copy of its shard).
Want to run more detailed benchmarks? See bench/parallel-browser in the repo.
Workers add overhead from data serialization (postMessage) and worker startup. For small datasets, this overhead outweighs the parallelism benefit. As a rule of thumb:
Fuse. Workers add latency.FuseWorker is faster and keeps the UI responsive.| Fuse | FuseWorker | |
|---|---|---|
search() return type | FuseResult[] | Promise<FuseResult[]> |
| UI blocking | Yes | No |
remove(predicate) | Supported | Use setCollection() instead |
getIndex() | Supported | Not available |
| Result object references | Same as input docs | Copies (structured clone) |
Custom sortFn | Supported | Throws at construction |
Custom getFn (top-level) | Supported | Throws at construction |
Custom keys[].getFn | Supported | Throws at construction |
| Cleanup required | No | Call terminate() |
FuseWorker rejects function-valued options at construction time. Functions can't be transferred to a worker via postMessage (they aren't structured-cloneable), so FuseWorker throws an explicit error instead of failing later with an opaque DataCloneError.
The unsupported options are:
sortFn — FuseWorker always sorts results by Fuse's default (score, refIndex) tie-break. If you need a custom sort, run Fuse on the main thread or sort the returned array yourself.getFn (top-level) — Fall back to dotted key paths ('a.b.c') or array paths (['a', 'b', 'c']).keys[].getFn — Same as above. Use a string or array path on the key.useTokenSearch — Token search depends on corpus-level statistics (df, fieldCount) that would be computed independently inside each worker shard, producing scores that don't match single-thread Fuse. Use Fuse directly on the main thread for token search.Default ordering is preserved: FuseWorker returns the same order as Fuse for the same inputs (with or without includeScore), and shouldSort: false returns results in global collection order.
FuseWorker needs to load a separate script file inside each Web Worker. By default, it finds this file automatically — you don't need to do anything.
Some bundlers (e.g., certain Webpack configurations) can't resolve the worker script path automatically. If you see errors about the worker failing to load, you can provide the path explicitly:
import workerUrl from 'fuse.js/worker-script'
const fuse = new FuseWorker(docs, options, { workerUrl })
If that doesn't work with your bundler either, copy dist/fuse.worker.mjs to your public/static directory and pass the URL directly:
const fuse = new FuseWorker(docs, options, {
workerUrl: '/static/fuse.worker.mjs'
})
Web Workers are supported in all modern browsers (Chrome, Firefox, Safari, Edge). FuseWorker will not work in:
FuseWorker uses the browser's Web Worker API, not Node's worker_threads. Use Fuse on the server.new Worker(). Check your CSP headers if workers fail to spawn.