MetaMask / MetaMask/metamask-mobile
BrowserTab inline selector rebuilds permitted-accounts array with .map on every dispatch
- 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
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