MetaMask / MetaMask/metamask-mobile

Memoize HomeTabs tab options and tabBar so browser-tab/account changes don't re-render the whole tab navigator

Open
#31,329 1 comment 0 reactions 0 assignees View on GitHub
area-performance Sev3 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: Uncovered
> Owner: `@MetaMask/mobile-platform (suggested)`
> File: `app/components/Nav/Main/MainNavigator.js:645`

### What is this about?

`HomeTabs` (the root bottom-tab navigator shown on every cold start) subscribes to four pieces of Redux state — `selectMoneyEnableMoneyAccountFlag`, `selectAccountsLength`, an inline `chainId` selector, and `state.browser.tabs.length` — and on **every** render rebuilds a fresh `options` object (`MainNavigator.js:662-761`) and a fresh `renderTabBar` closure (`:781-822`), which are then handed to every `Tab.Screen`'s `options`/`tabBar`. Because these are recreated as new object/function references on each render, react-navigation re-derives descriptors for the whole tab navigator subtree even when nothing relevant to the tabs changed.

**Why it matters**

`amountOfBrowserOpenTabs = useSelector((state) => state.browser.tabs.length)` (`:658-660`) makes `HomeTabs` re-render every time the user opens or closes a browser tab, and `selectAccountsLength` makes it re-render whenever accounts change. Each such render produces brand-new `options`/`tabBar` references for all tabs, forcing react-navigation to recompute tab descriptors and re-run the (non-memoized) `TabBar` (`app/component-library/components/Navigation/TabBar/TabBar.tsx:41`, no `React.memo`). The browser-tab count and account count are not used by the tab bar's visible UI, so this is wasted work on a hot, always-mounted shell.

### Scenario

N/A — see Technical Details.

### Design

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

### Technical Details

**Evidence**

`app/components/Nav/Main/MainNavigator.js:645-761`:
```js
const HomeTabs = () => {
const accountsLength = useSelector(selectAccountsLength);
const chainId = useSelector((state) => ChainId[selectProviderConfig(state).type]);
const amountOfBrowserOpenTabs = useSelector((state) => state.browser.tabs.length);
// ...
const options = { home: { /* recreated every render */ ... }, /* trade, browser, activity, money, rewards, trending, settings */ };
// ...
const renderTabBar = ({ state, descriptors, navigation }) => { /* recreated every render */ };
```

**Fix**

- Wrap `options` in `useMemo` keyed on the values that actually feed it, and wrap `renderTabBar` in `useCallback`.
- Move the analytics `number_of_accounts`/`number_of_open_tabs`/`chain_id` reads into the tab `callback` bodies via `store.getState()`/refs (or read them lazily) instead of subscribing at the navigator level, so `HomeTabs` no longer re-renders on every browser-tab open/close.
- Wrap `TabBar` in `React.memo`.

### Threat Modeling Framework

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

### Acceptance Criteria

- 1. Add a render counter (or `why-did-you-render`) to `HomeTabs` and `TabBar`; open/close browser tabs and confirm the tab navigator no longer re-renders.
2. Confirm the `WALLET_OPENED`/`BROWSER_OPENED` analytics still report correct `number_of_accounts`/`number_of_open_tabs`.

### References

- File: `app/components/Nav/Main/MainNavigator.js:645`
- Source: MetaMask Mobile performance audit — finding `nav-hometabs-options-rebuilt-every-render`
- Owner (CODEOWNERS / best-effort): @MetaMask/mobile-platform (suggested)
- Status: **UNVALIDATED**

Contributor guide

Open the contributing guide

Research direction

Start with HomeTabs in app/components/Nav/Main/MainNavigator.js:645-822, then read TabBar at app/component-library/components/Navigation/TabBar/TabBar.tsx:41. Trace the listed Redux subscriptions and analytics callbacks before choosing the memoization boundaries. Done means browser-tab changes no longer re-render the navigator or TabBar while WALLET_OPENED and BROWSER_OPENED retain correct counts; validate with a render counter or why-did-you-render.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, react-native
Domain
frontend, mobile, performance
Issue type
Refactor
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
55/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.