MetaMask / MetaMask/metamask-mobile
Stop recreating selectIconSeedAddressByAccountGroupId selector inside useSelector every render
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 3k
- Forks
- 1.7k
- Avg merge
- 1d 14h
- Merged PRs (30d)
- 669
Description
Performance audit finding · Severity: High · Effort: Easy · Fix risk: Simple · Test safety net: Partial
Owner:@MetaMask/confirmations
File:app/components/Views/confirmations/components/UI/recipient/recipient.tsx:57
What is this about?
selectIconSeedAddressByAccountGroupId is a selector factory — calling it
returns a fresh createDeepEqualSelector instance with its own private memo cache.
In recipient.tsx (and RewardSettingsAccountGroup.tsx) the factory is invoked
inside the inline useSelector closure, so a brand-new selector is created on
every render. A newly created memoized selector always misses its cache, so each
render fully recomputes findInternalAccountByScope (walks the wallet/group tree
to find an EVM account) instead of reusing the memoized result.
Why it matters
Recipient is a per-row component rendered by recipient-list.tsx, which renders
the whole recipient set with data.map(...) (no virtualization). Every recipient
row creates and runs a fresh tree-walking selector on every render of the send
recipient screen — O(rows × tree work) with zero memo benefit. The correct usage
already exists elsewhere (AccountCell, OndoPortfolio, SRPListItem,
AccountGroupDetails) where the factory is wrapped in useMemo([groupId]); these
inline call sites regressed that pattern.
Scenario
N/A — see Technical Details.
Design
N/A — internal performance change; no UI/design impact.
Technical Details
Evidence
app/components/Views/confirmations/components/UI/recipient/recipient.tsx:54
const accountAvatarSeedAddress = useSelector((state: RootState) => {
if (!recipient.accountGroupId) return recipient.address;
try {
const selector = selectIconSeedAddressByAccountGroupId(
recipient.accountGroupId, // new selector instance every render → cache miss
);
return selector(state);
} catch {
return recipient.address;
}
});
Same anti-pattern at app/components/UI/Rewards/components/Settings/RewardSettingsAccountGroup.tsx:55
and app/components/UI/Rewards/components/EndOfSeasonClaimBottomSheet/EndOfSeasonClaimBottomSheet.tsx:135.
Fix
Memoize the per-group selector instance and pass it directly to useSelector, matching
the existing correct call sites:
const seedSelector = useMemo(
() =>
recipient.accountGroupId
? selectIconSeedAddressByAccountGroupId(recipient.accountGroupId)
: null,
[recipient.accountGroupId],
);
const seed = useSelector((state: RootState) =>
seedSelector ? seedSelector(state) : undefined,
);
const accountAvatarSeedAddress = seed ?? recipient.address;
Threat Modeling Framework
N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.
Acceptance Criteria
-
yarn jest app/components/Views/confirmations/components/UI/recipient/recipient.test.tsx.
- Render the recipient screen with many recipients and profile re-renders;
confirm the tree-walk only runs whenaccountGroupIdactually changes.
References
- File:
app/components/Views/confirmations/components/UI/recipient/recipient.tsx:57 - Source: MetaMask Mobile performance audit — finding
selector-icon-seed-address-factory-recreated-inline - Owner (CODEOWNERS / best-effort): @MetaMask/confirmations
- Status: UNVALIDATED
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with the inline useSelector calls in app/components/Views/confirmations/components/UI/recipient/recipient.tsx, RewardSettingsAccountGroup.tsx, and EndOfSeasonClaimBottomSheet.tsx, then compare the existing useMemo pattern in AccountCell or OndoPortfolio. Run app/components/Views/confirmations/components/UI/recipient/recipient.test.tsx and verify that selector tree-walks are reused across re-renders and only change when accountGroupId changes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- frontend, performance
- Issue type
- Refactor
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 72/100