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/wasmand on the local-first 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 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 (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.
Reproduce it
Both benchmarks live in github.com/rindle-sh/rindle
and run from a checkout:
# 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
for the Chinook matrix and
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:
- 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
EXISTSagainst the SQLite-backed replica hydrates quadratically until you runANALYZEon the database — a one-line, data-driven fix that returns it to linear. The in-process engine is unaffected.
Next steps
- How it works — why incremental beats recompute.
- The local-first client — where sub-microsecond local maintenance becomes an instant UI.
- Supported queries — the honest matrix of query shapes.