MetaMask / MetaMask/metamask-mobile
Memoize HomeTabs tab options and tabBar so browser-tab/account changes don't re-render the whole tab navigator
- 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
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