perf: optimize hot paths in persistence, scheduling, and React hooks#1542
perf: optimize hot paths in persistence, scheduling, and React hooks#1542KyleAMathews wants to merge 2 commits into
Conversation
Reduce unnecessary allocations and algorithmic complexity: - Schwartzian transform for Set/Map sorting in toStableSerializable (O(n) vs O(n log n) serializations) - splice(0) queue flush instead of while/shift (O(n) vs O(n²)) - Map-based lookup in KeyScheduler.updateTransactions (O(n+m) vs O(n*m)) - Set-based filtering in loadPendingTransactions - Lazy property access in useLiveQuery — status-only consumers skip entries() entirely - Hoist Date.now() in resetRetryDelays for consistent timestamps The useLiveQuery lazy getters share a single entries snapshot to prevent tearing between state and data access. Co-Authored-By: Claude Opus 4.6 <[email protected]>
Co-Authored-By: Claude Opus 4.6 <[email protected]>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (9)
📝 WalkthroughWalkthroughThis PR optimizes hot paths across three packages: stable deterministic ordering and splice-based queue draining in SQLite persistence; Map-based bulk transaction updates and Set-based filtering in offline-transactions; and lazy entry materialization in useLiveQuery to avoid full collection traversal when only status is accessed. ChangesHot-Path Performance Optimizations
🎯 3 (Moderate) | ⏱️ ~25 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
More templates
@tanstack/angular-db
@tanstack/browser-db-sqlite-persistence
@tanstack/capacitor-db-sqlite-persistence
@tanstack/cloudflare-durable-objects-db-sqlite-persistence
@tanstack/db
@tanstack/db-ivm
@tanstack/db-sqlite-persistence-core
@tanstack/electric-db-collection
@tanstack/electron-db-sqlite-persistence
@tanstack/expo-db-sqlite-persistence
@tanstack/node-db-sqlite-persistence
@tanstack/offline-transactions
@tanstack/powersync-db-collection
@tanstack/query-db-collection
@tanstack/react-db
@tanstack/react-native-db-sqlite-persistence
@tanstack/rxdb-db-collection
@tanstack/solid-db
@tanstack/svelte-db
@tanstack/tauri-db-sqlite-persistence
@tanstack/trailbase-db-collection
@tanstack/vue-db
commit: |
|
Size Change: 0 B Total Size: 117 kB ℹ️ View Unchanged
|
|
Size Change: 0 B Total Size: 4.24 kB ℹ️ View Unchanged
|
Summary
Optimize hot paths across three packages to reduce unnecessary allocations and improve algorithmic complexity. The headline win is in
useLiveQuery— status-only consumers (isReady,isLoading,status) now skip entry materialization entirely, dropping from ~2.3ms to ~0.001ms on 50k-row collections.Root cause
Several hot paths used patterns with unnecessary overhead:
JSON.stringify—toStableSerializablecalledJSON.stringifyon both operands in every sort comparison, producing O(n log n) serializations instead of O(n)while/shift— repeatedshift()on a long array is O(n²) due to re-indexingfindIndexloops —KeyScheduler.updateTransactionsandloadPendingTransactionsused O(n×m) lookups where O(n+m) sufficeduseLiveQuerycalledArray.from(collection.entries())on every snapshot, even when the consumer only read status flagsApproach
@tanstack/db-sqlite-persistence-coreJSON.stringifyin the comparator.splice(0)queue flush replaceswhile/shift— detaches the queue atomically in O(n), then iterates. Items arriving during async processing accumulate in the original array for the next flush.@tanstack/offline-transactionsKeyScheduler.updateTransactions— builds a Map from the update array, then single-pass updates. O(n+m) instead of O(n×m).getEarliestRetryTimemoved toKeySchedulerwith a simple loop — avoids spreading intoMath.min(...array)which could stack-overflow on large arrays.loadPendingTransactionsfor removed transaction detection.Date.now()inresetRetryDelays— all transactions get the same timestamp.@tanstack/react-dbuseLiveQuery—stateanddataare now getters that defercollection.entries()until first access. Status flags (isReady,isLoading, etc.) are plain properties that never trigger materialization.Key invariants
stateanddataderive from a shared lazygetEntries()snapshot. If a consumer reads both, they see the same point-in-time view of the collection. ThesingleResultpath also usesgetEntries()for consistency, intentionally trading some getter-level speed for correctness.splice(0)detaches the queue before iterating. This is safe becauseisHydratingisfalseby the time flush runs, so no new items can be pushed during processing.KeyScheduler.updateTransactionsre-sorts bycreatedAtafter bulk updates to maintain execution order.Non-goals
dataorstateis accessed. Fixing that requires incremental update propagation from change payloads, which is a much larger effort.singleResultoptimization — an earlier version usedcollection.values().next().valueto avoid full materialization, but this could tear relative tostate. Correctness was chosen over the extra speed.Benchmarks
Local runs via
pnpm bench:perfon Node v24.11.1 / macOS arm64. Numbers are medians. Baseline numbers are from the pre-change implementation where available; the queue-drain benchmark compares the oldshift()strategy to the new cursor/splice-style strategy.useLiveQuerystatus-only, 50k rowsuseLiveQuerydata access, 50k rowsuseLiveQuerysingleResult, 50k rowsKeyScheduler.updateTransactionsall pending, 1kKeyScheduler.updateTransactionsall pending, 5kTransactionExecutor.loadPendingTransactionshalf filtered, 1kTransactionExecutor.loadPendingTransactionshalf filtered, 5kshift()→ cursor/splice-style)Additional after-change benchmark now covered by the harness:
KeyScheduler.getEarliestRetryTimewith setup, 5kMath.min(...array)argument-spread risk. No pre-change median was captured for this specific case.Verification
Files changed
packages/react-db/src/useLiveQuery.tsstate/datawith shared entries snapshotpackages/react-db/tests/useLiveQuery.test.tsxpackages/db-sqlite-persistence-core/src/persisted.tspackages/db-sqlite-persistence-core/tests/persisted.test.tspackages/offline-transactions/src/executor/KeyScheduler.tspackages/offline-transactions/src/executor/TransactionExecutor.tspackages/offline-transactions/tests/KeyScheduler.test.tspackage.jsonbench:perfscript🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Performance
Tests