git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/mobile-performance-patterns" ~/.claude/skills/intense-visions-harness-engineering-mobile-performance-patterns && rm -rf "$T"
agents/skills/claude-code/mobile-performance-patterns/SKILL.mdMobile Performance Patterns
Optimize React Native app performance with profiling, memoization, lazy loading, and native thread management
When to Use
- App feels sluggish during scrolling, navigation, or data loading
- Startup time is too slow (> 2 seconds to interactive)
- Animations drop below 60fps
- Bundle size is too large (> 10MB)
- Identifying and fixing unnecessary re-renders
Instructions
- Enable Hermes engine. Hermes is the optimized JavaScript engine for React Native. It improves startup time, reduces memory usage, and compiles bytecode at build time. Expo enables Hermes by default since SDK 48.
// app.json { "expo": { "jsEngine": "hermes" } }
- Minimize re-renders with
,React.memo
, anduseMemo
.useCallback
// Memoize components that receive the same props frequently const ProductCard = memo(function ProductCard({ product }: { product: Product }) { return ( <View> <Image source={{ uri: product.imageUrl }} style={styles.image} /> <Text>{product.name}</Text> <Text>${product.price}</Text> </View> ); }); // Memoize expensive computations function OrderList({ orders, filter }: Props) { const filteredOrders = useMemo( () => orders.filter((o) => o.status === filter).sort((a, b) => b.date - a.date), [orders, filter] ); // Stable callback reference for child components const handlePress = useCallback( (orderId: string) => { navigation.navigate('OrderDetail', { orderId }); }, [navigation] ); return <FlatList data={filteredOrders} renderItem={/* ... */} />; }
- Use
for FlatListuseCallback
andrenderItem
.keyExtractor
const renderItem = useCallback( ({ item }: { item: Order }) => <OrderCard order={item} onPress={handlePress} />, [handlePress] ); const keyExtractor = useCallback((item: Order) => item.id, []); <FlatList data={orders} renderItem={renderItem} keyExtractor={keyExtractor} />;
- Lazy-load screens and heavy components.
import { lazy, Suspense } from 'react'; const HeavyChart = lazy(() => import('./components/HeavyChart')); function Dashboard() { return ( <Suspense fallback={<ChartSkeleton />}> <HeavyChart data={chartData} /> </Suspense> ); }
- Optimize images. Images are often the largest performance bottleneck.
import { Image } from 'expo-image'; // expo-image provides caching, blurhash placeholders, and memory management <Image source={{ uri: product.imageUrl }} placeholder={{ blurhash: product.blurhash }} contentFit="cover" transition={200} style={styles.image} recyclingKey={product.id} />;
- Use appropriate image sizes (do not load 4K images for thumbnails)
- Use WebP format for smaller file sizes
- Use
instead of React Native'sexpo-image
for better cachingImage
- Profile with React DevTools and Flipper.
# Enable the React DevTools profiler npx react-devtools
- Open the Profiler tab to see which components re-render and why
- Look for components that re-render when their props have not changed
- Check for slow renders (> 16ms per frame for 60fps)
- Reduce bundle size with tree shaking and lazy imports.
// Bad — imports the entire library import { format, parse, addDays, subDays, isAfter } from 'date-fns'; // Good — import only what you need (tree-shakeable) import format from 'date-fns/format'; import addDays from 'date-fns/addDays'; // Check bundle size npx expo-doctor --check-dependencies
- Optimize startup time.
- Defer non-critical initialization (analytics, crash reporting) until after first render
- Use
to keep the splash visible until critical data loadsexpo-splash-screen - Minimize synchronous storage reads during startup
- Pre-load fonts and critical assets with
andexpo-fontexpo-asset
import * as SplashScreen from 'expo-splash-screen'; SplashScreen.preventAutoHideAsync(); function App() { const [ready, setReady] = useState(false); useEffect(() => { async function prepare() { await loadFonts(); await loadCriticalData(); setReady(true); } prepare(); }, []); const onLayoutRootView = useCallback(async () => { if (ready) await SplashScreen.hideAsync(); }, [ready]); if (!ready) return null; return <View onLayout={onLayoutRootView}>{/* app content */}</View>; }
-
Avoid bridge traffic for animations. Use Reanimated (UI thread) instead of
(JS thread). UseAnimated
instead ofuseAnimatedStyle
objects that depend on animated values.style -
Monitor performance in production with tools like Sentry Performance or custom metrics.
Details
React Native threading model: React Native has three threads — the JS thread (runs your React code), the UI/Main thread (renders native views), and the Shadow thread (calculates layout with Yoga). Performance problems usually fall into: JS thread overload (expensive re-renders), bridge congestion (too much data crossing), or main thread blocking (synchronous native calls).
Common re-render causes:
- Parent component re-renders (wrap children in
)memo - New object/array references in props (use
)useMemo - New function references in props (use
)useCallback - Context value changes (split contexts by update frequency)
Memory management:
- Large image caches can cause OOM — set cache limits
- Unmounted components that still hold subscriptions — clean up in
returnuseEffect - Large lists without virtualization — always use FlatList or FlashList
New Architecture (Fabric + TurboModules): The new architecture removes the bridge, enabling synchronous communication between JS and native. It improves performance for interop-heavy operations. Available in Expo SDK 51+.
Source
https://reactnative.dev/docs/performance
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.