Skip to content

Commit 75408fe

Browse files
authored
Merge pull request #19 from MetaMask/add-skill/mobile-performance
feat(performance): add mobile-performance skill for metamask-mobile
2 parents 66ff69d + d4cff54 commit 75408fe

45 files changed

Lines changed: 4995 additions & 0 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
---
2+
title: Analyze JS Bundle Size
3+
impact: CRITICAL
4+
tags: bundle, analysis, expo-atlas
5+
---
6+
7+
# Skill: Analyze JS Bundle Size
8+
9+
Use **Expo Atlas** to visualize what's in your JavaScript bundle. metamask-mobile
10+
is an Expo project (Metro bundler), and Atlas is the Expo-first-party analyzer —
11+
its hooks are built into the Expo CLI, so you enable it on the `expo export` you
12+
already run.
13+
14+
## Quick Command
15+
16+
```bash
17+
# One-time: add the Atlas viewer (Expo pins the SDK-compatible version)
18+
yarn expo install expo-atlas
19+
20+
# Emit bundle data, then open the treemap viewer
21+
EXPO_ATLAS=1 yarn expo export --platform ios
22+
yarn expo-atlas
23+
```
24+
25+
> Don't `npx expo-atlas``npx` fetches and runs an unpinned remote version on
26+
> every call. `yarn expo install` pins the SDK-compatible version once.
27+
28+
## When to Use
29+
30+
- JS bundle seems too large
31+
- Want to identify heavy dependencies
32+
- Investigating startup time issues
33+
- Before/after optimization comparison
34+
35+
> **Note**: Atlas produces a visual treemap (browser UI). Treemap analysis may
36+
> require exported reports, browser screenshots, or human review.
37+
38+
## Understanding Hermes Bytecode
39+
40+
Modern React Native (0.70+) uses Hermes bytecode, not raw JavaScript:
41+
42+
- Skips parsing at runtime
43+
- Still benefits from smaller bundles
44+
- Heavy imports still execute on startup
45+
46+
**Impact of bundle size:**
47+
48+
- Larger bytecode = longer download from store
49+
- More imports on init path = slower TTI
50+
51+
## Expo Atlas
52+
53+
The Atlas **data emission** is built into the Expo CLI (the `EXPO_ATLAS` env var
54+
on `expo export` / `expo start`); the `expo-atlas` package provides the
55+
**viewer**. Add it once with `yarn expo install expo-atlas`.
56+
57+
### Generate + view
58+
59+
```bash
60+
# Export with Atlas enabled
61+
EXPO_ATLAS=1 yarn expo export --platform ios
62+
63+
# Or while running the dev server
64+
EXPO_ATLAS=1 yarn expo start
65+
66+
# Open the treemap viewer (reads .expo/atlas.jsonl)
67+
yarn expo-atlas
68+
```
69+
70+
(The older env var name `EXPO_UNSTABLE_ATLAS=true` still works on some versions.)
71+
72+
![Expo Atlas Treemap](images/expo-atlas-treemap.png)
73+
74+
The treemap shows module sizes and dependencies — box area is proportional to
75+
size. Drill into `node_modules/` to find the heaviest packages (e.g.
76+
`react-native` core, renderer, `Animated`, `virtualized-lists`).
77+
78+
## What to Look For
79+
80+
### Red Flags
81+
82+
| Finding | Problem | Solution |
83+
|---------|---------|----------|
84+
| Entire library imported | Barrel exports | Use direct imports |
85+
| Duplicate packages | Multiple versions | Dedupe in `package.json` |
86+
| Dev dependencies in bundle | Incorrect imports | Check conditional imports |
87+
| Large polyfills | Unnecessary for Hermes | Remove (see [native-sdks-over-polyfills.md](./native-sdks-over-polyfills.md)) |
88+
| Moment.js with locales | Bloated date library | Switch to `dayjs` (already installed) |
89+
90+
### Common Offenders
91+
92+
- **Lodash full import**: use specific submodule imports (`import x from 'lodash/x'`)
93+
- **Moment.js**: replace with `dayjs` (already installed)
94+
- **Intl polyfills**: check Hermes API and method coverage before removing them
95+
- **AWS SDK**: import specific services only
96+
97+
## Code Examples
98+
99+
### Identify Barrel Import Impact
100+
101+
```tsx
102+
// BAD: Imports entire library through barrel
103+
import { format } from 'date-fns';
104+
// In bundle: all of date-fns loaded
105+
106+
// GOOD: Direct import
107+
import format from 'date-fns/format';
108+
// In bundle: only the format function
109+
```
110+
111+
## Comparing Bundles
112+
113+
Run Atlas before and after a change (or on two branches) and compare the
114+
treemaps — the heaviest boxes that grew/shrank show where the change landed.
115+
116+
## Related Skills
117+
118+
- [bundle-barrel-exports.md](./bundle-barrel-exports.md) - Fix barrel import issues
119+
- [bundle-tree-shaking.md](./bundle-tree-shaking.md) - Enable dead code elimination
120+
- [bundle-library-size.md](./bundle-library-size.md) - Check library sizes before adding
Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,248 @@
1+
---
2+
title: Avoid Barrel Exports
3+
impact: CRITICAL
4+
tags: bundle, imports, barrel, tree-shaking
5+
---
6+
7+
# Skill: Avoid Barrel Exports
8+
9+
Refactor barrel imports (index files) to reduce bundle size and improve startup time.
10+
11+
## Quick Pattern
12+
13+
**Incorrect:**
14+
15+
```tsx
16+
import { Button } from './components';
17+
// Loads ALL exports from components/index.ts
18+
```
19+
20+
**Correct:**
21+
22+
```tsx
23+
import Button from './components/Button';
24+
// Loads only Button
25+
```
26+
27+
## When to Use
28+
29+
- Bundle contains unused code from libraries
30+
- Circular dependency warnings in Metro
31+
- Hot Module Replacement (HMR) breaks frequently
32+
- TTI is slow due to module evaluation
33+
34+
## What Are Barrel Exports?
35+
36+
```tsx
37+
// components/index.ts (barrel file)
38+
export { Button } from './Button';
39+
export { Card } from './Card';
40+
export { Modal } from './Modal';
41+
export { Sidebar } from './Sidebar';
42+
43+
// Usage (barrel import)
44+
import { Button } from './components';
45+
```
46+
47+
## Problems with Barrel Imports
48+
49+
### 1. Bundle Size Overhead
50+
51+
Metro includes **all exports** even if you use one:
52+
53+
```tsx
54+
// Only need Button, but entire barrel is bundled
55+
import { Button } from './components';
56+
// Card, Modal, Sidebar also included!
57+
```
58+
59+
### 2. Runtime Overhead
60+
61+
All modules evaluate before returning your import:
62+
63+
```tsx
64+
import { Button } from './components';
65+
// JavaScript must evaluate:
66+
// - Button.tsx
67+
// - Card.tsx
68+
// - Modal.tsx
69+
// - Sidebar.tsx
70+
// Even though you only use Button
71+
```
72+
73+
### 3. Circular Dependencies
74+
75+
Barrel files make cycles easier to create accidentally:
76+
77+
```
78+
Warning: Require cycle:
79+
components/index.ts -> Button.tsx -> utils/index.ts -> components/index.ts
80+
```
81+
82+
Breaks HMR, causes unpredictable behavior.
83+
84+
## Solution 1: Direct Imports
85+
86+
Replace barrel imports with direct paths:
87+
88+
```tsx
89+
// BEFORE: Barrel import
90+
import { Button, Card } from './components';
91+
92+
// AFTER: Direct imports
93+
import Button from './components/Button';
94+
import Card from './components/Card';
95+
```
96+
97+
### Enforce with ESLint
98+
99+
```bash
100+
npm install -D eslint-plugin-no-barrel-files
101+
```
102+
103+
```javascript
104+
// eslint.config.js
105+
import noBarrelFiles from 'eslint-plugin-no-barrel-files';
106+
107+
export default [
108+
{
109+
plugins: { 'no-barrel-files': noBarrelFiles },
110+
rules: {
111+
'no-barrel-files/no-barrel-files': 'error',
112+
},
113+
},
114+
];
115+
```
116+
117+
## Solution 2: Tree Shaking (Automatic)
118+
119+
Enable tree shaking to automatically remove unused barrel exports.
120+
121+
### Expo SDK 52+
122+
123+
```tsx
124+
// metro.config.js
125+
const { getDefaultConfig } = require('expo/metro-config');
126+
const config = getDefaultConfig(__dirname);
127+
128+
config.transformer.getTransformOptions = async () => ({
129+
transform: {
130+
experimentalImportSupport: true,
131+
},
132+
});
133+
134+
module.exports = config;
135+
```
136+
137+
```bash
138+
# .env
139+
EXPO_UNSTABLE_METRO_OPTIMIZE_GRAPH=1
140+
EXPO_UNSTABLE_TREE_SHAKING=1
141+
```
142+
143+
### metro-serializer-esbuild
144+
145+
```bash
146+
npm install @rnx-kit/metro-serializer-esbuild
147+
```
148+
149+
### Re.Pack (Webpack/Rspack)
150+
151+
Tree shaking built-in.
152+
153+
## Real-World Example: date-fns
154+
155+
```tsx
156+
// BAD: Imports entire library
157+
import { format, addDays, isToday } from 'date-fns';
158+
159+
// GOOD: Direct imports
160+
import format from 'date-fns/format';
161+
import addDays from 'date-fns/addDays';
162+
import isToday from 'date-fns/isToday';
163+
```
164+
165+
## Library-Specific Solutions
166+
167+
Some libraries provide Babel plugins:
168+
169+
### React Native Paper
170+
171+
```javascript
172+
// babel.config.js
173+
module.exports = {
174+
plugins: [
175+
'react-native-paper/babel', // Auto-transforms imports
176+
],
177+
};
178+
```
179+
180+
Transforms:
181+
```tsx
182+
import { Button } from 'react-native-paper';
183+
// Into:
184+
import Button from 'react-native-paper/lib/module/components/Button';
185+
```
186+
187+
## Refactoring Strategy
188+
189+
### Step 1: Identify Barrel Files
190+
191+
Look for `index.ts` files with multiple exports:
192+
193+
```bash
194+
grep -r "export \* from" src/
195+
grep -r "export { .* } from" src/
196+
```
197+
198+
### Step 2: Update Imports
199+
200+
```tsx
201+
// Find all usages
202+
// VS Code: Cmd+Shift+F for "from './components'"
203+
204+
// Replace each with direct import
205+
import Button from './components/Button';
206+
```
207+
208+
### Step 3: (Optional) Keep Barrel for External API
209+
210+
If your package is consumed by others:
211+
212+
```tsx
213+
// Keep index.ts for package API
214+
// components/index.ts
215+
export { Button } from './Button';
216+
217+
// Internal code uses direct imports
218+
// src/screens/Home.tsx
219+
import Button from '../components/Button';
220+
```
221+
222+
## Migration Script Example
223+
224+
```bash
225+
# Use codemod or search-replace
226+
# Find: import { (\w+) } from '\.\/components';
227+
# Replace: import $1 from './components/$1';
228+
```
229+
230+
## Verification
231+
232+
After refactoring:
233+
234+
1. Run bundle analysis (see [bundle-analyze-js.md](./bundle-analyze-js.md))
235+
2. Compare sizes before/after
236+
3. Check for circular dependency warnings
237+
238+
## Common Pitfalls
239+
240+
- **Breaking external consumers**: If publishing a library, keep barrel for public API
241+
- **IDE auto-imports**: Configure IDE to prefer direct imports
242+
- **Inconsistent patterns**: Enforce with ESLint across team
243+
244+
## Related Skills
245+
246+
- [bundle-analyze-js.md](./bundle-analyze-js.md) - Verify impact
247+
- [bundle-tree-shaking.md](./bundle-tree-shaking.md) - Automatic solution
248+
- [bundle-library-size.md](./bundle-library-size.md) - Check library patterns

0 commit comments

Comments
 (0)