MetaMask / MetaMask/metamask-mobile

Fix 3 per-row parameterized-selector cache misses in `TokenListItem` (fresh object-literal args)

Open
#31,492 1 comment 0 reactions 0 assignees View on GitHub
area-performance Sev1-high Sev2 ta-needs-engineer-escalation ta-triaged team-assets
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.