Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Use when building a React Native or Expo screen that renders a long scrolling list: render it with FlashList and memoized rows instead of the built-in FlatList defaults — which cheaper models do not reach for by default.
.claude/skills/react-native-list-performance/SKILL.md| Model | Lift | Δ tokens | Δ turns | Cases | Verified |
|---|---|---|---|---|---|
| gemini-3.6-flashbest | +22% | +72% | 0% | 23 | 54d agov1 |
| gemini-3.5-flash | pending re-run | — | |||
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | — | — |
| case-14 | ✗→✓ | ▲ Improved | — | — |
| case-09 | ✗→✓ | ▲ Improved | — | — |
| case-05 | ✗→✓ | ▲ Improved | — | — |
| case-08 | ✗→✓ | ▲ Improved | — | — |
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.
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.
React.memo. Define the row as its ownnamed 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.
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.
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.
getItemType. When the list holds rows of different kinds (headersvs 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.
Image from expo-image. import { Image } from 'expo-image' andrender <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.
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} />
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.
FlashList with a flattened array andgetItemType distinguishing header vs item — do not reach for SectionList for perf.
FlashList with numColumns; do not hand-roll rows of <View>s.expo-image for list-perfreasons — this rule targets images that appear inside recycled rows.
keyExtractor should return a stable unique id, never the array index, so recyclingand memoization line up with item identity.
FlatList, SectionList, or ScrollView + .map().ALWAYS use FlashList from @shopify/flash-list.
renderItem. ALWAYS extract a React.memo row component.renderItem or as a per-row handler. ALWAYS wrap it inuseCallback (or hoist it).
style={{ … }} objects to row components. ALWAYS reference aStyleSheet.create key.
react-native's Image inside a row. ALWAYS use Image from expo-image.getItemType when rows have different shapes.FlatList because it is the first list component that comes to mind — it isthe built-in, but not the performant default this convention wants.
renderItem={({ item }) => <Row … />} inline "because it's shorter" — this is thesingle most common perf regression; it re-creates the closure and defeats row memoization.
React.memo but then handing it inline objects/callbacks, so theprops change identity every render and memo never skips a re-render.
react-native's Image in rows and then fighting flicker/jank — expo-image isthe intended component for recycled row images.
renderItem without getItemType, so the recycler treatsevery row as the same type and remeasures constantly.
FlashList from @shopify/flash-list (not FlatList/ScrollView).React.memo component (not inline JSX in renderItem).renderItem + per-row handlers wrapped in useCallback.StyleSheet.create; no inline style object in the row.getItemType passed when rows are of different kinds.Image from expo-image.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-15 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-23 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-13 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-21 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-01 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-17 | pass→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-19 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-14 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-09 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-22 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-05 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-20 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-03 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-11 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-12 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-02 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-04 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-06 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-07 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-08 | fail→pass | — | — | — | — | — | — | — | — | — | — | — | — |
case-10 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-16 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
case-18 | fail→fail | — | — | — | — | — | — | — | — | — | — | — | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +22 percentage points is the difference between those two pass rates over the 23 comparable cases. 1 case got worse with the skill loaded, and it is included in that figure.
The publisher has shipped newer versions since this run, so these numbers describe v1, not the version currently listed.
The per-case answers from this run were removed by the retention sweep, so the case table below shows the verdicts without the text either arm produced. The counts above were recorded at the time and are unaffected. Answers are now kept for 180 days.
| Model | Method | Date | Lift |
|---|---|---|---|
| gemini-3.5-flash | verified | 7/10/2026 | +17% |
Other measured skills in the registry, with their headline benchmark lift.