MetaMask / MetaMask/metamask-mobile

Code-split the AppFlow navigator's 150+ eager screen imports off the boot path

Open
#31,358 1 comment 0 reactions 0 assignees View on GitHub
area-performance needs-engineer Sev2 size-L 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/App/App.tsx:7`

### What is this about?

`app/components/Nav/App/App.tsx` statically imports **152 modules** at the top of the file — essentially every screen, sheet, and modal the app can ever show (Login, Onboarding, all MultichainAccounts sheets, Ledger/QR hardware flows, Ramp bootstrap, Perps/Predict toast hooks, confirmation modals, etc.). `App` is rendered by `Root` (`app/components/Views/Root/index.tsx:5`), which is the component registered in `AppRegistry.registerComponent` (`index.js:124`). Because every `import` is a top-level static import, Metro/Hermes must evaluate every one of those module factories (and their transitive dependency graphs) during the **initial JS bundle evaluation**, before the first frame can be painted. No `React.lazy`/dynamic `import()` is used anywhere in the navigation tree (a repo-wide search for `React.lazy`/`lazy(` in `app/` returns zero hits).

**Why it matters**

Module-factory execution for ~150 screens (plus their deep imports) runs synchronously on the JS thread at startup and directly inflates time-to-interactive. The vast majority of these screens are never shown during a cold start (the user lands on Login or the wallet home); paying their evaluation cost up front is wasted boot work. This is the single largest deferrable chunk on the JS startup path that does not require touching the Engine.

### Scenario

N/A — see Technical Details.

### Design

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

### Technical Details

**Evidence**

`app/components/Nav/App/App.tsx:1-168` — 152 top-level `import` statements feeding one `createStackNavigator()`:
```ts
const Stack = createStackNavigator();
// ...AppFlow registers ~70 entries, each referencing an eagerly-imported component

```
`index.js:124` registers `Root`, which renders `` (`app/components/Views/Root/index.tsx:92`), so all of `App.tsx`'s imports are on the cold-start evaluation path.

**Fix**

Convert rarely-used screen components to lazily-loaded modules so their factories are not evaluated at boot: wrap them with `React.lazy(() => import('...'))` and render inside a `` boundary, or extract the modal/sheet-only `Stack.Screen` groups (e.g. `RootModalFlow`, hardware-wallet flows, MultichainAccounts sheets) into separate files imported via dynamic `import()`. Keep only the cold-start-critical screens (`FoxLoader`, `Login`, `Main`) statically imported. Measure with the existing `TraceName.UIStartup` / `NavInit` spans before/after.

### Threat Modeling Framework

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

### Acceptance Criteria

- 1. Add a `console.time`/Sentry span around bundle evaluation, or use the existing `getUIStartupSpan()` traces.
2. Profile cold start with the Hermes sampling profiler; confirm reduced module-evaluation time for the navigation chunk.
3. Smoke-test every lazily-loaded route still mounts (no `Suspense` fallback flashes on first navigation) across onboarding, hardware wallet, and modal flows.

### References

- File: `app/components/Nav/App/App.tsx:7`
- Source: MetaMask Mobile performance audit — finding `startup-appflow-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/App/App.tsx, then trace Root in app/components/Views/Root/index.tsx and registration in index.js:124. Review the existing TraceName.UIStartup, NavInit, and getUIStartupSpan() instrumentation, then profile cold start with Hermes. Done means reduced navigation module-evaluation time while onboarding, hardware-wallet, and modal routes still mount without fallback flashes.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.