MetaMask / MetaMask/metamask-mobile
Stabilize `selectEarnTokens` and `selectBridgeHistoryForAccount` (allocating result functions on hot paths)
- Dominant language
- TypeScript
- Stars
- 3k
- Forks
- 1.7k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 669
Description
> **Performance audit finding** · Severity: **Medium** · Effort: Small-Medium · Fix risk: Simple · Test safety net: Partial
> Owner: Earn / Bridge CODEOWNERS
> Files: `app/selectors/earnController/earn/index.ts:122`, `app/selectors/bridgeStatusController/index.ts:14`
### What is this about?
A per-selector triage of the ten highest-subscription components (51 selector definitions read; ruled-out verdicts in this issue's edit history and #31493) left two selectors standing — both allocate heavily on hot, always-visible paths:
1. **`selectEarnTokens`** (`app/selectors/earnController/earn/index.ts:122`) — a `createDeepEqualSelector` that rebuilds a large nested structure (earn tokens × markets × chains via `.reduce`) on every recompute, and deep-compares its sizable inputs on **every check**. Consumed by both `WalletActions` and `TradeWalletActions`, which sit on the primary action surfaces.
2. **`selectBridgeHistoryForAccount`** (`app/selectors/bridgeStatusController/index.ts:14`) — a plain `createSelector` whose result function allocates a fresh object via `.reduce` per recompute, handing consumers a new reference whenever the upstream recomputes even if the history is unchanged.
**Why it matters**
Both costs scale with power-user data (token count, bridge history length) and run at the batched-flush cadence for as long as their screens are mounted. These are the *confirmed* offenders out of 80+ candidate reads — the rest were verified stable/cheap and ruled out.
### Scenario
N/A — see Technical Details.
### Design
N/A — internal performance change; no UI/design impact.
### Technical Details
For `selectEarnTokens`: narrow the inputs to the slices that actually change, split the mega-aggregation into composed memoized layers so a price tick doesn't rebuild the world, and verify whether the deep-equal input compare is still needed once inputs are narrow (see the reference-stability contract test in the `mms-performance` skill's `mm-selector-cascade`). For `selectBridgeHistoryForAccount`: stabilize the output with a `resultEqualityCheck` or narrow inputs so recomputes track actual history changes.
### Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
### Acceptance Criteria
- Both selectors return the same reference across unrelated flushes with unchanged data (assert `toBe` across two equal-content states).
- Profiler on a power-user profile: `WalletActions`/`TradeWalletActions` and bridge history consumers stop re-rendering on unrelated dispatches.
### References
- Files: `app/selectors/earnController/earn/index.ts:122`, `app/selectors/bridgeStatusController/index.ts:14`
- Related: #31499 (`selectLocalTransactions`, split out as dedicated), #31492 (per-row parameterized misses), #31493 (Wallet home ruled out)
- Source: `mms-performance` per-selector triage (MetaMask/skills#49)
- Status: **UNVALIDATED** (static evidence; needs profiler confirmation)
Contributor guide
Research direction
Read app/selectors/earnController/earn/index.ts:122 and app/selectors/bridgeStatusController/index.ts:14, then review the reference-stability contract in the mms-performance skill's mm-selector-cascade. Preserve selector behavior while ensuring both return the same reference across unrelated flushes with unchanged data; confirm the WalletActions, TradeWalletActions, and bridge history consumers stop re-rendering on unrelated dispatches.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- frontend, performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 55/100