---
name: rn-performance
description: Performance optimization for React Native. Use when optimizing lists, preventing re-renders, memoizing components, or debugging performance issues in Expo/React Native apps.
---
# React Native Performance
## Problem Statement
React Native performance issues often stem from unnecessary re-renders, unoptimized lists, and expensive computations on the JS thread. This codebase has performance-critical areas (shot mastery, player lists) with established optimization patterns.
---
## Pattern: FlatList Optimization
### keyExtractor - Stable Keys
```typescript
// ✅ CORRECT: Stable function reference
const keyExtractor = useCallback((item: Session) => item.id, []);
// ❌ WRONG: Creates new function every render
item.id}
renderItem={renderItem}
/>
// ❌ WRONG: Using index (causes issues with reordering/deletion)
keyExtractor={(item, index) => `${index}`}
```
### getItemLayout - Fixed Height Items
```typescript
const ITEM_HEIGHT = 80;
const SEPARATOR_HEIGHT = 1;
const getItemLayout = useCallback(
(data: Session[] | null | undefined, index: number) => ({
length: ITEM_HEIGHT,
offset: (ITEM_HEIGHT + SEPARATOR_HEIGHT) * index,
index,
}),
[]
);
```
**Why it matters:** Without `getItemLayout`, FlatList must measure each item, causing scroll jank.
### renderItem - Memoized
```typescript
// Extract to named component
const SessionItem = memo(function SessionItem({
session,
onPress
}: {
session: Session;
onPress: (id: string) => void;
}) {
return (
onPress(session.id)}>
{session.title}
);
});
// Stable callback
const handlePress = useCallback((id: string) => {
navigation.push(`/session/${id}`);
}, [navigation]);
// Stable renderItem
const renderItem = useCallback(
({ item }: { item: Session }) => (
),
[handlePress]
);
```
### Additional Optimizations
```typescript
```
---
## Pattern: FlashList for Large Lists
**When to use:** 1000+ items, complex item components, or FlatList still janky.
```typescript
import { FlashList } from '@shopify/flash-list';
```
**Note:** This codebase doesn't currently use FlashList. Consider for coach player lists.
---
## Pattern: Memoization
### useMemo - Expensive Computations
```typescript
// ✅ CORRECT: Memoize expensive calculation
const sortedAndFilteredItems = useMemo(() => {
return items
.filter(item => item.active)
.sort((a, b) => b.score - a.score)
.slice(0, 100);
}, [items]);
// ❌ WRONG: Recalculates every render
const sortedAndFilteredItems = items
.filter(item => item.active)
.sort((a, b) => b.score - a.score);
// ❌ WRONG: Memoizing simple access (overhead > benefit)
const userName = useMemo(() => user.name, [user.name]);
```
**When to use useMemo:**
- Array transformations (filter, sort, map chains)
- Object creation passed to memoized children
- Computations with O(n) or higher complexity
### useCallback - Stable Function References
```typescript
// ✅ CORRECT: Stable callback for child props
const handlePress = useCallback((id: string) => {
setSelectedId(id);
}, []);
// Pass to memoized child
// ❌ WRONG: useCallback with unstable deps
const handlePress = useCallback((id: string) => {
doSomething(unstableObject); // unstableObject changes every render
}, [unstableObject]); // Defeats the purpose
```
**When to use useCallback:**
- Callbacks passed to memoized children
- Callbacks in dependency arrays
- Event handlers that would cause child re-renders
---
## Pattern: React.memo
```typescript
// Wrap components that receive stable props
const PlayerCard = memo(function PlayerCard({
player,
onSelect
}: Props) {
return (
onSelect(player.id)}>
{player.name}
{player.rating}
);
});
// Custom comparison for complex props
const PlayerCard = memo(
function PlayerCard({ player, onSelect }: Props) {
// ...
},
(prevProps, nextProps) => {
// Return true if props are equal (skip re-render)
return (
prevProps.player.id === nextProps.player.id &&
prevProps.player.rating === nextProps.player.rating
);
}
);
```
**When to use React.memo:**
- List item components
- Components receiving stable primitive props
- Components that render frequently but rarely change
**When NOT to use:**
- Components that always receive new props
- Simple components (overhead > benefit)
- Root-level screens
---
## Pattern: Zustand Selector Optimization
**Problem:** Selecting entire store causes re-render on any state change.
```typescript
// ❌ WRONG: Re-renders on ANY store change
const store = useAssessmentStore();
// or
const { userAnswers, isLoading, retakeAreas, ... } = useAssessmentStore();
// ✅ CORRECT: Only re-renders when selected values change
const userAnswers = useAssessmentStore((s) => s.userAnswers);
const isLoading = useAssessmentStore((s) => s.isLoading);
// ✅ CORRECT: Multiple values with shallow comparison
import { useShallow } from 'zustand/react/shallow';
const { userAnswers, isLoading } = useAssessmentStore(
useShallow((s) => ({
userAnswers: s.userAnswers,
isLoading: s.isLoading
}))
);
```
**See also:** `rn-zustand-patterns/SKILL.md` for more Zustand patterns.
---
## Pattern: Image Optimization
```typescript
import { Image } from 'expo-image';
// expo-image provides caching and performance optimizations
// For lists, add priority
```
---
## Pattern: Avoiding Re-Renders
### Object/Array Stability
```typescript
// ❌ WRONG: New object every render
// ✅ CORRECT: Stable reference
const style = useMemo(() => ({ padding: 10 }), []);
const config = useMemo(() => ({ enabled: true }), []);
// ✅ CORRECT: Or use StyleSheet
const styles = StyleSheet.create({
container: { padding: 10 },
});
```
### Children Stability
```typescript
// ❌ WRONG: Inline function creates new element each render
{() => }
// ✅ CORRECT: Stable element
const child = useMemo(() => , [deps]);
{child}
```
---
## Pattern: Detecting Re-Renders
### React DevTools Profiler
1. Open React DevTools
2. Go to Profiler tab
3. Click record, interact, stop
4. Review "Flamegraph" for render times
5. Look for components rendering unnecessarily
### why-did-you-render
```typescript
// Setup in development
import React from 'react';
if (__DEV__) {
const whyDidYouRender = require('@welldone-software/why-did-you-render');
whyDidYouRender(React, {
trackAllPureComponents: true,
});
}
// Mark specific component for tracking
PlayerCard.whyDidYouRender = true;
```
### Console Logging
```typescript
// Quick check for re-renders
function PlayerCard({ player }: Props) {
console.log('PlayerCard render:', player.id);
// ...
}
```
---
## Pattern: Heavy Computation Off Main Thread
**Problem:** JS thread blocked causes UI jank.
```typescript
// ❌ WRONG: Blocks JS thread
const result = heavyComputation(data); // Takes 500ms
// ✅ CORRECT: Use InteractionManager
import { InteractionManager } from 'react-native';
InteractionManager.runAfterInteractions(() => {
const result = heavyComputation(data);
setResult(result);
});
// ✅ CORRECT: requestAnimationFrame for visual updates
requestAnimationFrame(() => {
// Update after current frame
});
```
---
## Performance Checklist
Before shipping list-heavy screens:
- [ ] FlatList has `keyExtractor` (stable callback)
- [ ] FlatList has `getItemLayout` (if fixed height)
- [ ] List items are memoized with `React.memo`
- [ ] Callbacks passed to items use `useCallback`
- [ ] Zustand selectors are specific (not whole store)
- [ ] Images use `expo-image` with caching
- [ ] No inline object/function props to memoized children
- [ ] Profiler shows no unnecessary re-renders
---
## Common Issues
| Issue | Solution |
|-------|----------|
| List scroll jank | Add `getItemLayout`, memoize items |
| Component re-renders too often | Check selector specificity, memoize props |
| Slow initial render | Reduce `initialNumToRender`, defer computation |
| Memory growing | Check for state accumulation, image cache |
| UI freezes on interaction | Move computation off main thread |
---
## Relationship to Other Skills
- **rn-zustand-patterns**: Selector optimization patterns
- **rn-styling**: StyleSheet.create for stable style references