MetaMask / MetaMask/metamask-mobile

Defer MainNavigator's 130 eager screen imports loaded behind the wallet home

Open
#31,359 1 comment 0 reactions 0 assignees View on GitHub
area-performance Sev2 ta-needs-engineer-escalation 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: **High** · Effort: Hard · Fix risk: Risky · Test safety net: Uncovered
> Owner: `@MetaMask/mobile-platform (suggested)`
> File: `app/components/Nav/Main/MainNavigator.js:7`

### What is this about?

`app/components/Nav/Main/MainNavigator.js` statically imports **130 modules** at the top of the file — the entire post-login surface area: Browser, all Settings sub-screens, Ramp/Deposit/Bridge/Perps/Predict/Earn/Stake/Money/Card route stacks, Rewards navigator, NFT details, Snaps settings, dev-only screens (`AesCryptoTestForm`, `FeatureFlagOverride`, `SampleFeature`), etc. `Main` is registered as a `Stack.Screen` in `AppFlow` (`app/components/Nav/App/App.tsx:990`) and `MainNavigator` is imported eagerly by `app/components/Nav/Main/index.js:38`, so all 130 modules are evaluated during initial JS bundle evaluation as part of the startup graph.

**Why it matters**

On a cold start the user sees Login then the wallet `HomeTabs`. Only a handful of these 130 modules are needed for first paint (`Wallet`, `TabBar`, the `HomeTabs` shell). The remaining feature stacks (Ramp, Bridge, Perps, Predict, Card, Settings, Snaps, dev tooling) are evaluated up front but reached only on explicit navigation, adding avoidable JS-thread work to TTI. `@react-navigation/bottom-tabs` already lazy-*mounts* inactive tabs at runtime, but lazy mounting does nothing about the **module-evaluation** cost incurred by these static imports.

### 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:7-159` — 130 top-level imports, e.g.:
```js
import Browser from '../../Views/Browser';
import RampRoutes from '../../UI/Ramp/Aggregator/routes';
import { PerpsScreenStack, PerpsModalStackWithErrorGate, ... } from '../../UI/Perps';
import { PredictScreenStack, PredictModalStack, ... } from '../../UI/Predict';
import { AesCryptoTestForm } from '../../Views/AesCryptoTestForm'; // dev/test only
import SampleFeature from '../../../features/SampleFeature/...'; // sample-feature only
```
`app/components/Nav/Main/index.js:38` (`import MainNavigator from './MainNavigator';`) and `app/components/Nav/App/App.tsx:990` (``) place this on the cold-start path.

**Fix**

Lazily load the heavy non-home feature stacks via `React.lazy(() => import('...'))` + `` (e.g. the Ramp/Bridge/Perps/Predict/Earn/Stake/Money/Card route stacks, Settings sub-tree, Snaps settings). Gate dev/test-only screens (`AesCryptoTestForm`, `FeatureFlagOverride`, `SampleFeature`) behind the existing `isTest`/build-type flags so they are dynamically imported only when needed. Keep the `HomeTabs` shell (`Wallet`, `TabBar`) statically imported.

### Threat Modeling Framework

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

### Acceptance Criteria

- 1. Profile cold start with the Hermes sampling profiler before/after; confirm the MainNavigator module-evaluation chunk shrinks.
2. Compare `TraceName.UIStartup` span durations across builds.
3. Smoke-test each lazily-loaded tab/stack mounts correctly on first navigation (no missing screens, no `Suspense` fallback jank), especially Ramp, Bridge, Perps, Predict, Settings.

### References

- File: `app/components/Nav/Main/MainNavigator.js:7`
- Source: MetaMask Mobile performance audit — finding `startup-mainnavigator-eager-screen-imports`
- Owner (CODEOWNERS / best-effort): @MetaMask/mobile-platform (suggested)
- Status: **UNVALIDATED**

Contributor guide

Open the contributing guide

Research direction

Start with app/components/Nav/Main/MainNavigator.js:7-159, then trace its eager import through app/components/Nav/Main/index.js:38 and app/components/Nav/App/App.tsx:990. Profile cold start with the Hermes sampling profiler and compare the TraceName.UIStartup spans before changing the navigation loading boundaries. Done means the non-home screens load on first navigation, dev-only screens remain gated, and the listed feature stacks still mount without fallback jank.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, react-native, typescript
Domain
frontend, mobile, performance
Issue type
Refactor
Difficulty
5/5
Estimated time
Over a week
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.