TanStack / TanStack/db

useLiveQuery: subscribers rerender on every collection revision bump even when their selected rows are unchanged (forceStoreRerender in flushPublications)

Open
#1,829 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
3.9k
Forks
266
Avg merge
1d 4h
Merged PRs (30d)
55

Description

Companion to #1827 (live-query graph run hook), same app, same measurement rig.

What we see (@tanstack/db 0.7.2, @tanstack/react-db)

On a page with 6 useLiveQuery subscribers over the same source collections (a chat composer, a main view with 2 queries, an outbox replay, a sidebar section, a tab registrar), every committed transaction on a source collection makes all 6 rerender, including subscribers whose query result did not change a single row. The path is LiveQueryObserverImpl.flushPublications → forceStoreRerender: the subscriber's snapshot object is new on every publication, so useSyncExternalStore sees a changed snapshot and React renders.

Measured (React DevTools hook walk restricted to cloned subtrees, unminified prod build, 24 sidebar rows, median of 3):

  • one row's pull_request update → 1 commit → 285 fibers with performed work, of which ~60 are the composer subtree (ChatComposer, tray, footer, focus trap) whose query selects nothing to do with pull requests, plus MainView ×2, SessionOutboxReplay, the sessions section; 55–120 ms main-thread busy per tick.
  • a bulk delivery of 1,500 changed rows arriving as 1,500 server transactions → 850–885 commits in ~15 s at a 10–50 ms cadence, 241 fibers each, 0 network in the window; the same 6 forceStoreRerender sources on every commit. Per-commit cost × commits, so batch size does not help.

The affected subscribers' selected rows are unchanged on those commits; their rerender does no DOM work, but the render itself (composer ~60 fibers) is the cost.

What would help

Either of:

  1. Identity-stable snapshot when the selection is unchanged. If a publication produces no change to the subscriber's result set (no key added/removed/updated within the query's selection), getSnapshot returns the previous snapshot object so useSyncExternalStore bails out. A structural compare on the D2 output for that subscriber, or a per-subscriber "dirty" flag set only when its graph emitted a change.
  2. A batched revision. Let a subscriber read a revision that advances only when its own result changed, so useLiveQuery can compare previousRevision === currentRevision before forcing a store rerender.

(1) is what a caller would expect from a live query: rerender when my rows changed. We can work around at the call site with useMemo on the result array by identity, but the store-level rerender still fires; the fix belongs where the snapshot is produced.

Repro

  • Two collections A and B, live queries qA = from(A), qB = from(B) mounted in one tree.
  • Insert one row into A.
  • Observe: the component holding qB rerenders (its useSyncExternalStore snapshot changed) though qB's result is identical.

Happy to test a patch against our rig; the numbers above are reproducible on demand.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start at LiveQueryObserverImpl.flushPublications and the forceStoreRerender path described in the report. Reproduce the A/B case with qA and qB, then trace how each subscriber's snapshot is produced and compared by useSyncExternalStore. Done means inserting into A does not rerender qB when its selected rows are unchanged, while changes to a subscriber's own result still trigger its rerender.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript
Domain
frontend, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.