Performance audit finding · Severity: High · Effort: Medium · Fix risk: Moderate · Test safety net: Partial
Owner: CODEOWNERS best-effort (transactions)
File: app/selectors/transactionController.ts:247
What is this about?
selectLocalTransactions is a createDeepEqualSelector over a pipeline that allocates aggressively upstream: selectNonReplacedTransactions spreads and sorts the merged transaction + pending-smart-transaction arrays on every recompute (transactionController.ts:240 area), and the result function filters the full list again. Because the wrapper is deep-equal, its inputs are deep-compared on every check — for a power-user transaction history (~2,000 entries) that is an O(n) structural comparison of large transaction objects per consumer per flush, on top of the spread+sort chains re-running on every TransactionController state change.
Why it matters
The transaction list is a primary power-user surface; this pipeline runs its compares a few times per second (250ms batched flush) for as long as the activity view is mounted, with cost proportional to history size. None of the 2026-06-09 audit-run issues cover it (#31354/#31355 cover the adjacent Map/Set allocations only).
Scenario
N/A — see Technical Details.
Design
N/A — internal performance change; no UI/design impact.
Technical Details
Fix direction
- Establish the reference-stability contract of the inputs:
TransactionController state arrives via the engine slice (per-controller key replace, Immer structural sharing), so the raw transactions array reference changes only when transactions actually change. If so, the deep-equal wrapper is paying for stability the store already provides — narrow the inputs and downgrade toward plain createSelector.
- Move the dedupe/sort/filter into one memoized step keyed on the raw transactions input rather than re-running per layer, so the chain recomputes once per actual data change.
- If consumers need stability across genuinely-churning inputs, prefer a
resultEqualityCheck on the (much smaller) output over deep-comparing the full input arrays.
Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
Acceptance Criteria
- Recompute count ~1 per actual transaction change (not per flush); no full-array deep compares per check on the hot path.
- Profiler on a power-user profile (~2,000 tx): activity view render time and JS-thread time during balance-poll flushes drop measurably.
- Reassure perf-test on the transactions view locks the win in CI.
References
What is this about?
selectLocalTransactionsis acreateDeepEqualSelectorover a pipeline that allocates aggressively upstream:selectNonReplacedTransactionsspreads and sorts the merged transaction + pending-smart-transaction arrays on every recompute (transactionController.ts:240area), and the result function filters the full list again. Because the wrapper is deep-equal, its inputs are deep-compared on every check — for a power-user transaction history (~2,000 entries) that is an O(n) structural comparison of large transaction objects per consumer per flush, on top of the spread+sort chains re-running on everyTransactionControllerstate change.Why it matters
The transaction list is a primary power-user surface; this pipeline runs its compares a few times per second (250ms batched flush) for as long as the activity view is mounted, with cost proportional to history size. None of the 2026-06-09 audit-run issues cover it (#31354/#31355 cover the adjacent
Map/Setallocations only).Scenario
N/A — see Technical Details.
Design
N/A — internal performance change; no UI/design impact.
Technical Details
Fix direction
TransactionControllerstate arrives via the engine slice (per-controller key replace, Immer structural sharing), so the raw transactions array reference changes only when transactions actually change. If so, the deep-equal wrapper is paying for stability the store already provides — narrow the inputs and downgrade toward plaincreateSelector.resultEqualityCheckon the (much smaller) output over deep-comparing the full input arrays.Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
Acceptance Criteria
References
app/selectors/transactionController.ts:247(and the upstreamselectNonReplacedTransactions/ sorted-transactions chain)mms-performanceper-selector triage (perf: Augmentmms-performancewith frontend learnings from the Extension performance audit skills#49,mm-selector-cascade— deep-equal selectors pay O(input) per check)