React Native performance problems have predictable patterns: FlatList rendering every scroll frame, re-renders cascading down component trees, JavaScript bundle parsing blocking startup, and images loaded at wrong sizes. Claude Code identifies these patterns and generates the optimized versions — memoized components, virtualized lists with proper keyExtractor, and Hermes-optimized bundles.
FlatList Optimization
Our FlatList with 1000 products is janky on scroll.
Items re-render on every scroll event. Fix it.
// Before — every scroll triggers re-render of visible items
function ProductList({ products, onAddToCart }: Props) {
return (
<FlatList
data={products}
renderItem={({ item }) => (
<ProductCard
product={item}
onAddToCart={onAddToCart} // New function reference every render
/>
)}
/>
);
}
// After — stable references prevent re-renders
const ProductCard = React.memo(({ product, onAddToCart }: ProductCardProps) => {
// Only re-renders when product or onAddToCart reference changes
return (
<TouchableOpacity onPress={() => onAddToCart(product.id)}>
<FastImage
source={{ uri: product.imageUrl, priority: FastImage.priority.normal }}
style={styles.image}
resizeMode={FastImage.resizeMode.cover}
/>
<Text>{product.name}</Text>
<Text>${(product.priceCents / 100).toFixed(2)}</Text>
</TouchableOpacity>
);
}, (prev, next) => {
// Custom comparison — only re-render if these change
return prev.product.id === next.product.id &&
prev.product.priceCents === next.product.priceCents;
});
function ProductList({ products, onAddToCart }: Props) {
// Stable callback reference
const handleAddToCart = useCallback((productId: string) => {
onAddToCart(productId);
}, [onAddToCart]);
// Stable keyExtractor
const keyExtractor = useCallback((item: Product) => item.id, []);
// Optimize item layout for fixed-size items (avoids layout recalculation)
const getItemLayout = useCallback((_: any, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}), []);
return (
<FlatList
data={products}
renderItem={({ item }) => (
<ProductCard product={item} onAddToCart={handleAddToCart} />
)}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
maxToRenderPerBatch={10} // Render 10 items per batch (default 10)
updateCellsBatchingPeriod={50} // Milliseconds between batch renders
windowSize={5} // Render 5 viewport heights (default 21)
removeClippedSubviews={true} // Unmount items far off screen
initialNumToRender={8} // Only render 8 for initial mount
onEndReachedThreshold={0.5} // Load more when 50% from end
/>
);
}
Reducing Re-Renders
Help me find and fix unnecessary re-renders in our profile screen.
The whole screen re-renders when any part of the user object changes.
// Problem: entire ProfileScreen renders when ANY user property changes
function ProfileScreen() {
const user = useUserStore(state => state.user); // Subscribes to entire user object
return (
<>
<Avatar url={user.avatarUrl} />
<UserStats orders={user.orderCount} joined={user.joinedAt} />
<BadgeList badges={user.badges} />
</>
);
}
// Fix: granular selectors — each component gets only what it needs
const Avatar = React.memo(({ url }: { url: string }) => (
<FastImage source={{ uri: url }} style={styles.avatar} />
));
function AvatarContainer() {
// Only re-renders when avatarUrl changes
const avatarUrl = useUserStore(state => state.user.avatarUrl);
return <Avatar url={avatarUrl} />;
}
const UserStats = React.memo(({ orders, joined }: { orders: number; joined: string }) => (
<View>
<Text>{orders} orders</Text>
<Text>Member since {joined}</Text>
</View>
));
function UserStatsContainer() {
// Zustand shallow comparison — only re-renders when these two change
const { orderCount, joinedAt } = useUserStore(
state => ({ orderCount: state.user.orderCount, joinedAt: state.user.joinedAt }),
shallow, // Prevents re-render if other user fields change
);
return <UserStats orders={orderCount} joined={joinedAt} />;
}
Hermes Profiling
Our app takes 3.2 seconds to become interactive on mid-range Android.
Find the startup bottleneck.
# Enable Hermes (already default in RN 0.70+, but verify)
# android/app/build.gradle:
# project.ext.react = [
# enableHermes: true,
# ]
# Profile startup:
# 1. Enable remote debugging: Shake device → Debug → Enable Remote Debugging off
# Instead: use Hermes inspector
# Start Metro with profiling
npx react-native start
# Capture startup profile via adb
adb shell am start -n com.yourapp/.MainActivity
# Download Hermes timeline
adb pull /sdcard/Android/data/com.yourapp/files/sampling_profiler_trace.cpuprofile .
Claude Code can analyze the profiler output:
Parse this Hermes CPU profile and find:
1. What's blocking the JS thread on startup?
2. Which modules are slowest to initialize?
3. What can be deferred until after first interaction?
(paste profile JSON)
Common findings and fixes:
// Problem: importing heavy libraries at module init time
import moment from 'moment'; // 300KB, initializes on startup
import _ from 'lodash'; // 70KB, loads all functions
// Fix: lazy imports for non-critical paths
const getDateFormatter = () => import('./utils/dateFormat');
// Or use lighter alternatives:
import { format } from 'date-fns'; // Tree-shakeable, ~20KB
import { debounce } from 'lodash-es/debounce'; // Single function
Image Performance
Our app has slow image loading and high memory usage from large images.
We're showing product photos at thumbnail size but loading full resolution.
// Use react-native-fast-image for caching + priority control
// AND specify correct size to avoid loading 4K images at 100x100px
// Product thumbnail component
function ProductThumbnail({ product }: { product: Product }) {
return (
<FastImage
source={{
uri: getOptimizedImageUrl(product.imageUrl, {
width: 200, // Match display size × pixel density
height: 200,
format: 'webp',
quality: 80,
}),
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable, // Never re-fetch same URL
}}
style={styles.thumbnail}
resizeMode={FastImage.resizeMode.cover}
/>
);
}
// Image URL optimizer (calls your image CDN)
function getOptimizedImageUrl(url: string, opts: { width: number; height: number; format: string; quality: number }) {
// Cloudflare Images / Imgix / Cloudinary transformation URL
const pixelDensity = PixelRatio.get(); // 2 or 3 on modern phones
return `${url}?w=${opts.width * pixelDensity}&h=${opts.height * pixelDensity}&f=${opts.format}&q=${opts.quality}`;
}
Bundle Size Analysis
# Analyze what's in the bundle
npx react-native bundle \
--platform android \
--dev false \
--entry-file index.js \
--bundle-output /tmp/main.bundle \
--assets-dest /tmp/assets \
--sourcemap-output /tmp/main.bundle.map
# Visualize with source-map-explorer
npx source-map-explorer /tmp/main.bundle /tmp/main.bundle.map
The source map explorer shows moment.js is 230KB.
We only use moment.format() in one place. Replace it.
// Before
import moment from 'moment';
const formatted = moment(date).format('MMM D, YYYY');
// After — native Intl API, zero bundle cost
const formatted = new Intl.DateTimeFormat('en-US', {
month: 'short',
day: 'numeric',
year: 'numeric',
}).format(new Date(date));
For the React Native project structure and navigation setup, see the React Native navigation guide. For mobile CI/CD that runs performance regression tests, see the mobile CI/CD guide. The Claude Skills 360 bundle includes mobile performance skill sets for React Native optimization, profiling, and bundle reduction. Start with the free tier to try performance analysis prompts.