---
name: react-native-performance
description: >-
  Diagnose and fix React Native performance: list virtualization, re-render
  elimination, memoization that actually works, native-thread animation, and
  context splitting. Use when a screen janks or a list stutters.
argument-hint: "[screen or list]"
license: MIT
metadata:
  author: John Felix Lim
  version: "1.0.0"
---
# Performance

Apply when something feels slow, and always measure first. Most React Native performance work is removing unnecessary re-renders and moving animation off the JS thread — reach for those before anything more exotic.

## Rules

1. **Measure before optimizing, on a real low-end device** — A release build on a mid-range Android phone is the only measurement that matters. Debug builds on a flagship simulator will tell you everything is fine right up until a user review says otherwise.
2. **Animate on the UI thread or don't bother** — Reanimated worklets and the native driver run animations off the JS thread, so a busy render or a JSON parse cannot stutter them. An `Animated` timing without `useNativeDriver` competes with everything else you're doing.
3. **Memoize the render path of list items, all of it** — `React.memo` on a row is defeated by an inline `onPress` arrow or an inline style object in the parent — both create a new reference every render. Memoizing the component without stabilizing its props is a no-op that looks like a fix.
4. **Give lists the information they need to skip work** — `keyExtractor` plus a stable `getItemLayout` lets a list skip measurement entirely. Without them the list measures every row on every change, which is exactly the work you were trying to avoid.
5. **Context is a re-render broadcast, not a state manager** — Every consumer re-renders when any part of the value changes. Split contexts by update frequency, or the theme provider will re-render your entire tree whenever a form field changes.

## Patterns

### List rows that actually memoize

**Avoid** — Three new references per row per render. The memo is decorative.

```tsx
const Row = React.memo(ProductRow);

<FlatList
  data={products}
  renderItem={({ item }) => (
    // New function + new object every single render.
    // React.memo can never bail out.
    <Row item={item} style={{ padding: 12 }} onPress={() => open(item.id)} />
  )}
/>
```

**Prefer** — Stable references all the way down, plus a fixed row height so the list can skip measurement.

```tsx
const styles = StyleSheet.create({ row: { padding: 12 } });

const Row = React.memo(function ProductRow({ item, onPress }: Props) {
  // The row calls back with its own id, so the parent's
  // handler never needs to close over the item.
  return <Pressable onPress={() => onPress(item.id)} style={styles.row} />;
});

const handlePress = useCallback((id: string) => open(id), [open]);
const renderItem = useCallback(
  ({ item }: { item: Product }) => <Row item={item} onPress={handlePress} />,
  [handlePress],
);

<FlatList
  data={products}
  renderItem={renderItem}
  keyExtractor={(item) => item.id}
  getItemLayout={(_, index) => ({ length: ROW_H, offset: ROW_H * index, index })}
/>
```

### Animation off the JS thread

**Avoid** — The animation competes with your data parsing, your navigation transition, and every other bit of JS work.

```tsx
const opacity = useRef(new Animated.Value(0)).current;

Animated.timing(opacity, {
  toValue: 1,
  duration: 300,
  // Defaults to false — every frame is computed in JS and
  // bridged across. A busy thread means dropped frames.
}).start();
```

**Prefer** — Reanimated worklets run on the UI thread, so JS work and animation smoothness stop being coupled.

```tsx
const opacity = useSharedValue(0);

const style = useAnimatedStyle(() => ({
  opacity: withTiming(opacity.value, { duration: 300 }),
}));

// Runs entirely on the UI thread. A blocked JS thread
// cannot stutter it.
<Animated.View style={style} />
```

### Splitting context by update frequency

**Avoid** — A new object literal every render, and one shared subscription for values that change at wildly different rates.

```tsx
// One provider, one value object.
<AppContext.Provider value={{ theme, user, cart, setCart }}>
  {children}
</AppContext.Provider>

// Adding a cart item re-renders every consumer of theme.
```

**Prefer** — Consumers only re-render for the slice they actually read.

```tsx
// Rarely changes — separate provider.
<ThemeContext.Provider value={theme}>
  {/* Changes constantly — its own provider, memoized value. */}
  <CartContext.Provider value={cartValue}>
    {children}
  </CartContext.Provider>
</ThemeContext.Provider>

const cartValue = useMemo(() => ({ cart, setCart }), [cart, setCart]);
```

## Review checklist

- [ ] Profiled in a release build on a mid-range Android device.
- [ ] Every list has `keyExtractor`, and `getItemLayout` wherever rows are fixed-height.
- [ ] No inline functions, objects, or array literals in `renderItem`.
- [ ] All animations use Reanimated worklets or `useNativeDriver: true`.
- [ ] Contexts are split by update frequency, and every provider value is memoized.
- [ ] Expensive screens are verified with the React DevTools profiler, not by eye.

---

From the John Felix Lim Frontend Engineering Playbook — https://github.com/JohnFelixLim