---
name: react-native-list-performance
source: https://app.decimal.ai/s/react-native-list-performance@2/SKILL.md
source_sha256: aed0dcb30f86
---

# React Native list-performance idioms

## Contract

Enforces the React Native / Expo performant-list idiom set on any screen that renders a
long or unbounded vertical list. Apply whenever the task is to build a mobile list, feed,
grid, inbox, or timeline component. The base default (`FlatList`, inline `renderItem`,
unmemoized rows, `react-native` `Image`) works but drops frames at scale; this is the
form to emit instead.

## Rules

1. **List container — use `FlashList` from `@shopify/flash-list`.** Import and render
   `<FlashList data={…} renderItem={…} keyExtractor={…} />`. Do NOT use React Native's
   built-in `FlatList` or `SectionList`, do NOT wrap items in a `ScrollView`, and never
   `.map()` a large array into `<View>`s. `FlashList` is a drop-in with the same core props.

2. **Row component — extract it and wrap it in `React.memo`.** Define the row as its own
   named component and export a `React.memo(Row)`. Never return the row markup from an
   inline arrow inside `renderItem` — an inline body re-creates and re-renders every visible
   row on each parent render.

3. **Callbacks — stabilize with `useCallback`.** `renderItem` and every per-row handler
   (`onPress`, `onToggle`, `onSelect`, …) must be wrapped in `useCallback` with correct deps
   (or defined once at module scope). A fresh inline arrow passed each render defeats the
   row's `React.memo` because the prop identity changes every time.

4. **Styles — declare with `StyleSheet.create`, reference by key.** Build a `StyleSheet.create({…})`
   object once and pass `style={styles.row}`. Never pass an inline object literal
   (`style={{ padding: 12 }}`) to a component that lives inside the list — a new object each
   render breaks memoization the same way an inline callback does.

5. **Mixed rows — pass `getItemType`.** When the list holds rows of different kinds (headers
   vs items, text vs image vs system messages, posts vs ads vs banners), pass
   `getItemType={(item) => item.type}` so `FlashList`'s recycler pools each kind separately
   instead of remeasuring on every type switch.

6. **Row images — use `Image` from `expo-image`.** `import { Image } from 'expo-image'` and
   render `<Image source={{ uri }} />` for any image inside a row. Do NOT use the `Image`
   exported by `react-native`; `expo-image` caches and decodes off the JS thread, which is
   what keeps an image-heavy list smooth.

## Worked examples

One BEFORE (the base default) → AFTER (the conforming form) per rule.

**Rule 1 — container.**
```jsx
// BEFORE (base default)
import { FlatList } from 'react-native';
<FlatList data={data} renderItem={({ item }) => <Cell value={item} />} />

// AFTER
import { FlashList } from '@shopify/flash-list';
<FlashList data={data} renderItem={renderCell} keyExtractor={(it) => it.id} />
```

**Rule 2 — memoized row.**
```jsx
// BEFORE
<FlashList data={data} renderItem={({ item }) => (
  <View><Text>{item.title}</Text></View>
)} />

// AFTER
const Cell = React.memo(function Cell({ item }) {
  return <View style={styles.cell}><Text>{item.title}</Text></View>;
});
```

**Rule 3 — stable callbacks.**
```jsx
// BEFORE
<FlashList renderItem={({ item }) => <Cell item={item} onPress={() => open(item.id)} />} />

// AFTER
const onPress = useCallback((id) => open(id), [open]);
const renderCell = useCallback(({ item }) => <Cell item={item} onPress={onPress} />, [onPress]);
<FlashList data={data} renderItem={renderCell} />
```

**Rule 4 — StyleSheet, not inline object.**
```jsx
// BEFORE
<View style={{ paddingHorizontal: 16, height: 56 }}>…</View>
// AFTER
const styles = StyleSheet.create({ cell: { paddingHorizontal: 16, height: 56 } });
<View style={styles.cell}>…</View>
```

**Rule 5 — getItemType for mixed kinds.**
```jsx
// BEFORE  (one renderItem branches with if/else, recycler pools everything as one type)
<FlashList data={feed} renderItem={renderAny} />
// AFTER
<FlashList data={feed} renderItem={renderAny} getItemType={(it) => it.kind} />
```

**Rule 6 — expo-image.**
```jsx
// BEFORE
import { Image } from 'react-native';
<Image source={{ uri }} style={styles.thumb} />
// AFTER
import { Image } from 'expo-image';
<Image source={{ uri }} style={styles.thumb} />
```

## Edge cases & exceptions

- **A short, fixed, non-scrolling group of a handful of items** (a 3-button toolbar, a
  settings header block) is fine as plain `<View>`s — the idiom is for lists that scroll and
  grow. When in doubt for anything unbounded, use `FlashList`.
- **Sectioned data** (headers + items): keep ONE flat `FlashList` with a flattened array and
  `getItemType` distinguishing header vs item — do not reach for `SectionList` for perf.
- **A grid** is still `FlashList` with `numColumns`; do not hand-roll rows of `<View>`s.
- **A single hero image outside any list** does not require `expo-image` for list-perf
  reasons — this rule targets images that appear inside recycled rows.
- **`keyExtractor`** should return a stable unique id, never the array index, so recycling
  and memoization line up with item identity.

## Do / Don't

- DON'T render a large list with `FlatList`, `SectionList`, or `ScrollView` + `.map()`.
  ALWAYS use `FlashList` from `@shopify/flash-list`.
- DON'T inline the row markup in `renderItem`. ALWAYS extract a `React.memo` row component.
- DON'T pass a fresh inline arrow as `renderItem` or as a per-row handler. ALWAYS wrap it in
  `useCallback` (or hoist it).
- DON'T pass inline `style={{ … }}` objects to row components. ALWAYS reference a
  `StyleSheet.create` key.
- DON'T use `react-native`'s `Image` inside a row. ALWAYS use `Image` from `expo-image`.
- DON'T omit `getItemType` when rows have different shapes.

## Common mistakes

- Reaching for `FlatList` because it is the first list component that comes to mind — it is
  the built-in, but not the performant default this convention wants.
- Writing `renderItem={({ item }) => <Row … />}` inline "because it's shorter" — this is the
  single most common perf regression; it re-creates the closure and defeats row memoization.
- Memoizing the row with `React.memo` but then handing it inline objects/callbacks, so the
  props change identity every render and `memo` never skips a re-render.
- Using `react-native`'s `Image` in rows and then fighting flicker/jank — `expo-image` is
  the intended component for recycled row images.
- Branching row types inside one `renderItem` without `getItemType`, so the recycler treats
  every row as the same type and remeasures constantly.

## Quick checklist

- [ ] List rendered with `FlashList` from `@shopify/flash-list` (not `FlatList`/`ScrollView`).
- [ ] Row is a separate `React.memo` component (not inline JSX in `renderItem`).
- [ ] `renderItem` + per-row handlers wrapped in `useCallback`.
- [ ] Styles via `StyleSheet.create`; no inline style object in the row.
- [ ] `getItemType` passed when rows are of different kinds.
- [ ] Row images use `Image` from `expo-image`.