# Performance

Why incremental maintenance is sub-microsecond and stays flat as data grows — measured end to end over a real dataset, plus a head-to-head against TanStack DB.

The whole point of incremental view maintenance is that the cost of a write
tracks the **change**, not the table. This page is the measured version of that
claim — real numbers, on a real dataset, with the methodology you can rerun.

> **The headline:** on the in-process engine — the same one that runs in the
> browser via [`@rindle/wasm`](/docs/wasm-client) and on the
> [local-first client](/docs/client) — an incremental update costs **a few
> hundred nanoseconds to a few microseconds**, and stays essentially **flat as the
> dataset grows 30×**.

## Incremental maintenance is sub-microsecond — and flat

Measured over the [Chinook](https://github.com/lerocha/chinook-database) dataset,
scaled 1× / 10× / 30× (up to ~124k source rows), applying one row change to a
materialized view and reading the result:

| Pattern | Per write (in-process engine) | As data grows 30× |
| --- | --- | --- |
| filtered match | **~430 ns** | flat |
| ordered top-N (`limit`) | **~420 ns** | flat |
| nested child (`sub`) | **~2.5 µs** | flat |
| flipped `EXISTS` | **~880 ns** | flat |

The number that matters is the last column. A `push_nested_child` costs ~2.5 µs at
6.8k rows and ~4 µs at 124k — it **does not grow with the table**. That is the IVM
contract made physical: you pay for the delta, not the dataset.

Compare that to the alternative everyone reaches for first — re-running the query
on every write. A from-scratch hydrate of the same view is in the **hundreds of
microseconds to low milliseconds** range and grows with the data. Incremental
maintenance replaces that per-write recompute with a sub-microsecond delta. The
gap widens every time the table does.

## A small, embeddable engine

The engine is built to go anywhere, so the footprint is part of the contract:

- **~200 kB gzipped** — the entire engine, compiled to WebAssembly, running
  in-process in a browser tab. No server, no worker pool, no round-trip.
- **`std`-only core** — the Rust engine (`rindle`) has no required C toolchain and no
  heavy dependencies; it embeds directly in a binary.
- **Single-threaded by design** — one engine per thread (`!Send`), so there are no
  locks on the hot path. You scale with independent engines and message passing,
  not a shared mutex.

## Head-to-head: Rindle vs. TanStack DB

The abstract claim — incremental beats recompute — is easiest to read against
another **client-side, incremental-view-maintenance store**. So we ran Rindle's
wasm engine head-to-head with [TanStack DB](https://tanstack.com/db) (whose live
queries are maintained by differential dataflow), same dataset byte-for-byte,
same logical query on both, both indexed for the workload. The dataset is an
issue-tracker shape at **61k rows** (1k users, 10k issues, 50k comments).

The representative client workload is a **bounded viewport** — a UI fetches
enough to fill a view (≤ 50 rows), not the whole table. On that workload Rindle
wins both paths across the board:

### Hydrate from scratch — the cold first paint (ms, lower is better)

| Query | TanStack DB | Rindle | Speedup |
| --- | --: | --: | --: |
| list: newest 50 open | 2.11 | 1.46 | **1.45×** |
| list + author | 8.03 | 1.35 | **5.93×** |
| list + comment count | 9.90 | 1.49 | **6.64×** |
| list + 3 recent comments | 7.92 | 1.96 | **4.05×** |
| issue detail + comments | 7.19 | 0.534 | **13.45×** |

### Incremental update — the steady-state hot path (ms per write, lower is better)

| Query | TanStack DB | Rindle | Speedup |
| --- | --: | --: | --: |
| list: newest 50 open | 5.36 | 0.113 | **47×** |
| list + author | 6.61 | 0.133 | **50×** |
| list + comment count | 25.29 | 0.086 | **295×** |
| list + 3 recent comments | 24.80 | 0.129 | **192×** |
| issue detail + comments | 24.91 | 0.072 | **347×** |

The incremental column is the IVM contract again: Rindle pays for the delta, so a
write stays sub-millisecond and the margin **widens with query complexity** —
from ~47× on a flat list to ~347× once the view nests children. The hydrate
column is closer (TanStack is pure JS; Rindle pays `serde-wasm-bindgen` to ship
the viewport across the wasm boundary), but even there the engine's efficiency
shows through on every shape but the simplest list.

One honest edge: if you pull the **entire** result set into JS — a 10k-row
unfiltered scan, which is not how a client app queries — the per-row boundary
cost makes the pure-JS store faster on bulk hydration. Incremental stays a
decisive Rindle win regardless. The full matrix, including that case, is in
[`apps/bench-tanstack/RESULTS.md`](https://github.com/rindle-sh/rindle/blob/main/apps/bench-tanstack/RESULTS.md).

## Reproduce it

Both benchmarks live in [`github.com/rindle-sh/rindle`](https://github.com/rindle-sh/rindle)
and run from a checkout:

```bash
# end-to-end IVM matrix over Chinook (hydration + incremental push, both leaves)
cargo run -p rindle-sqlite --release --features fast-alloc --example bench_chinook_rs

# head-to-head against TanStack DB over a 61k-row issue-tracker dataset
SCALE=large pnpm --filter @rindle/bench-tanstack bench
```

See [`CHINOOK-PERF.md`](https://github.com/rindle-sh/rindle/blob/main/CHINOOK-PERF.md)
for the Chinook matrix and
[`apps/bench-tanstack/README.md`](https://github.com/rindle-sh/rindle/blob/main/apps/bench-tanstack/README.md)
for the TanStack head-to-head — the dataset, the query ladders, the fairness
notes, and the timing harness. Numbers are machine-specific; re-run locally.

## Honesty about the edges

The numbers above are the in-process engine — the one the browser and local-first
clients run. Two caveats, both documented in
[`CHINOOK-PERF.md`](https://github.com/rindle-sh/rindle/blob/main/CHINOOK-PERF.md):

- **Hydration is not incremental.** Materializing a view from scratch is
  proportional to the result it produces (hundreds of µs to low ms). It's the
  *steady-state* push that's sub-microsecond. Most apps hydrate once and push
  forever.
- **One pattern needs index stats.** A correlated `EXISTS` against the SQLite-backed
  [replica](/docs/replica-and-views) hydrates quadratically until you run `ANALYZE`
  on the database — a one-line, data-driven fix that returns it to linear. The
  in-process engine is unaffected.

## Next steps

- [How it works](/docs/how-it-works) — why incremental beats recompute.
- [The local-first client](/docs/client) — where sub-microsecond local
  maintenance becomes an instant UI.
- [Supported queries](/docs/supported-queries) — the honest matrix of query shapes.

---

[View this page on Rindle](https://rindle.sh/docs/performance)
