Skip to content

Solid renderer rework - huge performance gains#1609

Open
fezproof wants to merge 3 commits into
TanStack:refactor/live-query-observerfrom
fezproof:solid-renderer-rework
Open

Solid renderer rework - huge performance gains#1609
fezproof wants to merge 3 commits into
TanStack:refactor/live-query-observerfrom
fezproof:solid-renderer-rework

Conversation

@fezproof

@fezproof fezproof commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Changes how the solid-js renderer updates data to be much more performant in all cases.
This PR manages to keep the API interface exactly the same, while improving change updates + mounting performance.

Currently whenever any data changes, the entire store is reset. This is reliable to keep the data consistent, but for huge collections that only have one field change it is a bit excessive. The changes handle different cases in a few different ways with a huge focus on performance and memory improvements.

  • It keeps avoids cloning any data in the solid renderer, it will only reference the data already in the solid store
    • It also handles the double data set on mount that was happening with the old renderer
  • Uses $key for reconcile
  • It handles singleResult queries much more cleanly
  • If the changes are only just updates that will not change the data order, it will update the rows in place
  • It avoids creating new objects and arrays at all costs, reusing temp arrays so that memory only needs to grow once
  • It lazily mounts state if it is accessed. State is rarely used in reality and can probably be removed, but in the interest of compatibility I just made it faster

All this adds together to a much more reliable and performant library that keeps the promised performance by default of both solid-js and tanstack-db.

I have also extensively tested this in a real world app, and no problems where witnessed.

Benchmarks

When writing this PR I wrote extensive benchmarks that compared this version with the version currently in main. Here are the results.

JSDOM Benchmarks

Initial All-Row Mount

Case Current Main Result
10 rows 0.08ms 0.12ms 1.42× faster
1,000 rows 4.77ms 7.97ms 1.67× faster
10,000 rows 48.2ms 86.2ms 1.79× faster

Single-Row Update in All-Row Query

Case Current Main Result
10 rows 1.71ms 1.75ms 1.03× faster
1,000 rows 7.37ms 12.4ms 1.69× faster
10,000 rows 57.3ms 123.3ms 2.15× faster

10% Row Batch Update in All-Row Query

Case Current Main Result
10 rows 1.71ms 1.74ms 1.02× faster
1,000 rows 17.5ms 21.9ms 1.26× faster
10,000 rows 200.8ms 276.0ms 1.37× faster

Non-Matching Update in Single-Row Query

Case Current Main Result
10 rows 0.61ms 0.60ms 1.00× slower
1,000 rows 0.66ms 0.66ms 1.00× faster
10,000 rows 1.73ms 1.76ms 1.02× faster

Remount After Update

Case Current Main Result
10 rows 2.41ms 2.49ms 1.03× faster
1,000 rows 12.4ms 20.8ms 1.68× faster
10,000 rows 110.6ms 210.6ms 1.90× faster

Sorted Query Moves One Row

Case Current Main Result
10 rows 0.12ms 0.15ms 1.20× faster
1,000 rows 7.28ms 10.7ms 1.47× faster
10,000 rows 152.8ms 194.4ms 1.27× faster

Single-Row Query Update

Case Current Main Result
10 rows 1.88ms 1.81ms 1.04× slower
1,000 rows 2.01ms 1.86ms 1.08× slower
10,000 rows 2.76ms 2.71ms 1.02× slower

Library-Unfavourable Repeated Update Commits

Case Current Main Result
10k rows, 200 single-row update commits 1594.3ms 7728.8ms 4.85× faster
1k rows, indexed single-row query, 100 matching update commits 26.3ms 30.3ms 1.15× faster
1k rows, indexed single-row query, 100 non-matching update commits 14.5ms 14.4ms 1.01× slower

Large Change Sets

Case Current Main Result
1k rows, 100% row batch update in all-row query 62.4ms 68.1ms 1.09× faster

Deep Row Cloning

Case Current Main Result
1k deep rows, single-row update in all-row query 11.2ms 25.3ms 2.26× faster
1k deep rows, 3-level nested update in all-row query 11.2ms 25.2ms 2.26× faster

Real browser benchmarks

Initial Table Render

Chrome

Case Current Main Result
100 rows initial table render 8.35ms 8.33ms 1.00× slower
1,000 rows initial table render 28.7ms 31.1ms 1.09× faster

Firefox

Case Current Main Result
100 rows initial table render 8.33ms 8.33ms 1.00× faster
1,000 rows initial table render 38.9ms 42.7ms 1.10× faster

Safari

Case Current Main Result
100 rows initial table render 16.7ms 16.7ms 1.00× faster
1,000 rows initial table render 32.1ms 33.3ms 1.04× faster

Single-Row Update Commits

Chrome

Case Current Main Result
1,000 rows table, 25 single-row update commits 53.5ms 120.7ms 2.26× faster

Firefox

Case Current Main Result
1,000 rows table, 25 single-row update commits 55.4ms 126.4ms 2.28× faster

Safari

Case Current Main Result
1,000 rows table, 25 single-row update commits 51.8ms 88.6ms 1.71× faster

Batch Update

Chrome

Case Current Main Result
1,000 rows table, 100-row batch update 40.8ms 45.7ms 1.12× faster

Firefox

Case Current Main Result
1,000 rows table, 100-row batch update 53.8ms 54.8ms 1.02× faster

Safari

Case Current Main Result
1,000 rows table, 100-row batch update 43.2ms 46.2ms 1.07× faster

Sorted Reorder

Chrome

Case Current Main Result
1,000 rows sorted table, update reorders one row 33.3ms 34.1ms 1.02× faster

Firefox

Case Current Main Result
1,000 rows sorted table, update reorders one row 46.5ms 54.8ms 1.18× faster

Safari

Case Current Main Result
1,000 rows sorted table, update reorders one row 47.5ms 47.0ms 1.01× slower

Query Filter Changes

Chrome

Case Current Main Result
1,000 rows table, query filter changes after mount 17.1ms 18.7ms 1.10× faster

Firefox

Case Current Main Result
1,000 rows table, query filter changes after mount 23.2ms 34.1ms 1.47× faster

Safari

Case Current Main Result
1,000 rows table, query filter changes after mount 33.3ms 33.3ms 1.00× faster

Deep Nested Update Commits

Chrome

Case Current Main Result
1,000 deep rows table, 25 deep nested update commits 52.4ms 219.8ms 4.20× faster

Firefox

Case Current Main Result
1,000 deep rows table, 25 deep nested update commits 77.2ms 217.8ms 2.82× faster

Safari

Case Current Main Result
1,000 deep rows table, 25 deep nested update commits 55.3ms 166.1ms 3.00× faster

I am happy to provide the benchmark code if wanted, its just a tad noisy and adds playwright as a dependency which could slow down other runners. Can clean it up and add some actions that show benchmarks in comparison with main when creating PRs possibly

✅ Checklist

  • I have tested this code locally with pnpm test.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features
    • Added a shared live-query observer layer in the core package and updated framework adapters (including Angular and Solid) to use it for consistent lifecycle and snapshot behavior.
  • Performance
    • Improved live-query result synchronization in Solid for faster updates and more efficient state/data reconciliation.
  • Bug Fixes
    • Fixed live-query resync issues when switching or recompiling queries, preventing stale keys/rows from persisting and improving status/snapshot update handling.
  • Tests
    • Added regression and conformance coverage for observer snapshot/delivery semantics and state consistency.

@fezproof fezproof changed the title Solid renderer rework Solid renderer rework - huge performance gains Jun 22, 2026
@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

@tanstack/db gains a shared live-query observer with revision-aware snapshots and serialized lifecycle handling. Angular adopts it, conformance tests cover keyed state cleanup, and Solid receives reconciliation-based data/state synchronization with granular patching and collection-switch regression coverage.

Changes

Live-query observer and collection revisions

Layer / File(s) Summary
Observer contracts and collection revision tracking
packages/db/src/collection/..., packages/db/src/live-query-observer.ts, packages/db/src/errors.ts, packages/db/src/index.ts
Adds revision-backed snapshot caching, granular and wholesale observer modes, subscription lifecycle management, FIFO notification delivery, disposal errors, and public exports.
Observer delivery and lifecycle validation
packages/db/tests/live-query-observer.test.ts
Tests snapshot identity, readiness and status transitions, subscription behavior, disposal, bootstrap differences, and ordered non-reentrant dispatch.

Angular integration and conformance coverage

Layer / File(s) Summary
Angular observer integration and state conformance
packages/angular-db/src/index.ts, packages/angular-db/tests/..., packages/db/tests/conformance/...
Migrates Angular synchronization to the wholesale observer, updates the mock collection contract, forwards state, and verifies stale keyed entries are removed after query recompilation.

SolidJS renderer rework

Layer / File(s) Summary
Reconciliation, patching, and lazy state synchronization
packages/solid-db/src/useLiveQuery.ts
Uses reconciliation scratch storage, granular update paths, authoritative fallback resyncs, collection-reference tracking, conditional resource tracking, and lazy .state synchronization.
Collection-switch regression coverage and release metadata
packages/solid-db/tests/useLiveQuery.test.tsx, .changeset/*.md
Adds collection-switch and stale-continuation regression coverage, normalizes no-op callbacks, and documents the observer migration and Solid renderer release changes.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Collection
  participant LiveQueryObserver
  participant AngularAdapter
  participant SolidRenderer

  Collection->>LiveQueryObserver: emit row or status change
  LiveQueryObserver->>AngularAdapter: publish wholesale notification
  AngularAdapter->>Collection: read current snapshot
  LiveQueryObserver->>SolidRenderer: publish change batch
  SolidRenderer->>SolidRenderer: patch rows or resync collection
Loading

Possibly related issues

Possibly related PRs

  • TanStack/db#1636 — Shares the conformance harness changes, including keyed state assertions and Angular driver wiring.
  • TanStack/db#1642 — Directly overlaps with Solid reconciliation and shared observer-driven live-query semantics.

Suggested reviewers: kyleamathews, samwillis, kevin-dp

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 71.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly reflects the main change: a Solid renderer rework focused on performance improvements.
Description check ✅ Passed The description follows the template and includes changes, testing confirmation, and release impact/changeset info.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/solid-db/src/useLiveQuery.ts`:
- Line 520: The reconcile call in the if (!needsResync) block is missing the
reconciliation options used elsewhere in the code. Update the reconcile(row)
call to match the same pattern used in patchStoreRow by adding the
RECONCILE_DEEP option and merge: true parameters. This ensures consistent
behavior and preserves fine-grained field subscriptions to prevent unnecessary
re-renders for unchanged fields in single-result queries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 97063a35-6dcc-4e9c-b2bb-dcecc6cb68d8

📥 Commits

Reviewing files that changed from the base of the PR and between 9a5e8e5 and c90c950.

📒 Files selected for processing (3)
  • .changeset/small-doors-raise.md
  • packages/solid-db/src/useLiveQuery.ts
  • packages/solid-db/tests/useLiveQuery.test.tsx

Comment thread packages/solid-db/src/useLiveQuery.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/db/src/live-query-observer.ts`:
- Around line 206-258: Update attach() so wholesale subscriptions do not
synchronously publish the status transition triggered by subscribeChanges()
starting an idle or cleaned-up collection. Register the status:change listener
only after the initial subscription completes, or otherwise suppress that first
wholesale status notification, while preserving granular initial-state delivery
and existing cleanup behavior through release.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c87db4e9-35fe-47fe-a179-b805a975f40b

📥 Commits

Reviewing files that changed from the base of the PR and between d9641a6 and ec8098d.

📒 Files selected for processing (13)
  • .changeset/live-query-observer.md
  • .changeset/small-doors-raise.md
  • packages/angular-db/src/index.ts
  • packages/angular-db/tests/conformance.test.ts
  • packages/angular-db/tests/inject-live-query.test.ts
  • packages/db/src/collection/changes.ts
  • packages/db/src/collection/index.ts
  • packages/db/src/errors.ts
  • packages/db/src/index.ts
  • packages/db/src/live-query-observer.ts
  • packages/db/tests/conformance/contract.ts
  • packages/db/tests/conformance/suite.ts
  • packages/db/tests/live-query-observer.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • .changeset/small-doors-raise.md

Comment thread packages/db/src/live-query-observer.ts
@fezproof
fezproof force-pushed the solid-renderer-rework branch from ec8098d to f6306d3 Compare July 22, 2026 15:17
@fezproof
fezproof changed the base branch from main to refactor/live-query-observer July 22, 2026 15:21

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
packages/solid-db/tests/useLiveQuery.test.tsx (1)

2709-2820: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Solid regression test for collection-switch resync — well targeted.

This test correctly exercises the race the PR is guarding against: it blocks newLiveQuery.toArrayWhenReady() so the createResource fetcher stalls, then verifies the hook still drops old-only/adds new-only and reflects a subsequent newSource.update() — confirming the observer's authoritative resync (not the resource) drives the switch, matching the resourceGeneration guard and syncedCollection !== currentCollection resync path in useLiveQuery.ts.

One nit: resolveNewLiveQuery!() is fired at the end without awaiting the resumed fetcher continuation (it calls originalToArrayWhenReady() then re-enters syncDataFromCollection/setStatus). If the hook/component is torn down by the testing-library's automatic cleanup before that continuation settles, the resolved promise chain runs after teardown, which can produce unhandled-update warnings or floating promises across tests. Consider await the resolution (e.g., wrap in a promise the test awaits) before the test returns.

As per coding guidelines, "Handle async race conditions where operations may resolve in unexpected order; ensure stale results are properly ignored" — the test itself should also fully settle its own deferred async work before completing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/solid-db/tests/useLiveQuery.test.tsx` around lines 2709 - 2820,
Update the deferred newLiveQuery fetch in the test around
originalToArrayWhenReady and resolveNewLiveQuery so the test awaits the resumed
fetcher continuation before returning. Preserve the existing assertions while
ensuring the promise chain that re-enters syncDataFromCollection and setStatus
is fully settled before teardown.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/solid-db/tests/useLiveQuery.test.tsx`:
- Around line 2709-2820: Update the deferred newLiveQuery fetch in the test
around originalToArrayWhenReady and resolveNewLiveQuery so the test awaits the
resumed fetcher continuation before returning. Preserve the existing assertions
while ensuring the promise chain that re-enters syncDataFromCollection and
setStatus is fully settled before teardown.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 21febaa2-54c2-4120-b940-81e897a3adc7

📥 Commits

Reviewing files that changed from the base of the PR and between ec8098d and f6306d3.

📒 Files selected for processing (3)
  • .changeset/small-doors-raise.md
  • packages/solid-db/src/useLiveQuery.ts
  • packages/solid-db/tests/useLiveQuery.test.tsx
🚧 Files skipped from review as they are similar to previous changes (2)
  • .changeset/small-doors-raise.md
  • packages/solid-db/src/useLiveQuery.ts

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant