MetaMask / MetaMask/metamask-mobile

Memoize useWatchedNFTNames per-request flatMap/find scan and returned array

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

Nobody has claimed this yet.

area-performance Sev3 size-S ta-triaged team-confirmations team-mobile-platform
Dominant language
TypeScript
Stars
3k
Forks
1.7k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

> **Performance audit finding** · Severity: **Medium** · Effort: Easy · Fix risk: Simple · Test safety net: Covered (app/components/hooks/DisplayName/useWatchedNFTNames.test.ts)
> Owner: `@MetaMask/confirmations (suggested)`
> File: `app/components/hooks/DisplayName/useWatchedNFTNames.ts:12`

### What is this about?

`useWatchedNFTNames` returns `requests.map(...)` with **no `useMemo`**, and for every request it re-derives `Object.keys(nftContractsByChainIdByAccount)`, builds a fresh `flatMap` of all NFT contracts for the chain across all accounts, then `.find()`s the match. This is O(requests × accounts × contracts) recomputed on every render, returning a fresh array each render.
```ts
return requests.map(({ type, value, variation }) => {
...
const accounts = Object.keys(nftContractsByChainIdByAccount); // per request, per render
const chainNfts = accounts.flatMap(
(account) => nftContractsByChainIdByAccount[account]?.[chainId] ?? [],
); // rebuilt per request, per render
const watchedNft = chainNfts.find(
(nft) => nft.address.toLowerCase() === contractAddress,
);
return watchedNft?.name ?? null;
});
```

**Why it matters**

This sub-hook is aggregated by `useDisplayNames`, which runs once per rendered name/address (confirmation screens, simulation rows, transaction lists, frequently per virtualized row). For wallets with many accounts and many watched NFTs, the per-request `flatMap` over all accounts' contracts is expensive and is repeated on every render. The fresh return array also defeats downstream memoization (compounding the wave-1 `useDisplayNames` issue).

### Scenario

N/A — see Technical Details.

### Design

N/A — internal performance change; no UI/design impact.

### Technical Details

**Evidence**

`app/components/hooks/DisplayName/useWatchedNFTNames.ts:12-30` — the whole body is an unmemoized `requests.map(...)`; `Object.keys(...)` (line 19) and the `flatMap` (line 21) execute inside the per-request callback on every render.

**Fix**

- Precompute a per-chain address→name index once with `useMemo` keyed on `[nftContractsByChainIdByAccount]` (flatten all accounts' contracts into `Map>`), then do O(1) lookups per request.
- Wrap the final `requests.map(...)` in `useMemo` keyed on `[requests, index]` so the returned array is stable.

### Threat Modeling Framework

N/A — performance-only change; behavior is preserved, no new data flow / trust boundary / attack surface.

### Acceptance Criteria

- - Run `yarn jest app/components/hooks/DisplayName/useWatchedNFTNames.test.ts`.
- Add a test rendering twice with identical `requests` and unchanged Redux state; assert returned array is `===` across renders.

### References

- File: `app/components/hooks/DisplayName/useWatchedNFTNames.ts:12`
- Source: MetaMask Mobile performance audit — finding `unstablehook-usewatchednftnames-on2-flatmap-every-render`
- Owner (CODEOWNERS / best-effort): @MetaMask/confirmations (suggested)
- 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 app/components/hooks/DisplayName/useWatchedNFTNames.ts and read the existing coverage in app/components/hooks/DisplayName/useWatchedNFTNames.test.ts. Run yarn jest app/components/hooks/DisplayName/useWatchedNFTNames.test.ts first, then verify the per-chain lookup work and returned request results are memoized without changing behavior. Done means the added repeated-render identity test passes.

Written by the indexing model from the issue text.

Assessment

Tech stack
react-native, typescript
Domain
performance
Issue type
Refactor
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
78/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.