Reference

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.

View as Markdown

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 and 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 EXISTS against the SQLite-backed replica 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