Skip to content

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

Description

@the-company-company-staging

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions