MetaMask / MetaMask/metamask-mobile
Stop deep-comparing the full transaction history per check in `selectLocalTransactions`
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3k
- Forks
- 1.7k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 669
Description
> **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**
1. 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`.
2. 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.
3. 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
- Files: `app/selectors/transactionController.ts:247` (and the upstream `selectNonReplacedTransactions` / sorted-transactions chain)
- Related (already filed): #31354, #31355
- Source: `mms-performance` per-selector triage (MetaMask/skills#49, `mm-selector-cascade` — deep-equal selectors pay O(input) per check)
- Status: **UNVALIDATED** (static evidence; needs profiler confirmation)
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in app/selectors/transactionController.ts around line 247, then trace selectNonReplacedTransactions and the sorted-transactions chain to verify their reference-stability assumptions. Run the transactions-view performance test and profile a roughly 2,000-transaction activity view during balance-poll flushes; done means recomputation is limited to actual transaction changes without full-array deep comparisons on each check.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100