Skip to content

Reduce UI lag: YAOB throttle, debounce, dedup, and balance cache#709

Open
paullinator wants to merge 8 commits intomasterfrom
paul/fixUiLag
Open

Reduce UI lag: YAOB throttle, debounce, dedup, and balance cache#709
paullinator wants to merge 8 commits intomasterfrom
paul/fixUiLag

Conversation

@paullinator
Copy link
Member

@paullinator paullinator commented Feb 25, 2026

CHANGELOG

Does this branch warrant an entry to the CHANGELOG?

  • Yes
  • No

Dependencies

none

Description

Reduces RN JS thread starvation during wallet scene transitions by cutting redundant YAOB bridge crossings and Redux dispatches. Six targeted fixes:

  • Seed balanceMap from wallet cache: Initialize balanceMap from cached balances via a new ACCOUNT_CACHED_BALANCES_LOADED action, eliminating a wave of redundant onTokenBalanceChanged callbacks on startup. Adds an early-return guard to drop callbacks when the value matches the current Redux state.
  • Increase YAOB throttle 50ms → 200ms: Reduces peak bridge message frequency by 4x across all wallets.
  • Debounce update(walletApi) to 300ms: Collapses rapid-fire Redux state mutations into a single bridge crossing per wallet. Adds hasYaobVisibleChange to skip no-op updates for internal-only field changes.
  • Slow updateQueue to 1 item per 1000ms: Prevents the queue from flooding the JS thread during the post-login sync burst.
  • Gate CURRENCY_ENGINE_CHANGED_TXS dispatch: Only dispatches when compare() finds actual changed/created transactions.
  • Dedup balanceMap reducer: Returns the existing Map reference when the balance value is unchanged, preventing spurious watcher triggers.

Note

Medium Risk
Touches login sequencing, wallet/config objects, and disk-backed caching, which can affect startup behavior and wallet visibility if cache data is stale/invalid or delegation/polling misbehaves. Changes also alter update/dispatch timing, so regressions may show up as delayed UI refreshes or missed updates.

Overview
Adds a cache-first login path that loads a per-account walletCache.json to instantly expose lightweight EdgeCurrencyWallet/EdgeCurrencyConfig stubs (with delegation/polling to real engines), and merges these cached wallets into EdgeAccount.currencyWallets until engines finish loading.

Introduces wallet-cache persistence: a throttled cacheSaver pixie writes tokens/customTokens, wallet metadata, balances, enabled tokens, and otherMethods names; cached balances are dispatched via new ACCOUNT_CACHED_BALANCES_LOADED so wallet reducers seed balanceMap without a startup callback storm. Adds extensive end-to-end tests plus deterministic fake-plugin gating for engine load.

Reduces UI/bridge churn by increasing YAOB_THROTTLE_MS (50→200), debouncing wallet update(walletApi) with a YAOB-visible-field diff, deduping no-op balance/token/tx Redux dispatches, and slowing updateQueue cadence.

Written by Cursor Bugbot for commit 19b037e. This will update automatically on new commits. Configure here.

Add missing return in fake plugin getBalance for token balance.
Improve @ts-expect-error comment and log swap quote close errors.
Improve login performance by caching wallet state on a per account level in unencrypted JSON file and rehydrate before wallets load.
During login, 45% of balance-related YAOB bridge crossings were
redundant. The Redux balanceMap started empty despite cached balances
being available, treating every cached balance as "new".

(A) Initialize balanceMap directly from the wallet cache in the reducer
via a new ACCOUNT_CACHED_BALANCES_LOADED action dispatched before wallet
creation, so YAOB never observes an empty map.

(B) Early-return guard in onTokenBalanceChanged drops callbacks when the
balance matches the current Redux state, catching any duplicates the
cache initialization missed.

Removes the now-redundant initial parent-balance dispatch from the
wallet pixie since balanceMap is pre-populated.
At 50ms, YAOB sent up to 20 bridge messages per second per wallet.
With 168 wallets, the RN JS thread was overwhelmed with JSON parsing
and proxy updates. Increasing to 200ms batches more updates per message
and reduces interrupt frequency by 4x.
Every Redux state change triggered the pixie watcher to call
update(walletApi), sending a bridge message even when multiple changes
arrived within milliseconds. Per-wallet debouncing collapses rapid-fire
state mutations into a single bridge crossing.

Also adds hasYaobVisibleChange to skip no-op bridge messages caused by
Redux reference changes to internal-only fields.
The update queue processed 3 items per 500ms (6/sec), but with 168
wallets filling it, the queue peaked at 227 items and never drained.
Slowing individual processing while relying on deduplication keeps
the queue shallow.
The onTransactions callback dispatched CURRENCY_ENGINE_CHANGED_TXS
unconditionally, even when compare() filtered out all transactions as
unchanged. Each dispatch mutated wallet state, triggered the pixie
watcher, and sent a YAOB bridge message. Gating the dispatch eliminates
no-op bridge crossings.

Also guards onNewTokens against empty arrays.
The CURRENCY_ENGINE_CHANGED_BALANCE reducer always created a new Map
reference, marking the wallet as dirty and triggering a YAOB bridge
message even when the balance was identical. A same-value guard keeps
the existing reference, letting the watcher debounce skip the update.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is ON. A Cloud Agent has been kicked off to fix the reported issue.

update(walletApi)
}
}
}, 300)
Copy link

Choose a reason for hiding this comment

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

Debounce timer uses stale closure over walletState

Medium Severity

The setTimeout callback on line 476 captures walletState from the enclosing async function's local scope. This value is frozen to whatever state existed when the timer was first created. During the 300ms debounce window, subsequent updates set updatePending = true but the timer still holds the stale walletState. When the timer fires, hasYaobVisibleChange compares the stale snapshot (not the latest state) against lastUpdatedState, and lastUpdatedState is then set to the stale value. This causes lastUpdatedState to diverge from what yaob actually received, which can lead to skipped update(walletApi) calls in subsequent debounce windows. Using the outer-scope lastState variable (which always holds the most recent state) instead of the closure-captured walletState would fix this.

Fix in Cursor Fix in Web

@cursor
Copy link

cursor bot commented Feb 25, 2026

Bugbot Autofix prepared fixes for 1 of the 1 bugs found in the latest run.

  • ✅ Fixed: Debounce timer uses stale closure over walletState
    • Changed the setTimeout callback to use the outer-scope lastState variable instead of the closure-captured walletState, ensuring the timer uses the most recent state when it fires.

Create PR

Or push these changes by commenting:

@cursor push e705a0120b
Preview (e705a0120b)
diff --git a/src/core/currency/wallet/currency-wallet-pixie.ts b/src/core/currency/wallet/currency-wallet-pixie.ts
--- a/src/core/currency/wallet/currency-wallet-pixie.ts
+++ b/src/core/currency/wallet/currency-wallet-pixie.ts
@@ -473,8 +473,11 @@
             updateTimer = undefined
             if (updatePending) {
               updatePending = false
-              if (hasYaobVisibleChange(walletState, lastUpdatedState)) {
-                lastUpdatedState = walletState
+              if (
+                lastState != null &&
+                hasYaobVisibleChange(lastState, lastUpdatedState)
+              ) {
+                lastUpdatedState = lastState
                 update(walletApi)
               }
             }

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