name: react-native-best-practices description: React Native performance optimization guide covering JavaScript/React patterns, native module usage, bundle optimization, and profiling. Use when building or optimizing React Native applications. Adapted from Callstack Agent Skills.
React Native Best Practices Skill
Adapted from Callstack React Native Agent Skills.
Priority-ordered guidelines for building fast, efficient React Native applications.
Priority 1: Rendering & Re-Render Optimization
Minimize Re-Renders
The biggest React Native performance issue. Every re-render triggers layout, diffing, and potentially native view updates.
// ❌ Inline objects cause re-renders on every parent render
function Parent() {
return <Child style={{ flex: 1, padding: 16 }} />;
}
// ✅ Stable reference with StyleSheet
const styles = StyleSheet.create({
container: { flex: 1, padding: 16 },
});
function Parent() {
return <Child style={styles.container} />;
}
Memoize Expensive Components
// ✅ Only re-renders when item or onPress change
const ListItem = React.memo(function ListItem({ item, onPress }) {
return (
<Pressable onPress={() => onPress(item.id)}>
<Text>{item.title}</Text>
</Pressable>
);
});
Stable Callbacks with useCallback
// ❌ New function reference every render → ListItem re-renders
function List({ items }) {
const handlePress = (id) => selectItem(id);
return items.map(item => <ListItem key={item.id} onPress={handlePress} item={item} />);
}
// ✅ Stable reference
function List({ items }) {
const handlePress = useCallback((id) => selectItem(id), []);
return items.map(item => <ListItem key={item.id} onPress={handlePress} item={item} />);
}
React Compiler (Automatic Memoization)
If using React 19+, enable the React Compiler for automatic memoization. It eliminates the need for manual React.memo, useMemo, and useCallback in most cases.
// babel.config.js
module.exports = {
plugins: [
['babel-plugin-react-compiler', {}],
],
};
Incremental adoption: start with specific files or directories rather than the entire codebase.
// babel.config.js — opt-in per file
module.exports = {
plugins: [
['babel-plugin-react-compiler', {
sources: (filename) => {
return filename.includes('src/components/');
},
}],
],
};
Priority 2: Animations on the UI Thread
Use Reanimated for All Animations
Never use the built-in Animated API for performance-critical animations. Reanimated runs animations on the UI thread, avoiding JS thread bottlenecks.
import Animated, {
useSharedValue,
useAnimatedStyle,
withSpring,
} from 'react-native-reanimated';
function AnimatedCard() {
const scale = useSharedValue(1);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ scale: scale.value }],
}));
const onPressIn = () => {
scale.value = withSpring(0.95);
};
const onPressOut = () => {
scale.value = withSpring(1);
};
return (
<Pressable onPressIn={onPressIn} onPressOut={onPressOut}>
<Animated.View style={[styles.card, animatedStyle]}>
<Text>Press me</Text>
</Animated.View>
</Pressable>
);
}
Worklets for UI Thread Logic
import { runOnUI } from 'react-native-reanimated';
// This function runs on the UI thread
function updatePositionWorklet(x, y) {
'worklet';
position.value = { x, y };
}
// Call from JS thread
runOnUI(updatePositionWorklet)(100, 200);
When to Use What
| Animation Type | Use |
|---|---|
| Simple opacity/transform | Reanimated withTiming/withSpring |
| Gesture-driven | Reanimated + Gesture Handler |
| Scroll-driven | Reanimated useAnimatedScrollHandler |
| Layout animations | Reanimated Layout Transitions |
| Lottie animations | lottie-react-native |
| Complex sequences | Reanimated + withSequence/withDelay |
Priority 3: List Performance
Use FlashList Over FlatList
import { FlashList } from '@shopify/flash-list';
function UserList({ users }) {
return (
<FlashList
data={users}
renderItem={({ item }) => <UserCard user={item} />}
estimatedItemSize={80}
keyExtractor={(item) => item.id}
/>
);
}
List Optimization Checklist
- Always provide
keyExtractor - Use
estimatedItemSize(FlashList) orgetItemLayout(FlatList) - Avoid inline functions in
renderItem - Memoize
renderItemcomponents withReact.memo - Set
removeClippedSubviews={true}for long lists - Use
windowSizeprop to control render-ahead distance - Never nest scrollable lists — use section headers instead
Priority 4: Native Modules Over Polyfills
Replace JS Polyfills with Native SDKs
JS polyfills are significantly slower than native implementations. Always prefer native modules.
| Task | ❌ JS Polyfill | ✅ Native Module |
|---|---|---|
| Internationalization | intl polyfill |
expo-localization + react-native-intl |
| Crypto | crypto-browserify |
react-native-quick-crypto |
| Date formatting | moment.js (500KB) |
Intl.DateTimeFormat (native) |
| Navigation | JS-based routing | react-native-screens + native navigation |
| Image processing | JS canvas operations | react-native-fast-image |
| Storage | AsyncStorage for large data | react-native-mmkv |
| SQLite | JS-based SQL | expo-sqlite or react-native-quick-sqlite |
Decision Matrix
Use a native module when:
- The operation is CPU-intensive (crypto, image processing)
- There's a well-maintained native library
- The polyfill adds >50KB to bundle
- The feature requires frequent calls (storage, formatting)
Keep JS when:
- The logic is simple and rarely called
- No native alternative exists
- Cross-platform consistency is more important than speed
Priority 5: Bundle Size Optimization
Analyze Bundle Contents
# Generate bundle stats
npx react-native-bundle-visualizer
# Or with Expo
npx expo export --dump-sourcemap
Tree-Shaking Best Practices
// ❌ Imports entire library
import _ from 'lodash';
const result = _.map(data, fn);
// ✅ Import only what you need
import map from 'lodash/map';
const result = map(data, fn);
Lazy Loading Screens
import { lazy, Suspense } from 'react';
const SettingsScreen = lazy(() => import('./screens/Settings'));
function App() {
return (
<Suspense fallback={<LoadingScreen />}>
<Navigator>
<Screen name="Settings" component={SettingsScreen} />
</Navigator>
</Suspense>
);
}
Hermes Engine Best Practices
Hermes is the default JS engine for React Native. Optimize for it:
- Prefer
forloops overArray.map()for hot paths - Use
IntlAPIs (supported natively in Hermes) - Avoid
eval()andnew Function()— not supported - Use bytecode precompilation for faster startup
Priority 6: Startup Performance (TTI)
Reduce Time-to-Interactive
- Minimize root component complexity — render a skeleton first, load data after
- Defer non-critical initialization — analytics, deep linking, crash reporting can initialize after first render
- Use
InteractionManager.runAfterInteractions()for deferred work
useEffect(() => {
InteractionManager.runAfterInteractions(() => {
// Heavy work after animations complete
prefetchData();
initializeAnalytics();
});
}, []);
- Preload critical assets — fonts, images used on first screen
- Minimize bridge calls at startup (old architecture) — batch native calls
Splash Screen Strategy
import * as SplashScreen from 'expo-splash-screen';
SplashScreen.preventAutoHideAsync();
function App() {
const [ready, setReady] = useState(false);
useEffect(() => {
async function prepare() {
await loadFonts();
await prefetchCriticalData();
setReady(true);
SplashScreen.hideAsync();
}
prepare();
}, []);
if (!ready) return null;
return <MainNavigator />;
}
Profiling & Debugging
React DevTools Profiler
- Open React DevTools → Profiler tab
- Record a user interaction
- Look for:
- Components rendering more than once per interaction
- Components taking >16ms to render
- Unnecessary renders (no visual change)
Flame Graph Interpretation
- Wide bars: Expensive renders — optimize or memoize
- Repeated bars: Same component re-rendering — check props stability
- Deep trees: Too many nested components — flatten hierarchy
Native Profiling
- iOS: Xcode Instruments → Time Profiler, Allocations
- Android: Android Studio Profiler → CPU, Memory
Common Pitfalls
| Symptom | Cause | Fix |
|---|---|---|
| Janky scrolling | Heavy renderItem |
Memoize, simplify component |
| Slow screen transitions | Large component trees | Lazy load, defer rendering |
| High memory usage | Image caching | Use FastImage with cache limits |
| JS thread blocked | Expensive computation | Move to worklet or native module |
| Slow startup | Too much initialization | Defer non-critical work |
State Management
Atomic State (Jotai/Zustand)
Prefer atomic state libraries over Redux for React Native:
// Jotai — atomic state
import { atom, useAtom } from 'jotai';
const countAtom = atom(0);
const doubledAtom = atom((get) => get(countAtom) * 2);
function Counter() {
const [count, setCount] = useAtom(countAtom);
return <Button onPress={() => setCount(c => c + 1)} title={`${count}`} />;
}
// Zustand — simple global state
import { create } from 'zustand';
const useStore = create((set) => ({
count: 0,
increment: () => set((state) => ({ count: state.count + 1 })),
}));
function Counter() {
const count = useStore((s) => s.count);
const increment = useStore((s) => s.increment);
return <Button onPress={increment} title={`${count}`} />;
}
Zustand Selector Pattern
Always use selectors to prevent unnecessary re-renders:
// ❌ Re-renders on ANY store change
const { count, user, theme } = useStore();
// ✅ Re-renders only when count changes
const count = useStore((s) => s.count);
Concurrent React Patterns
useDeferredValue for Expensive Renders
function SearchResults({ query }) {
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => filterItems(deferredQuery), [deferredQuery]);
return (
<View style={{ opacity: query !== deferredQuery ? 0.8 : 1 }}>
<FlashList data={results} renderItem={renderItem} estimatedItemSize={60} />
</View>
);
}
useTransition for Navigation
function TabBar() {
const [isPending, startTransition] = useTransition();
const [activeTab, setActiveTab] = useState('home');
function switchTab(tab) {
startTransition(() => {
setActiveTab(tab); // non-urgent, can be interrupted
});
}
return (
<View style={{ opacity: isPending ? 0.7 : 1 }}>
<TabContent tab={activeTab} />
</View>
);
}
Quick Reference
| Metric | Target | Tool |
|---|---|---|
| FPS | 60 (≤16ms/frame) | React DevTools Profiler |
| Re-renders | 1 per interaction | React.memo, useCallback |
| Bundle Size | <5MB JS bundle | Bundle Visualizer |
| TTI | <2s cold start | Splash screen + deferred init |
| List scroll | No jank | FlashList + estimatedItemSize |
| Problem | Skill to Apply |
|---|---|
| Janky animations | Reanimated (UI thread) |
| Slow lists | FlashList + memoization |
| Large bundle | Tree-shaking + lazy loading |
| Slow JS operations | Native modules |
| Re-render cascades | Atomic state + selectors |
| Slow startup | InteractionManager + deferred init |
Attribution
Based on Callstack React Native Agent Skills — React Native Performance Optimization.