Client-Side Search & Database Options for Static Sites β Master Report
1. Executive summary
A static site has no server to run queries, so every option either ships an index or database to the browser and queries it in memory, or queries a file on the static host in small pieces (a chunked index, or HTTP Range requests into a single file).
| If you need⦠| Use | One-line reason |
|---|---|---|
| A search box over the site's pages | Pagefind | Works after any generator, chunked index, ~100β300 kB per search even on 10k-page sites |
| Search over a structured JSON dataset (< ~5k records) | MiniSearch | ~6 kB, real ranking, prefix + fuzzy + autosuggest, prebuilt index |
| Typo-tolerant lookup over short strings (command palette, titles) | Fuse.js | Best fuzzy matching; but it scans linearly, so it isn't a full-text engine |
| Raw speed / very large in-memory index / IndexedDB caching | FlexSearch 0.8 | Fastest of the group, persistent indexes |
| Facets, filters, typo tolerance, or vector/hybrid search | Orama | Has the most built in (~26 kB gz) |
| Real SQL over a large read-only dataset | DuckDB-WASM with remote Parquet | Fetches only the columns and row groups a query needs |
| Real SQL (joins, sorts, FTS5) over a small-to-medium dataset | Official SQLite WASM | Full SQLite; the whole .sqlite file downloads once |
| Local-first app that writes and persists data | Official SQLite WASM + OPFS (or PGlite if you need Postgres) | Durable, transactional storage in the browser |
| Analytics and dashboards over Parquet/CSV | DuckDB-WASM (heavy) or hyparquet (light, read-only) | Columnar Range-request reads |
Combining tools is normal. A common pattern is Pagefind for site-wide search plus MiniSearch or SQLite for one data-heavy page.
2. Architectural families
| Family | How it works | Examples | How download scales |
|---|---|---|---|
| A. Chunked static index | A post-build crawler writes an index split into small files; the client fetches only the chunks a query needs | Pagefind | Sub-linear: a small slice per search |
| B. In-memory inverted index | A prebuilt JSON index is loaded whole and queried in memory | MiniSearch, FlexSearch, Orama | Linear: the whole index downloads up front |
| C. Linear fuzzy scan | Raw records are loaded and every query scans all of them | Fuse.js | Linear in data, and in query time too |
| D. SQLite in WASM | A .sqlite file is queried with full SQL in the browser | sql.js, official SQLite WASM, wa-sqlite / SQLocal | Linear: the whole DB file downloads once |
| E. Columnar reads over Range requests | Parquet/CSV is read in pieces with HTTP Range requests | DuckDB-WASM, hyparquet | Sub-linear: only needed columns and row groups |
| F. Postgres in WASM | Full Postgres compiled to WASM, persisted locally | PGlite | Heavy fixed runtime, plus your data |
| G. Client-side vector search | Embeddings precomputed at build time, query embedded in-browser | Orama (vector plugin), client-vector-search, altor-vec, Transformers.js | Linear: vectors ship as JSON or binary |
3. Master comparison table
Versions are from the npm registry and sizes are measured gzip, both as of 2026-10-23. "Fixed cost" is the library or runtime only; your index or data is extra and is usually the bigger number for families B, C and G.
| Tool | Family | Latest version (date) | Fixed client cost (gzip, measured) | Query model | Browser persistence | Maintenance signal |
|---|---|---|---|---|---|---|
| Pagefind | A | 1.5.2 (2026-04-12) | Runtime + chunks; ~100β300 kB total per search (project's figure) | Full-text, stemming, prefix, filters, facets, multi-site | N/A (fetched on demand) | Very active; 1.5 added web-component UI, Web Worker search, CJK segmentation, diacritics, smaller indexes |
| MiniSearch | B | 7.2.0 (2025-09-16) | ~6 kB | BM25-style, prefix, fuzzy (edit distance), field boosts, autosuggest | Serialize to JSON | Active, stable |
| Fuse.js | C | 7.5.0 (2026-07-13) | ~9 kB | Bitap fuzzy, weighted keys, extended operators | Prebuilt index JSON | Very active (7.2 plugins, 7.3 Fuse.match(), 7.4 Web Workers) |
| FlexSearch | B | 0.8.212 (2025-09-06) | ~4.5 kB (light) to ~18 kB (bundle) | Contextual full-text, resolvers, tags, workers | IndexedDB (0.8+) | Active; 0.7 β 0.8 is a breaking migration |
| Orama | B / G | 3.1.18 (2025-12-19) | ~26 kB (core ESM) | BM25, typo tolerance, facets, filters, geo, vector/hybrid | Serialize via plugin | Active; has a commercial cloud alongside |
| sql.js | D | 1.15.1 (2026-10-19) | ~335 kB WASM | Full SQLite SQL (in memory) | None built in | Active |
| Official SQLite WASM | D | 3.54.1 (2026-10-19) | ~406 kB WASM | SQL, FTS5, JSON functions | OPFS (Worker) | Official SQLite subproject, tracks SQLite releases |
| wa-sqlite / SQLocal | D | 1.0.0 (2024) / 0.18.0 (2026-04) | ~similar to official build | SQL, pluggable VFSes (OPFS, IndexedDB) | OPFS / IndexedDB | wa-sqlite develops mainly on GitHub; SQLocal is a friendly wrapper over official WASM |
| DuckDB-WASM | E | 1.33.x (2026-06) | ~8 MB (EH build) to ~9 MB (MVP build); 36β41 MB uncompressed | Vectorized analytical SQL, Parquet/CSV/JSON/Arrow | OPFS (optional) | Active |
| hyparquet | E | 1.31.5 (2026-10-18) | ~20 kB | Read Parquet columns and rows via Range requests; not SQL | N/A | Very active; zero dependencies |
| PGlite | F | 0.5.11 (2026-10-21) | ~5.3 MB (3.4 MB WASM + 1.9 MB data) | Full Postgres, pgvector, PostGIS extensions | IndexedDB / OPFS | Very active (ElectricSQL) |
4. Tool-by-tool findings
4.1 Pagefind
What it is. A Rust CLI that runs after your generator, crawls the built HTML, and writes a pagefind/ folder containing a fragmented index, a JS/WASM search API, and optional UI.
How it works. On first search the client loads a small metadata file, around 10β30 kB, that maps term prefixes to index chunks. Then it fetches only the chunks matching the query terms. The project states a 10,000-page site stays under ~300 kB of total transfer, usually nearer 100 kB.
Strengths
- Framework-agnostic, because it only needs HTML output. It's distributed as a standalone binary, on npm, and as Python bindings, so Hugo users don't need Node.
data-pagefind-body,data-pagefind-ignore,data-pagefind-filteranddata-pagefind-metacontrol what gets indexed and exposed.- Multilingual with zero config, faceted filters, and multi-site search.
- New in 1.5: a web-component search UI (the older Default and Modular UIs still work), improved ranking, diacritics support, automatic CJK segmentation, search in a Web Worker, smaller indexes, and faster indexing. 1.5.2 fixed a musl-allocator slowdown in Linux builds.
- It's the default search in Astro Starlight, which is a strong adoption signal.
Weaknesses
- It doesn't run in the dev server. The index exists only after a real build, so test with
build && preview. - Matching is prefix plus stemming, not true edit-distance fuzzy. A typo that changes the term prefix finds nothing.
- It only indexes rendered pages, so it can't query records that never become HTML.
- Bundlers can choke on its runtime import (a known Next.js issue, "Failed to load Pagefind metadata"). Use a bundler-ignored dynamic import.
- The index is tied to the build. Any content or layout change needs the post-build step in CI.
Best for: blogs, docs, knowledge bases, i.e. "search the pages".
4.2 MiniSearch
What it is. A tiny, dependency-free, in-memory full-text engine built on a radix tree and an inverted index.
Strengths
- BM25-style ranking, prefix search, fuzzy search by edit distance, field boosting and restriction, and
autoSuggest. - Pluggable tokenizer, term processing and field extraction, which helps with non-English text.
- Build the index at compile time, then load it in the browser with
MiniSearch.loadJSON. - Supports adding, removing and replacing documents, which suits dynamic data.
- Query time depends on matching postings, not total document count, so queries take under a millisecond for thousands of records.
Weaknesses
- The whole index downloads up front and lives in memory. That's fine for a few thousand records but gets costly for large corpora with long bodies.
- No UI and no chunking.
Best for: catalogs, recipe or glossary collections, and small-to-medium content sites where you want ranking control.
4.3 Fuse.js
What it is. A zero-dependency approximate string matcher using the Bitap algorithm.
Strengths
- The best typo tolerance on short strings: names, titles, tags, commands.
- Weighted keys, extended search syntax (exact, prefix, inverse), logical queries, and match highlighting.
- Actively developed: v7.2 added
Fuse.use()plugins, v7.3 a staticFuse.match(), v7.4 Web Worker search with typed entry points, and 7.5.0 shipped July 2026.
Weaknesses
- Not a full-text engine. There's no inverted index, and each query is O(N Γ string length), so latency grows linearly with the data.
- It gives noisy, CPU-heavy results on long article bodies.
Best for: command palettes and quick filters over a few hundred to a few thousand short items.
4.4 FlexSearch
What it is. A full-text library built for raw throughput.
Strengths
- Consistently near the top of JS search benchmarks.
- The v0.8 rewrite added persistent indexes (IndexedDB in the browser; Redis, SQLite, Postgres, MongoDB and ClickHouse on servers), worker indexes, export/import, a Resolver for boolean queries, and an Encoder class for language customization.
- Multiple builds: light is ~4.5 kB gz and bundle ~18 kB gz.
Weaknesses
- The API is complex and the 0.7 β 0.8 migration is non-trivial. Scoring is less transparent than BM25, and the docs are dense.
Best for: large in-browser datasets, and offline-first apps caching their index in IndexedDB.
4.5 Orama
What it is. A TypeScript search engine, formerly "Lyra", that runs in any JS runtime, with an optional hosted cloud.
Strengths
- The most built in: BM25, typo tolerance, facets, filters, sorting, grouping, geosearch.
- Vector and hybrid search via plugins, which is rare for a client-side library.
- Adopted by docs tooling: Fumadocs moved its built-in search from FlexSearch to Orama, and the Node.js docs use it too.
Weaknesses
- Bigger than MiniSearch or Fuse. Its "< 2 kB" marketing claim doesn't match the measured ~26 kB gz for the v3.1.18 core; third-party trackers report ~22β24 kB.
- The whole index lives in memory, and vector indexes make it heavier still.
- Some features point you toward the commercial cloud.
Best for: React/Next.js docs sites, faceted shop-like search, and semantic-search experiments.
4.6 SQLite in the browser
| Variant | How it works | Read/Write | Best for |
|---|---|---|---|
@sqlite.org/sqlite-wasm (official) | Maintained by the SQLite project | R/W; persistent via OPFS in a Web Worker | Local-first apps, editing data in-browser |
| sql.js | Emscripten build; whole DB in memory | R/W, memory only | Small DBs downloaded in full |
| wa-sqlite / SQLocal | Alternative builds and wrappers with pluggable VFSes | R/W | When you want more VFS choices, or a friendlier API |
Build-time tips
- Build the
.sqlitefile at site-build time with any language (Go, Python, a Hugo data pipeline), runVACUUM, and serve it as a static asset. - Because the whole file downloads, keep it small: drop unused columns, and consider splitting large datasets into per-section files that load on demand.
- Use a cache-busting filename when the DB changes.
Caveats
- The WASM runtime (~335β406 kB gz) is the biggest fixed cost outside DuckDB and PGlite.
- OPFS persistence needs a Worker. The
opfsVFS needs cross-origin isolation (COOP/COEP headers), which many static hosts can't set. Theopfs-sahpoolVFS avoids that requirement. - Bundling the worker plus the
.wasmfile needs per-bundler configuration. - The whole database lives in Worker memory. On mobile, a large DB can push the browser to kill the Worker, which forces a cold restart.
Best for: small-to-medium filterable, sortable catalogs, data journalism, "browse this dataset" pages, and offline-first tools.
4.7 DuckDB-WASM
An in-process analytical (OLAP) SQL engine. It reads Parquet, CSV, JSON and Arrow, either local or over HTTP. For Parquet it fetches the footer first, then only the referenced column chunks.
- Pros: fast vectorized aggregates over millions of rows, and remote Parquet over Range requests. Great for dashboards.
- Cons: measured ~7β9 MB gzip runtime, plus 1β3 MB per extension. It runs in memory with high RAM use (~100β300 MB is typical for real workloads), is slower than native, and isn't a search engine.
- Best for: interactive charts and data exploration, not site search.
4.8 hyparquet
A pure-JS, zero-dependency Parquet reader (~20 kB gz) built to read Parquet over HTTP with Range requests, so a Parquet file on static hosting can be read straight from the browser. It doesn't support SQL. You select columns and row ranges, then filter or aggregate in JS.
- Use it when you want DuckDB-style "only fetch what you need" reads of a Parquet file without the ~8 MB runtime, for example a table viewer or a chart over a few columns.
- Gotcha: cross-origin Range requests trigger CORS preflights. Host the file on the same origin, or configure
OPTIONSand 206 responses with exposed range headers.
4.9 PGlite
Postgres compiled to WASM by ElectricSQL. Measured ~5.3 MB gz (3.4 MB WASM plus 1.9 MB data file). It supports pgvector, PostGIS and other extensions, and persists to IndexedDB or OPFS.
- Pros: the same SQL dialect as a server Postgres, and a good fit for local-first sync (ElectricSQL).
- Cons: heavy, and historically slower than SQLite WASM for simple workloads. Overkill for static search.
4.10 Client-side vector / semantic search
The pattern for static sites: compute document embeddings at build time, ship them as JSON or binary (or cache them in IndexedDB), and embed only the user's query in the browser. Transformers.js running a small model such as all-MiniLM-L6-v2 (384 dimensions) does the embedding.
- Libraries: Orama with its vector plugin (hybrid BM25 plus vector), client-vector-search (aimed at hundreds to thousands of vectors; HNSW is still on its roadmap), and altor-vec (a ~54 KB WASM vector index; its author reports ~0.6 ms p95 for 10k vectors).
- Cost: the embedding model download, typically tens of MB, usually dwarfs everything else. Lazy-load it, and consider hybrid search where keyword results show instantly and semantic results load later.
4.11 Others worth knowing
- staticseek: a lightweight full-text engine supporting all Unicode languages. It's niche but worth a look for CJK-heavy sites that don't use Pagefind.
- Hosted search (Algolia DocSearch, Typesense, Meilisearch): not static, but the usual escape hatch when you need analytics, synonyms or merchandising, or when the corpus outgrows client-side search. Starlight documents Algolia and Typesense plugins as alternatives to Pagefind.
5. Performance at a glance: a 10,000-page / ~30 MB text site (Rough guideline only)
| Metric | Pagefind | MiniSearch | Fuse.js | SQLite WASM | DuckDB-WASM | Orama |
|---|---|---|---|---|---|---|
| Runtime download (measured, gz) | small JS/WASM, loaded with chunks | ~6 kB | ~9 kB | ~335 kB | ~7β9 MB | ~26 kB |
| Index / data size | ~100β300 kB fetched, fragmented | ~2β5 MB JSON index | ~5β10 MB raw JSON | ~10β25 MB .sqlite, mostly not fetched | ~8β15 MB Parquet, mostly not fetched | ~3β8 MB |
| Transfer, 1st query | ~30β80 kB | full index | full data | ~5β20 kB | ~50β200 kB | full index |
| Transfer, later queries | ~5β15 kB of new chunks | 0 | 0 | ~1β5 kB | ~5β10 kB | 0 |
| Query latency | ~10β50 ms (network-bound) | < 1β5 ms | ~50β300 ms (linear scan) | ~5β15 ms after warm-up | ~10β50 ms | < 1β5 ms |
| Typo tolerance | prefix + stemming | edit distance | Bitap (best) | FTS5 / trigram (DIY) | LIKE / regex | edit distance |
| Client RAM | very low | moderate | high | lowβmoderate | high | moderate |
| Build overhead | fast Rust crawl | JSON export plus prebuilt index | raw JSON export | DB creation and indexing | Parquet compilation | JSON export |
Network-profile insight
Pagefind and the Range-request tools (DuckDB-WASM, hyparquet) swap one big download for many small requests. That works well on HTTP/2 or HTTP/3 multiplexing but suffers head-of-line blocking on HTTP/1.1. Every major static host (GitHub Pages, Netlify, Cloudflare Pages, Vercel) serves HTTP/2+, but check self-hosted nginx and Apache setups.
6. Charts
Scores are qualitative (1β10) and drawn from the findings above.
6.1 Setup effort vs. query power
6.2 Measured runtime download (kB, gzip, excluding your data)
Each gridline is 10Γ the one before it. On a normal scale the JS libraries would be invisible next to the WASM databases, which are one to three orders of magnitude heavier. Pagefind is left out because its cost is runtime plus index chunks, ~100β300 kB per search session.
6.3 Capability scores (1β10)
| Capability | Pagefind | MiniSearch | Fuse.js | FlexSearch | Orama | SQLite WASM | DuckDB-WASM | hyparquet |
|---|---|---|---|---|---|---|---|---|
| Ease of setup | 10 | 8 | 10 | 6 | 7 | 4 | 3 | 6 |
| Full-text relevance | 8 | 8 | 3 | 7 | 9 | 7 (FTS5) | 2 | 1 |
| Typo tolerance | 5 | 7 | 10 | 6 | 8 | 2 | 1 | 1 |
| Scales to 10k+ pages / large data | 10 | 5 | 2 | 7 | 6 | 5 | 9 | 8 |
| Structured queries / filters | 6 | 4 | 3 | 5 | 8 | 10 | 10 | 5 |
| Small payload | 9 | 10 | 10 | 9 | 8 | 4 | 2 | 9 |
| Framework-agnostic | 10 | 8 | 8 | 8 | 8 | 7 | 7 | 8 |
| Maintenance health (2026) | 10 | 8 | 10 | 7 | 8 | 10 | 9 | 10 |
6.4 Decision flow
7. Framework integration
7.1 Compatibility matrix
| Tool | Hugo | SvelteKit (adapter-static) | Next.js (output: 'export') | Astro / Starlight |
|---|---|---|---|---|
| Pagefind | β hugo && pagefind --site public; binary, no Node | β run on build/ | β
run on out/; bundler-ignored import | β Starlight default; integrations for plain Astro |
| MiniSearch | β JSON custom output format | β prerendered +server.js index | β build index, loadJSON in client component | β static endpoint emits index |
| Fuse.js | β very common (JSON + Fuse) | β | β | β |
| FlexSearch | β several themes ship it | β | β | β |
| Orama | β community integrations | β | β first-class in Fumadocs | β official Astro plugin |
| SQLite WASM | β οΈ build .sqlite separately, copy to static/ | β
Vite ?worker / ?url | β οΈ worker + WASM bundling config | β Vite-based, like SvelteKit |
| DuckDB-WASM / hyparquet | β οΈ possible, rarely worth it | β dashboards | β dashboards | β islands for dashboards |
7.2 Hugo
Hugo is a single Go binary with no JS runtime at build time.
- Pagefind fits best:
hugo --minify pagefind --site public - JSON-index libraries: add a JSON output format for the home page and an
index.jsontemplate that ranges over.Site.RegularPages. Bundle the library with Hugo Pipes (js.Build, which uses esbuild). Fetch the index on search-box focus, not on page load. - SQLite: generate the DB in a separate step (Go's
modernc.org/sqlite, a Python script, etc.), apply the build tips from Β§4.6 (SQLite in the browser), and drop it intostatic/.
7.3 Svelte / SvelteKit
- Use
@sveltejs/adapter-staticand prerender every route. - Hydration isolation: anything touching
windowor WASM goes insideonMountor a dynamicimport(). Otherwise prerendering fails. - Pagefind: run it on
build/aftervite build, and load/pagefind/pagefind.jswith a dynamic import insideonMount. - MiniSearch/Orama: emit
/search-index.jsonfrom a prerendered+server.jsendpoint. - SQLite/DuckDB: Vite's
?workerand?urlimports make this the smoothest framework for WASM. Wrap the Worker message interface in a Svelte store.
7.4 Next.js (App Router, static export)
- Set
output: 'export'innext.configto get anout/directory. - Search components need
'use client'. Initialize engines inuseEffect, because client-side routing doesn't re-run<script>tags. - Pagefind: add
"postbuild": "pagefind --site out"and load it withimport(/* webpackIgnore: true */ '/pagefind/pagefind.js'). Community wrappers such asnext-static-searchandpagefind-reactpackage this up. - WASM: configure webpack/Turbopack to emit
.wasmas a static asset, and never evaluate it during prerender. - Orama is the path of least resistance for React docs (Fumadocs).
- With SSR or ISR instead of export, search can live in a route handler, but then the setup is no longer static.
7.5 Astro
- Starlight ships Pagefind by default with no config. Algolia DocSearch and Typesense are documented plugin alternatives.
- Plain Astro: use a Pagefind integration, or emit a JSON index from a static endpoint (
src/pages/search.json.ts) for MiniSearch, Fuse or Orama, then hydrate a search island withclient:idleorclient:visible.
8. Recommendations by scenario
| Scenario | Recommendation | Why |
|---|---|---|
| Blog or docs site, any framework | Pagefind | Zero code, tiny payload, scales |
| Hugo site, no Node toolchain | Pagefind binary | One command, no npm |
| Astro/Starlight docs | Pagefind (built in) | Already configured |
| Next.js docs with a React UI | Orama (Fumadocs) or Pagefind | Native integration vs. framework-free |
| Catalog of < ~5k structured records | MiniSearch | Full-text, fuzzy and prefix, ~6 kB, prebuilt index |
| Command palette or title/tag filter | Fuse.js | Best fuzzy matching on short strings |
| Large in-browser dataset with offline cache | FlexSearch 0.8 | Speed plus IndexedDB |
| Faceted shop-like search | Orama | Facets, filters, sorting built in |
| Semantic or "ask the docs" search | Orama vector plugin + Transformers.js, build-time embeddings | Hybrid lexical and semantic, fully client-side |
| Filter/sort/join a large read-only dataset | DuckDB-WASM + Parquet | Real SQL, fetches only the columns it needs |
| Filter/sort/join a small-to-medium dataset | Official SQLite WASM | Full SQLite, one-time download |
| Offline-first app that edits data | Official SQLite WASM + OPFS (or SQLocal) | Durable, transactional |
| Local-first app needing Postgres or pgvector | PGlite | Same dialect as the server |
| Dashboard over Parquet with SQL | DuckDB-WASM | Columnar analytics |
| Table or chart from a Parquet file, payload-sensitive | hyparquet | ~20 kB, Range reads, no SQL |
| Needs analytics, synonyms, merchandising | Hosted: Algolia / Typesense / Meilisearch | Beyond what static can do |
9. Practical gotchas
- Measure the index, not just the library. A 6 kB library with a 4 MB index is a 4 MB feature. Only Pagefind, DuckDB/Parquet and hyparquet avoid an all-at-once download.
- Prebuild indexes at compile time for MiniSearch, Fuse, FlexSearch and Orama, so users don't spend CPU on indexing.
- Lazy-load search. Load the runtime and index on search-box focus, not on every page view.
- Range requests need uncompressed, Range-capable hosting. GitHub Pages works. Some CDNs gzip on the fly and break Range requests. Cross-origin Range requests need CORS that exposes
Content-Range,Accept-RangesandContent-Length. - Prefer HTTP/2 or HTTP/3 for the many-small-requests tools (Pagefind, DuckDB-WASM, hyparquet).
- CSP: the WASM tools (Pagefind, SQLite, DuckDB, PGlite) may need
'wasm-unsafe-eval'. - COOP/COEP: some SQLite OPFS modes and multithreaded WASM builds need cross-origin isolation headers, which many static hosts can't set. Pick a VFS that doesn't need them (e.g.
opfs-sahpool). - Dev vs. production: Pagefind's index exists only after a build, so test with
build && preview. - Index staleness: build-tied indexes (Pagefind, prebuilt SQLite or Parquet) must regenerate in CI on every content change. Use cache-busting filenames.
- Worker memory: in-memory databases (SQLite, DuckDB, PGlite) can grow large, and mobile browsers may kill Workers. Handle re-initialization gracefully.
- Privacy: every client-side option keeps queries on the visitor's device, unlike hosted search.
- Re-check sizes and versions before committing. These numbers drift between releases.

