Why Your React Native App is Slow (And How to Actually Fix It)

By Michael Ouroumis — React Native developer and creator of react-native-turboxml
You've built your React Native app. It works. But something feels off—the scroll stutters, the startup takes forever, and animations that looked smooth in development now feel like a slideshow on a real device.
You're not alone. Performance issues are the most common complaint in the React Native ecosystem, and most developers struggle to pinpoint what's actually causing them.
Let's fix that.
The JavaScript Thread is Doing Too Much
React Native runs your JavaScript on a separate thread from the UI. When your JS thread gets overwhelmed, your app can't respond to user input quickly enough, and everything feels laggy.
The biggest offenders:
Inline functions in props. Every render creates a new function reference, which can trigger unnecessary re-renders in child components.
// Slow - creates new function every render
<TouchableOpacity onPress={() => handlePress(item.id)}>
// Better - stable reference
const handleItemPress = useCallback(() => handlePress(item.id), [item.id]);
<TouchableOpacity onPress={handleItemPress}>
Missing memoization. If your component re-renders but its props haven't changed, you're wasting cycles.
// Wrap expensive components
const ExpensiveList = React.memo(({ data }) => {
// rendering logic
});
// Memoize expensive calculations
const sortedData = useMemo(() =>
data.sort((a, b) => b.score - a.score),
[data]
);
Heavy computations on the JS thread. Anything that takes more than 16ms blocks your frame. Move complex calculations to effects or consider offloading to native modules.
FlatList Mistakes That Kill Performance
FlatList is powerful but unforgiving. Here's what typically goes wrong:
Not setting keyExtractor properly. Without stable keys, React Native can't efficiently update your list.
// Bad - index as key causes issues on data changes
keyExtractor={(item, index) => index.toString()}
// Good - unique, stable identifier
keyExtractor={(item) => item.id}
Ignoring getItemLayout. If your items have fixed heights, tell FlatList. It skips measurement calculations entirely.
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
Rendering too much. Use initialNumToRender, maxToRenderPerBatch, and windowSize to control how many items render at once.
<FlatList
data={items}
initialNumToRender={10}
maxToRenderPerBatch={5}
windowSize={5}
removeClippedSubviews={true}
// ...
/>
The Bridge Problem
Here's what most tutorials won't tell you: the bridge between JavaScript and native code is often your real bottleneck.
Every time data crosses from JS to native (or back), it gets serialized to JSON. For small payloads, this is fine. For large datasets—API responses, local database queries, XML parsing—it becomes a serious problem.
A common example: working with large XML payloads in React Native. The standard approach is painfully slow, with the JS thread locked up during parsing. The solution is moving the parsing logic to the native side and only sending the processed result back to JavaScript.
That's why I built react-native-turboxml—it handles XML parsing natively and returns structured data without the bridge overhead choking your app.
The lesson applies broadly: if you're processing large amounts of data, consider whether that work belongs on the native side.
Images Are Probably Bigger Than You Think
Unoptimized images are silent performance killers.
Resize before rendering. Loading a 4000x3000 image to display at 400x300 wastes memory and processing power.
// Use a library like react-native-fast-image
<FastImage
source={{
uri: imageUrl,
priority: FastImage.priority.normal,
}}
resizeMode={FastImage.resizeMode.cover}
/>
Cache aggressively. Network requests for images add latency and drain battery.
Use appropriate formats. WebP typically offers better compression than JPEG or PNG with comparable quality.
Console Statements in Production
This sounds trivial, but it tanks performance in real apps.
console.log in React Native isn't free—it serializes data and sends it across the bridge. A few logs in a list item that renders 100 times adds up fast.
Use a babel plugin to strip them in production:
// babel.config.js
module.exports = {
presets: ['module:metro-react-native-babel-preset'],
env: {
production: {
plugins: ['transform-remove-console'],
},
},
};
Hermes: The Easy Win
If you're not using Hermes, enable it. It's React Native's optimized JavaScript engine and provides noticeable improvements in startup time and memory usage.
For React Native 0.70+, Hermes is enabled by default. For older versions, check your android/gradle.properties and ios/Podfile.
The difference is measurable—typically 20-30% faster startup and reduced memory footprint.
Using AI to Debug Performance Issues
Here's something that's changed how I approach performance debugging: AI tools are surprisingly good at analyzing React Native code for performance issues.
Try this with Claude or ChatGPT:
Prompt for re-render analysis:
Analyze this React Native component for unnecessary re-renders
and suggest optimizations:
[paste your component code]
Prompt for Flipper trace interpretation:
I'm seeing these metrics in my React Native Flipper performance trace:
- JS FPS: 45
- UI FPS: 60
- RAM: 280MB
The JS thread shows spikes during scroll. What should I investigate?
Prompt for FlatList optimization:
Review this FlatList implementation for performance issues.
The list has 500+ items and scrolling is janky on Android:
[paste your FlatList code]
AI won't catch everything, but it's remarkably good at spotting common patterns and suggesting specific fixes. It's like having a senior developer do a first-pass code review.
The Debugging Toolkit
Before you start optimizing, you need to measure. Here's what to use:
Flipper — Meta's debugging platform. The React DevTools plugin shows you exactly which components re-render and why.
React Native Performance Monitor — Shake your device or press Cmd+D in the simulator and enable the performance monitor. Watch for JS frame drops.
Systrace (Android) — For deep native performance analysis. Overkill for most issues, but invaluable when you need it.
Xcode Instruments (iOS) — Profile memory, CPU, and energy usage on iOS.
Where to Start
If your app feels slow, here's my recommended order of attack:
- Enable Hermes if you haven't
- Check for console statements in production
- Profile with Flipper to identify which components re-render excessively
- Audit your FlatLists
- Look at image sizes and caching
- Consider bridge overhead for large data operations
Most performance issues come from a small number of problematic patterns. Find the worst offender, fix it, measure again. Repeat until your app feels native.

