MetaMask / MetaMask/metamask-mobile

BrowserTab inline selector rebuilds permitted-accounts array with .map on every dispatch

Open
#31,346 1 comment 0 reactions 0 assignees View on GitHub
area-performance Sev2 size-S ta-triaged 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: Partial
> Owner: `@MetaMask/mobile-platform`
> File: `app/components/Views/BrowserTab/BrowserTab.tsx:215`

### What is this about?

`permittedCaipAccountAddressesList` is computed by an inline (non-memoized) `useSelector` whose callback calls `selectPermissionControllerState(state)`, re-parses the URL with `new URLParse(...)`, runs `sortMultichainAccountsByLastSelected(...)`, and then `.map()`s each account through `parseCaipAccountId(...)` — producing a fresh array. The sibling `permittedEvmAccountsList` does similar work. Both rely solely on the `isEqual` 2nd arg to suppress re-renders.

**Why it matters**

Because the callback is inline and not a memoized selector, the entire chain (URL parse + sort + per-account `parseCaipAccountId` + `.map`) runs on EVERY Redux dispatch while a browser tab is mounted, regardless of whether permission state changed. With multiple browser tabs open, each tab re-runs this on every dispatch. The `isEqual` only prevents the downstream re-render; it does not prevent the wasted CPU work in the selector itself, and `isEqual` deep comparison of the resulting array is itself O(n) on every dispatch.

### Scenario

N/A — see Technical Details.

### Design

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

### Technical Details

**Evidence**

`app/components/Views/BrowserTab/BrowserTab.tsx:215`
```ts
const permittedCaipAccountAddressesList = useSelector(
(state: RootState) => {
const permissionsControllerState = selectPermissionControllerState(state);
const hostname = new URLParse(resolvedUrlRef.current).origin;
const permittedAccountIds = getPermittedCaipAccountIdsByHostname(permissionsControllerState, hostname);
const sortedPermittedAccountIds = sortMultichainAccountsByLastSelected(permittedAccountIds);
const permittedAccountAddresses = sortedPermittedAccountIds.map((accountId) => {
const { address } = parseCaipAccountId(accountId);
return address;
});
return permittedAccountAddresses;
},
isEqual,
);
```

**Fix**

Move this into a memoized parametrized selector keyed on `(permissionControllerState, hostname)` using `createSelector`, e.g. `selectPermittedCaipAddressesByHostname(state, hostname)`, so the sort + `.map` only run when permission state or hostname change. Pass `hostname` as the 2nd reselect input. Then `useSelector(selectPermittedCaipAddressesByHostname.bind(null, hostname))` (or via `useMemo`-stable selector) and drop the `isEqual` deep compare. Same treatment for `permittedEvmAccountsList`.

### Threat Modeling Framework

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

### Acceptance Criteria

- Add a unit test on the new selector asserting a stable array reference across unrelated state changes and recompute only on permission/hostname change. BrowserTab has existing render tests (Partial) but they do not assert selector recompute counts.

### References

- File: `app/components/Views/BrowserTab/BrowserTab.tsx:215`
- Source: MetaMask Mobile performance audit — finding `selector-browsertab-permitted-caip-accounts-inline-map`
- Owner (CODEOWNERS / best-effort): @MetaMask/mobile-platform
- Status: **UNVALIDATED**

Contributor guide

Open the contributing guide

Research direction

Start in app/components/Views/BrowserTab/BrowserTab.tsx around line 215 and inspect the inline selectors for permitted CAIP and EVM accounts, then review existing BrowserTab render tests. Add a unit test for the new selector showing a stable array reference across unrelated state changes and recomputation when permission state or hostname changes.

Written by the indexing model from the issue text.

Assessment

Tech stack
redux, typescript
Domain
frontend, performance, testing-qa
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.