Marketplace rn-performance
Performance optimization for React Native. Use when optimizing lists, preventing re-renders, memoizing components, or debugging performance issues in Expo/React Native apps.
git clone https://github.com/aiskillstore/marketplace
T=$(mktemp -d) && git clone --depth=1 https://github.com/aiskillstore/marketplace "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/cjharmath/rn-performance" ~/.claude/skills/aiskillstore-marketplace-rn-performance && rm -rf "$T"
skills/cjharmath/rn-performance/SKILL.mdReact 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
// ✅ CORRECT: Stable function reference const keyExtractor = useCallback((item: Session) => item.id, []); <FlatList data={sessions} keyExtractor={keyExtractor} renderItem={renderItem} /> // ❌ WRONG: Creates new function every render <FlatList data={sessions} keyExtractor={(item) => item.id} renderItem={renderItem} /> // ❌ WRONG: Using index (causes issues with reordering/deletion) keyExtractor={(item, index) => `${index}`}
getItemLayout - Fixed Height Items
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, }), [] ); <FlatList data={sessions} getItemLayout={getItemLayout} // ... other props />
Why it matters: Without
getItemLayout, FlatList must measure each item, causing scroll jank.
renderItem - Memoized
// Extract to named component const SessionItem = memo(function SessionItem({ session, onPress }: { session: Session; onPress: (id: string) => void; }) { return ( <Pressable onPress={() => onPress(session.id)}> <Text>{session.title}</Text> </Pressable> ); }); // Stable callback const handlePress = useCallback((id: string) => { navigation.push(`/session/${id}`); }, [navigation]); // Stable renderItem const renderItem = useCallback( ({ item }: { item: Session }) => ( <SessionItem session={item} onPress={handlePress} /> ), [handlePress] ); <FlatList data={sessions} renderItem={renderItem} // ... />
Additional Optimizations
<FlatList data={sessions} renderItem={renderItem} keyExtractor={keyExtractor} getItemLayout={getItemLayout} // Performance props removeClippedSubviews={true} // Unmount off-screen items maxToRenderPerBatch={10} // Items per render batch windowSize={5} // Render window (screens) initialNumToRender={10} // Initial render count updateCellsBatchingPeriod={50} // Batch update delay (ms) // Prevent extra renders extraData={selectedId} // Only re-render when this changes />
Pattern: FlashList for Large Lists
When to use: 1000+ items, complex item components, or FlatList still janky.
import { FlashList } from '@shopify/flash-list'; <FlashList data={players} renderItem={renderItem} estimatedItemSize={80} // Required - estimate item height keyExtractor={keyExtractor} />
Note: This codebase doesn't currently use FlashList. Consider for coach player lists.
Pattern: Memoization
useMemo - Expensive Computations
// ✅ 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
// ✅ CORRECT: Stable callback for child props const handlePress = useCallback((id: string) => { setSelectedId(id); }, []); // Pass to memoized child <MemoizedItem onPress={handlePress} /> // ❌ 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
// Wrap components that receive stable props const PlayerCard = memo(function PlayerCard({ player, onSelect }: Props) { return ( <Pressable onPress={() => onSelect(player.id)}> <Text>{player.name}</Text> <Text>{player.rating}</Text> </Pressable> ); }); // 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.
// ❌ 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
import { Image } from 'expo-image'; // expo-image provides caching and performance optimizations <Image source={{ uri: player.avatarUrl }} style={{ width: 50, height: 50 }} contentFit="cover" placeholder={blurhash} // Show while loading transition={200} // Fade in duration cachePolicy="memory-disk" // Cache strategy /> // For lists, add priority <Image source={{ uri: player.avatarUrl }} priority={isVisible ? 'high' : 'low'} />
Pattern: Avoiding Re-Renders
Object/Array Stability
// ❌ WRONG: New object every render <ChildComponent style={{ padding: 10 }} /> <ChildComponent config={{ enabled: true }} /> // ✅ CORRECT: Stable reference const style = useMemo(() => ({ padding: 10 }), []); const config = useMemo(() => ({ enabled: true }), []); <ChildComponent style={style} /> <ChildComponent config={config} /> // ✅ CORRECT: Or use StyleSheet const styles = StyleSheet.create({ container: { padding: 10 }, }); <ChildComponent style={styles.container} />
Children Stability
// ❌ WRONG: Inline function creates new element each render <Parent> {() => <Child />} </Parent> // ✅ CORRECT: Stable element const child = useMemo(() => <Child />, [deps]); <Parent>{child}</Parent>
Pattern: Detecting Re-Renders
React DevTools Profiler
- Open React DevTools
- Go to Profiler tab
- Click record, interact, stop
- Review "Flamegraph" for render times
- Look for components rendering unnecessarily
why-did-you-render
// 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
// 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.
// ❌ 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
(stable callback)keyExtractor - FlatList has
(if fixed height)getItemLayout - List items are memoized with
React.memo - Callbacks passed to items use
useCallback - Zustand selectors are specific (not whole store)
- Images use
with cachingexpo-image - No inline object/function props to memoized children
- Profiler shows no unnecessary re-renders
Common Issues
| Issue | Solution |
|---|---|
| List scroll jank | Add , memoize items |
| Component re-renders too often | Check selector specificity, memoize props |
| Slow initial render | Reduce , 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