MetaMask / MetaMask/metamask-mobile
Fix 3 per-row parameterized-selector cache misses in `TokenListItem` (fresh object-literal args)
- 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: Simple · Test safety net: Partial
> Owner: `@MetaMask/metamask-assets (suggested)`
> File: `app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.tsx`
### What is this about?
Per-selector triage of the 12 `useSelector` reads in `TokenListItem`: **9 ruled out** (memoized selectors returning primitives/booleans or stable controller-state refs). The remaining **3 are per-row parameterized selectors whose caches bust on every call**:
1. `selectAsset(state, { address, chainId, isStaked })` (`TokenListItem.tsx:179`) — called with a **fresh object literal argument per render per row**, which defeats any memoizer: a single-entry cache misses on the changed arg, and `weakMapMemoize` misses because every object literal is a new WeakMap key.
2. `selectNativeCurrencyByChainId(state, chainId)` (`TokenListItem.tsx:224`) — varying `chainId` across rows busts a single-entry cache row-by-row.
3. `selectIsStakeableToken(state, asset)` (`TokenListItem.tsx:490`) — takes the `asset` **object** as the argument; identity churns whenever `selectAsset` recomputes (which is every render, per item 1), compounding the miss rate.
**Why it matters**
The token list is the primary power-user surface; the misses multiply by visible rows on every render, and `selectAsset` recomputing per row hands each row a fresh `asset` ref, defeating the row's `React.memo`.
### Scenario
N/A — see Technical Details.
### Design
N/A — internal performance change; no UI/design impact.
### Technical Details
**Fix**
For (1): stabilize the argument — memoize the `{ address, chainId, isStaked }` key object per row (`useMemo` on the three primitives), or re-key the selector on the three primitives directly; pair with `weakMapMemoize` or a per-row factory instance. For (2): per-row factory instance via `useMemo`, or a chainId-keyed lookup-map selector. For (3): key on `asset.address`/`asset.chainId` primitives instead of the object. The codebase already has both correct tools in use: `weakMapMemoize` (`selectNetworkConfigurationByChainId`) and `useMemo`-instantiated factory selectors (`AccountGroupBalance.tsx:91-98`).
### Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
### Acceptance Criteria
- The 3 selectors hit their caches across renders with unchanged data (assert result `toBe` across two calls; recomputation count ~0 while scrolling with stable data).
- Profiler: rows with unchanged data do not re-render during a balance-poll flush.
- Reassure perf-test on the token list locks the win in CI.
### References
- Files: `app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.tsx:179,224,490`
- Source: `mms-performance` sweep + per-selector triage (MetaMask/skills#49, `mm-state-normalization`)
- Status: **UNVALIDATED** (static evidence; needs profiler confirmation)
Contributor guide
Research direction
Start in app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.tsx at lines 179, 224, and 490, then compare the factory-selector pattern in AccountGroupBalance.tsx:91-98 and the weakMapMemoize usage in selectNetworkConfigurationByChainId. Validate unchanged-data cache hits and row identity across renders, then run the token-list Reassure performance test and confirm the profiler shows no unnecessary row re-renders.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- react-native, typescript
- Domain
- frontend, mobile, performance
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100