Performance audit finding · Severity: Medium · 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:
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.
selectNativeCurrencyByChainId(state, chainId) (TokenListItem.tsx:224) — varying chainId across rows busts a single-entry cache row-by-row.
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
What is this about?
Per-selector triage of the 12
useSelectorreads inTokenListItem: 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: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, andweakMapMemoizemisses because every object literal is a new WeakMap key.selectNativeCurrencyByChainId(state, chainId)(TokenListItem.tsx:224) — varyingchainIdacross rows busts a single-entry cache row-by-row.selectIsStakeableToken(state, asset)(TokenListItem.tsx:490) — takes theassetobject as the argument; identity churns wheneverselectAssetrecomputes (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
selectAssetrecomputing per row hands each row a freshassetref, defeating the row'sReact.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 (useMemoon the three primitives), or re-key the selector on the three primitives directly; pair withweakMapMemoizeor a per-row factory instance. For (2): per-row factory instance viauseMemo, or a chainId-keyed lookup-map selector. For (3): key onasset.address/asset.chainIdprimitives instead of the object. The codebase already has both correct tools in use:weakMapMemoize(selectNetworkConfigurationByChainId) anduseMemo-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
toBeacross two calls; recomputation count ~0 while scrolling with stable data).References
app/components/UI/Tokens/TokenList/TokenListItem/TokenListItem.tsx:179,224,490mms-performancesweep + per-selector triage (perf: Augmentmms-performancewith frontend learnings from the Extension performance audit skills#49,mm-state-normalization)