React Native Performance Optimization: Hermes, New Architecture, and Beyond
React Native has evolved dramatically. With the Hermes JavaScript engine now the default and the New Architecture (Fabric + JSI) rolling out across major apps, developers have powerful new tools to build truly performant mobile experiences.
Understanding the React Native Threading Model
React Native runs on three threads:
- JS Thread: Executes your JavaScript code
- Main/UI Thread: Handles native UI rendering
- Shadow Thread: Calculates layouts using Yoga
Traditional React Native bridged communication between JS and Native asynchronously, causing frame drops and jank. The New Architecture replaces this bridge with JSI (JavaScript Interface), enabling synchronous native calls.
Enabling and Optimizing Hermes
Hermes is a JavaScript engine optimized specifically for React Native. It precompiles JavaScript to bytecode at build time, reducing startup time and memory usage.
Enabling Hermes
In android/app/build.gradle:
project.ext.react = [
enableHermes: true
]
In ios/Podfile:
use_react_native!(
:hermes_enabled => true
)
Hermes Performance Characteristics
Hermes excels at startup time, memory efficiency, and Time to Interactive (TTI). Benchmark data from production apps shows 40-60% improvement in TTI with Hermes vs V8.
Profiling with Hermes
Use the Hermes profiler via Flipper or the standalone profiler:
npx react-native start --experimental-debugger
Migrating to New Architecture
The New Architecture consists of:
- Fabric: New renderer for synchronous UI operations
- JSI: Direct JS-to-native bindings without the bridge
- TurboModules: Lazy-loaded native modules via JSI
- Codegen: Type-safe native module interfaces
Step-by-Step Migration
Enable New Architecture in Android:
// android/gradle.properties
newArchEnabled=true
Enable New Architecture in iOS:
cd ios && RCT_NEW_ARCH_ENABLED=1 bundle exec pod install
Create a TurboModule spec file NativeMyModule.ts:
import type { TurboModule } from 'react-native';
import { TurboModuleRegistry } from 'react-native';
export interface Spec extends TurboModule {
multiply(a: number, b: number): Promise<number>;
getDeviceInfo(): Promise<{ model: string; os: string }>;
}
export default TurboModuleRegistry.getEnforcing<Spec>('MyModule');
Optimizing FlatList and VirtualizedList
FlatList is one of the most commonly misused components. Use getItemLayout when all items have fixed height:
const ITEM_HEIGHT = 80;
<FlatList
data={items}
getItemLayout={(data, index) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
})}
renderItem={({ item }) => <ItemRow item={item} />}
/>
Memoize renderItem
const renderItem = useCallback(({ item }) => (
<MemoizedItemRow item={item} />
), []);
const MemoizedItemRow = React.memo(({ item }) => {
return (
<View style={styles.row}>
<Text>{item.title}</Text>
<Text>{item.subtitle}</Text>
</View>
);
}, (prevProps, nextProps) => prevProps.item.id === nextProps.item.id);
Key windowSize and maxToRenderPerBatch
<FlatList
windowSize={5}
maxToRenderPerBatch={10}
initialNumToRender={8}
updateCellsBatchingPeriod={50}
removeClippedSubviews={true}
/>
Image Optimization
Use react-native-fast-image for better caching:
import FastImage from 'react-native-fast-image';
<FastImage
style={styles.image}
source={{
uri: 'https://example.com/image.jpg',
priority: FastImage.priority.normal,
cache: FastImage.cacheControl.immutable,
}}
resizeMode={FastImage.resizeMode.cover}
/>
Native Module Best Practices with JSI
With New Architecture, write JSI modules for performance-critical operations:
// ios/MyJSIModule.cpp
void MyJSIModule::install(jsi::Runtime& runtime) {
auto multiply = jsi::Function::createFromHostFunction(
runtime,
jsi::PropNameID::forAscii(runtime, "multiply"),
2,
[](jsi::Runtime& runtime, const jsi::Value& thisValue,
const jsi::Value* args, size_t count) -> jsi::Value {
double a = args[0].asNumber();
double b = args[1].asNumber();
return jsi::Value(a * b);
}
);
runtime.global().setProperty(runtime, "multiply", std::move(multiply));
}
Avoiding Common Performance Pitfalls
Avoid anonymous functions in render:
// Bad - creates new function each render
<TouchableOpacity onPress={() => handlePress(item.id)}>
// Good - stable reference
const handleItemPress = useCallback(() => handlePress(item.id), [item.id]);
<TouchableOpacity onPress={handleItemPress}>
Use InteractionManager for expensive operations:
import { InteractionManager } from 'react-native';
useEffect(() => {
const task = InteractionManager.runAfterInteractions(() => {
processLargeDataset(data);
});
return () => task.cancel();
}, [data]);
Measuring Performance
Use custom performance marks:
import { PerformanceObserver, performance } from 'react-native';
performance.mark('render-start');
// render code
performance.mark('render-end');
performance.measure('render-duration', 'render-start', 'render-end');
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
console.log(`${entry.name}: ${entry.duration}ms`);
});
});
observer.observe({ entryTypes: ['measure'] });
Conclusion
React Native performance optimization in 2026 is a multi-layered discipline. Start with Hermes and New Architecture adoption for foundational gains. Optimize your lists with proper FlatList configuration. Minimize your bundle with tree shaking and lazy loading. Profile early and often with Flipper. The combination of these techniques can bring your React Native app within striking distance of native performance.