MetaMask / MetaMask/metamask-extension

Onboarding: intermittent full-screen ChunkLoadError flash on fresh install (React.lazy has no retry)

Open
#44,369 4 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

INVALID-ISSUE-TEMPLATE Sev2 size-S ta-ai-fixable ta-triaged team-extension-platform
Dominant language
TypeScript
Stars
13.2k
Forks
5.6k
Avg merge
2d 5h
Merged PRs (30d)
451

Description

## Describe the bug

loom video of the flash:
https://www.loom.com/share/00bf246ae60d46d3b8e335efd56ba978

Image

On a **fresh install**, opening the onboarding tab (`chrome-extension:///home.html#/onboarding/welcome`) can briefly flash the full-screen **"MetaMask encountered an error"** page before the UI recovers on its own.

The error is a webpack `ChunkLoadError`:

```
Message: Loading chunk 9532 failed. (error: chrome-extension:///9532.js)
Code: ChunkLoadError
Stack:
ChunkLoadError
at __webpack_require__.f.j (runtime.js)
at Array.reduce ()
at __webpack_require__.e (runtime.js)
...
```

## Steps to reproduce

1. Load an unpacked / freshly-installed build of the extension.
2. Let the onboarding tab open automatically on first install (`#/onboarding/welcome`).
3. Observe the error page flash for a moment, then the welcome screen renders normally.

It reproduces intermittently — it depends on winning/losing a startup race (see below), so it's most reliable on a cold install or a slow machine.

## Root cause

Routes are code-split and lazily loaded through `mmLazy`, which is a thin wrapper around `React.lazy` **with no retry logic**:

- `ui/pages/routes/routes.component.tsx` — `const OnboardingFlow = mmLazy(() => import('../onboarding-flow/index.ts'));`
- `ui/helpers/utils/mm-lazy.ts` — `mmLazy` → `React.lazy(async () => { const importedModule = await fn(); ... })`

The hash router renders a full-screen error page whenever a route fails to render:

- `ui/pages/index.js` — `createHashRouter([{ element: , errorElement: , children: routeConfig }])`, where `RouteErrorBoundary` renders the `ErrorPage` shown in the screenshot.

On a fresh MV3 install, the onboarding tab opens immediately while the service worker / extension resources are still warming up. The first `import()` of the onboarding route chunk (`9532.js`) loses that race and rejects. Because `React.lazy` does not retry, the rejection bubbles up to the router's `errorElement`, rendering the full-screen crash page.

Webpack does **not** cache failed chunk loads (it clears the entry in `installedChunks` on error), so the next re-render / navigation re-fetches successfully and the UI renders normally — which is why the error only *flashes*.

The same no-retry pattern is also used for the two Rive animations on the welcome page, via plain `React.lazy`:

- `ui/pages/onboarding-flow/welcome/welcome.tsx` — `MetaMaskWordMarkAnimation` and `FoxAppearAnimation`.

## Impact

- **Functional severity is low** — the UI self-recovers and there is no wallet/fund/data risk (the reassurance banner is accurate).
- **But** every occurrence is reported to Sentry via `captureException` (`ui/pages/index.js`), adding error-monitoring noise on cold starts, and it's a poor first-run impression.

## Proposed fix

Add retry-with-backoff to the dynamic `import()` at its single choke point in `mmLazy`, so transient chunk-fetch failures are retried before surfacing as a route crash:

```ts
// ui/helpers/utils/mm-lazy.ts
async function importWithRetry(
fn: () => Promise,
retries = 3,
delayMs = 250,
): Promise {
try {
return await fn();
} catch (err) {
if (retries <= 0) {
throw err;
}
await new Promise((resolve) => setTimeout(resolve, delayMs));
return importWithRetry(fn, retries - 1, delayMs * 2);
}
}

// then in mmLazy: `const importedModule = await importWithRetry(fn);`
```

Because all route chunks flow through `mmLazy`, this single change covers the whole router. The two `React.lazy` animation imports in `welcome.tsx` should be migrated to `mmLazy` (or wrapped identically) so they benefit as well.

Optionally, as a complement (not a substitute) to cut monitoring noise, `ChunkLoadError` could be filtered from Sentry via `ignoreErrors`/`beforeSend` in `app/scripts/lib/setupSentry.js` — but the retry is what actually removes the visible flash.

## Environment

- Discovered on a fresh wallet during first-run onboarding.
- Manifest V3 (Chrome).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with ui/helpers/utils/mm-lazy.ts and the route setup in ui/pages/routes/routes.component.tsx and ui/pages/index.js to trace how failed chunks reach RouteErrorBoundary. Then inspect the plain React.lazy imports in ui/pages/onboarding-flow/welcome/welcome.tsx. Done means transient chunk failures retry before the error page appears, including the welcome animations, while preserving the existing route behavior.

Written by the indexing model from the issue text.

Assessment

Tech stack
react, typescript, webpack
Domain
frontend
Issue type
Bug
Difficulty
3/5
Estimated time
1-2 days
Activity status
Quiet
Clarity
Clearly specified
Newbie friendliness
74/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.