MetaMask / MetaMask/metamask-mobile

Stop deep-comparing the full transaction history per check in `selectLocalTransactions`

Open
#31,499 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area-performance Sev1-high Sev2 ta-needs-engineer-escalation ta-triaged team-transactions
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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.