MetaMask / MetaMask/metamask-mobile

Stop recreating selectIconSeedAddressByAccountGroupId selector inside useSelector every render

Open
#31,349 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

area-performance Sev3 size-S ta-triaged team-confirmations
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 when accountGroupId actually 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

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.