Next.js guide

Offline-First Search in Next.js with altor-vec

Use altor-vec to add offline-first search to your Next.js app — entirely in the browser, with no server, no API keys, and zero per-query cost. Build search that works without a network connection — cache the vector index in IndexedDB and serve search entirely from browser storage, enabling PWAs and offline-first apps to maintain full search capability offline.

Install: npm install altor-vec @xenova/transformers

Implementation

Uses App Router with 'use client' directive. Uses useRef for the engine, useState for results.

// Next.js PWA — offline-first search with service worker + IndexedDB
// next.config.js: add next-pwa for service worker support
// components/OfflineSearch.tsx
'use client';
import { useState, useEffect, useRef } from 'react';
import init, { WasmSearchEngine } from 'altor-vec';
import { pipeline } from '@xenova/transformers';

export function OfflineSearch({ indexUrl }: { indexUrl: string }) {
  const engine = useRef(null);
  const embedder = useRef(null);
  const [ready, setReady] = useState(false);
  const [isOffline, setIsOffline] = useState(false);
  const [results, setResults] = useState([]);

  useEffect(() => {
    setIsOffline(!navigator.onLine);
    window.addEventListener('online', () => setIsOffline(false));
    window.addEventListener('offline', () => setIsOffline(true));
  }, []);

  useEffect(() => {
    (async () => {
      await init();
      embedder.current = await pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2');
      // Service worker caches this via next-pwa — works offline after first load
      const resp = await fetch(indexUrl);
      engine.current = WasmSearchEngine.from_json(await resp.text());
      setReady(true);
    })();
  }, [indexUrl]);

  async function search(q: string) {
    if (!engine.current || q.length < 2) return;
    const out = await embedder.current(q, { pooling: 'mean', normalize: true });
    const hits = JSON.parse(engine.current.search(new Float32Array(out.data), 5));
    setResults(hits);
  }

  return (
    
{isOffline &&

Offline — using cached index

} search(e.target.value)} placeholder={ready ? 'Search (works offline)...' : 'Loading...'} />
    {results.map((r, i) =>
  • Result #{r.id} (score: {r.score.toFixed(2)})
  • )}
); }

Performance

Load from IndexedDB: ~50–200ms. Search: <1ms. Zero network dependency after first load. Measured on M2 MacBook Pro, Chrome 124. Mobile is typically 2–4× slower — test on target devices before deploying.

Index sizeDimensionsQuery p50Memory
1,000 vectors384~0.1ms~2MB
10,000 vectors384~0.4ms~17MB
50,000 vectors384~0.9ms~85MB

When this approach works best

Limitations

Frequently asked questions

How do I detect when the user is offline and fall back to cached search?

Use navigator.onLine and the 'online'/'offline' window events. When offline, load the index from IndexedDB. When online, optionally fetch a fresher index. In a service worker, cache the index file with a cache-first strategy.

What is the maximum index size I can store in IndexedDB?

IndexedDB storage limits are browser- and device-dependent: Chrome allows up to ~60% of available disk space, Firefox up to 50%. In practice, a 50K-document index at 384 dimensions (~85MB JSON) is the practical upper limit for reliable cross-device support.

How do I use altor-vec in a service worker for offline search?

altor-vec WASM runs in service workers. Import altor-vec in your service worker, cache the index JSON with a cache-first strategy, and respond to search fetch events by loading the cached index and running engine.search() in the service worker context.

Related resources

framework

use case

reference