MetaMask / MetaMask/metamask-mobile
Defer MainNavigator's 130 eager screen imports loaded behind the wallet home
- 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
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