Much of the performance advice written for older React Native apps starts with the bridge: reduce bridge traffic, batch native calls, avoid excessive JSON serialization. That advice belongs to an architecture that is no longer current.
The New Architecture became the default in React Native 0.76. React Native 0.82 removed the opt-out, making it the only supported runtime, while React Native 0.84 introduced Hermes V1 as the default engine in February 2026. The asynchronous JSON bridge that shaped React Native performance work for years is gone.
That does not make every React Native app fast. It changes where teams should look when an app is not fast. The likely causes are now inside the application: unnecessary renders, poorly virtualized lists, excessive startup work, expensive image handling, blocked threads, or an upgrade that has been postponed for too long.
Effective React Native performance optimization starts by identifying which part of the system is failing. A fix is useful only if it moves a number the team has measured.
"Slow" is not a diagnosis
Before changing code, capture five metrics:
Cold start to interactive (TTI): the time between tapping the app icon and reaching the first frame the user can act on.
Time to first meaningful draw (TTFD): when the important content on a particular screen becomes visible.
JS-thread FPS and UI-thread FPS: measured separately because either thread can fail while the other remains healthy.
Interaction-to-response latency: the delay between a tap and visible feedback.
Peak memory or RSS: a useful predictor of out-of-memory kills, particularly on inexpensive Android devices.
The frame budget is simple arithmetic. A 60 Hz display gives the app 16.6 milliseconds to produce each frame. At 120 Hz, the budget drops to 8.3 milliseconds. Miss that deadline and the frame is dropped.
This is also why average FPS is a weak diagnostic metric. A single 400-millisecond stall can disappear inside the average for an otherwise smooth minute, yet remain painfully obvious to the person using the app.
Test results also depend heavily on the device. Performance problems that never appear in an iPhone simulator can become immediate on a sub-$200 Android phone with older hardware. If that device class represents a meaningful part of the audience, benchmarking only on flagship phones produces an inaccurate picture of the product.
Platform metrics provide useful external thresholds. Google Play flags apps when user-perceived ANRs cross 0.47% of daily active users. Android vitals treats frames longer than 16 milliseconds as slow and frames longer than 700 milliseconds as frozen. On iOS, Xcode Organizer and MetricKit define launch time as the interval between the user tapping the icon and the first frame being drawn, typically reported at the 50th and 90th percentiles.
Record the baseline before optimizing. Otherwise, there is no reliable way to show that a change worked.
Metric
What it captures
Flagship target
Mid-tier Android target
Where to measure
Time from tap to the first interactive frame
Release build on a physical device; Xcode Organizer or Android vitals
Time until important content is visible
react-native-performance or custom marks
Headroom for logic and React render work
Perfetto or React Native DevTools
Scrolling and animation smoothness
60 FPS with no sustained over-budget frames
Risk of out-of-memory termination
Returns to baseline after repeated flows
Instruments or Android Studio Profiler
These figures are reasonable starting points, not universal standards. Set targets that fit the app, its users, and the devices it supports.
Find the overloaded thread before choosing a fix
Three execution areas account for most React Native performance symptoms.
The JS thread runs business logic, state updates, and React's render calculations. The UI or main thread handles native layout, view creation, gestures, and drawing. Native modules and background workers carry tasks such as networking, storage, and other platform operations.
The symptom often indicates where to look:
Taps fail to register or inputs respond late: the JS thread is probably blocked.
Scrolling stutters while the JS thread remains relatively idle: inspect the UI, commit, and mount pipeline.
Data arrives late but the screen is smooth once it appears: the bottleneck is probably in the network, storage, or data layer.
The New Architecture changed communication between these areas. JSI allows JavaScript to call native functions through direct C++ references, while Fabric uses a concurrent render, commit, and mount pipeline. React Native 0.82 removed the legacy opt-out, and subsequent releases stripped more bridge code from iOS and Android builds.
What it did not change is the cost of bad work. A synchronous JSI function that performs a heavy loop can still freeze the calling thread. Telling a team to "reduce bridge traffic" no longer addresses the actual problem. The useful question is which thread is saturated and what work can be removed, deferred, or moved elsewhere.
Unnecessary re-renders: fix the subscription before adding memoization
Re-renders often cause the vague sluggishness that users notice even when the app does not crash or freeze completely. The component tree is doing work, but much of that work produces no visible change.
Three patterns account for a large share of wasted renders.
A child component declared inside its parent's render function receives a new identity every time the parent runs. React may then unmount and rebuild it instead of reusing the existing component.
Unstable props cause a similar problem. A new object, array, or arrow function is a different reference on every render, even when its contents are identical. That defeats memoization further down the tree.
The third source is an unnecessarily broad state subscription. A component that reads an entire Context, subscribes to an unfiltered Redux state object, or uses a Zustand store without a narrow selector can re-render whenever any part of that state changes.
The appropriate fix depends on the cause:
Move component definitions to module scope.
Stabilize genuinely useful callbacks with useCallback.
Place expensive derived calculations behind useMemo.
Add React.memo around pure components when render cost justifies it.
Use selectors that subscribe only to the data a component displays.
Split an oversized Context into smaller, more focused contexts.
Batch related state updates so they trigger one render rather than several.
React Compiler changes part of this work. Its first stable release, version 1.0, arrived in October 2025 and supports React Native. It applies memoization at build time, including in code paths that are awkward to optimize manually, such as calculations after an early return.
Meta has reported double-digit load-time improvements and faster interactions after adopting the compiler. Those results come from Meta's applications and workloads, so they should not be treated as a universal benchmark. The compiler also cannot repair a poor state model, narrow an over-broad selector, or split a Context that contains unrelated data.
The sensible rollout is incremental: pin the compiler version, enable it on a controlled part of the application, and measure. It is not evidence that render performance has been solved everywhere.
Blanket memoization is rarely a good substitute for profiling. Each memoization boundary adds comparison work and retains data in memory. Applying it indiscriminately can slow the app, obscure the component tree, and make the few expensive renders harder to find.
Record the affected interaction in the React DevTools Profiler. Inspect the flame chart, identify components rendering without a meaningful input change, and compare render counts before and after the fix. If the count or render cost did not fall, the change did not address the problem.
Long lists: virtualization is necessary, but the cell still matters
Long lists tend to fail in several ways at once. Scrolling stutters, blank areas appear during fast movement, memory rises, and the first render takes too long. In many cases, the underlying mistake is using the wrong container.
ScrollView mounts all of its children immediately. That is reasonable for twenty small items. With two thousand items, it creates two thousand native view trees, increases memory consumption, and forces a large amount of work into the initial render.
Any list longer than a screen or two — or one whose eventual size is not controlled — usually needs virtualization. Only the visible rows and a limited buffer should remain mounted.
FlatList provides that virtualization, but its configuration affects the result:
A stable keyExtractor prevents unnecessary row remounts when data changes.
getItemLayout avoids repeated measurements when rows have a fixed size.
windowSize, maxToRenderPerBatch, and removeClippedSubviews balance memory use against the risk of blank cells during fast scrolling.
A stable renderItem prevents every visible row from being recalculated unnecessarily.
For New Architecture applications, FlashList v2 is a strong option. Shopify rebuilt it as a JS-only implementation that does not require size estimates, removing one of the more troublesome parts of v1. It recycles views through pools grouped by getItemType and supports layouts such as masonry grids.
Shopify reports 60 FPS on lists containing thousands of items. That remains a vendor benchmark and should be reproduced on the app's own cells, data, and target devices.
The architectural restriction is firm: FlashList v2 is New-Architecture-only. An application that remains on the old architecture cannot adopt it.
Capability
ScrollView
FlatList
FlashList v2
getItemLayout helps with fixed-height rows
No estimates required; items are measured automatically
Short, known-length content
Moderate lists with relatively simple rows
Hundreds or thousands of rows, especially with complex cells
Virtualization does not make an expensive row cheap. Nested lists, multiple shadows, large image decodes, and complicated layout calculations can make a cell jank in any container. Profile the row by itself before replacing the list component.
The benchmark should also be repeatable. Use a scripted scroll rather than dragging a thumb across the screen. Shopify's react-native-performance can report list TTI and blank area, while Flashlight or Perfetto can capture an FPS curve during an automated fling.
Slow cold start requires work on the startup path
Render optimization does not automatically improve startup. Cold start has its own sequence:
-
Native process initialization
-
JavaScript bundle loading
-
JavaScript execution
-
First render
-
Data hydration
Each stage has different costs.
Hermes compiles JavaScript to bytecode ahead of time, allowing the engine to skip much of the parsing work during launch. Inline requires defer module initialization until the module is first used. Metro tree shaking and screen-level code splitting reduce how much code must load before the first frame.
Third-party initialization deserves particular scrutiny. Analytics, attribution, crash reporting, feature-flag clients, and other SDKs often start at launch even though the app does not need them before the first interactive frame. Moving nonessential initialization off the critical path can improve TTI without removing the SDK.
Navigation structure can also delay startup. react-native-screens backs screens with native containers so inactive screens do not remain fully mounted as live view trees. Lazy screen mounting avoids creating the entire navigation hierarchy at launch. At the root of the app, inspect blocking await chains that postpone the first render until storage or network requests have completed.
Android has additional startup levers:
R8 and ProGuard reduce and optimize bytecode.
ABI splits prevent the app from shipping every native architecture to every device.
Baseline or startup profiles allow the runtime to precompile hot code paths rather than interpret them on the first run.
React Native 0.84 made Hermes V1 the default, added precompiled iOS binaries, and raised the Node.js requirement to 22.11 or later. Apps already using Hermes did not need a separate engine migration.
Claims that Hermes V1 provides "43% faster cold starts" or "26% lower memory" are widely repeated, but those figures generally lead back to agency articles rather than a published benchmark with a clearly stated device and methodology. Upgrade for the supported runtime and engineering improvements, then measure the application's own cold-start distribution before and after.
Animation jank is usually a thread problem
The legacy Animated API can drive animation calculations from the JS thread. If that thread is also processing state updates, parsing data, or running business logic, the animation competes for the same frame budget.
useNativeDriver still helps for supported properties such as transform and opacity, because it moves their execution to the UI thread. It does not support every property, including many layout and color changes, and it cannot help when values are recalculated in JavaScript on every frame.
For interactive animations and gestures, the common choice is Reanimated with React Native Gesture Handler. Reanimated executes small animation functions, or worklets, on the UI thread. The gesture can therefore continue smoothly while the JS thread is occupied.
Reanimated 4 was the first stable version built around the New Architecture. It moved worklet infrastructure into the separate react-native-worklets package and added a declarative, CSS-style animation API alongside the imperative worklet model.
Property choice matters as much as the library. transform and opacity are relatively cheap because they can be composed without recalculating layout. Animating width, height, top, or left forces layout work on every frame. This is a common reason an animation that looked acceptable during design review performs badly in production.
Do not approve animation performance by watching it on a flagship phone. Test the release build on mid-tier hardware and capture UI-thread FPS. Development mode may hide the concurrent workload the animation will compete with in production.
Images consume memory at decoded size
A 4000×3000 photo shown inside a 300-pixel thumbnail may still be decoded at its full source dimensions. The downloaded file might be compressed, but the decoded bitmap occupies tens of megabytes in memory. Repeat that across a feed and memory pressure rises quickly.
Resize assets on the server or through CDN transformations so the device receives dimensions appropriate for the display. Set resizeMode or contentFit correctly, cache through a component designed for image workloads, and restrict prefetching to assets likely to appear above the fold.
Expo Image is a common default for current applications, while FastImage remains an established alternative. Expo Image uses SDWebImage on iOS and Glide on Android, performs decoding away from the JS thread, and downsamples images toward their target size. Its documented cachePolicy options are none, disk, memory, and memory-disk, with memory-disk as the default.
WebP and AVIF can reduce network payload compared with JPEG, provided the backend pipeline and supported OS versions can serve and decode them reliably.
Memory leaks reveal themselves over time
Some performance failures are difficult to reproduce in a short profiling session because the application becomes slower only after extended use.
Typical causes include:
useEffect subscriptions without cleanup
Timers and intervals that continue after unmount
Event listeners that are never removed
Navigation parameters retaining large objects
Animation loops that survive the screen that created them
In-memory caches without a size limit or eviction policy
The characteristic signal is not simply high memory. It is memory that rises through a repeated interaction and does not return toward its baseline after the screen is closed.
Run a soak test that enters and leaves the same screen fifty times. Track allocations with Xcode Instruments on iOS or Android Studio Memory Profiler on Android. A retained-object curve is easier to see in this repeated flow than in a single heap snapshot.
Low-RAM Android devices are particularly exposed. Instead of gradually slowing down, the app may be terminated by the operating system once its memory allocation becomes unsustainable.
Architecture debt can block every other fix
Performance guidance written for modern React Native assumes the app is already using the New Architecture. For applications pinned to older versions, that assumption may be the main obstacle.
FlashList v2 and Reanimated 4 will not run on the legacy architecture. As more packages remove legacy support, teams that postpone migration lose access to current libraries before they encounter a visible framework failure.
The difficult part of an upgrade is often the dependency audit rather than the application's JavaScript. Every native module must support the New Architecture. An abandoned module can force the team to replace the dependency, maintain a fork, or build a compatibility layer. These costs frequently appear only after the upgrade has started and a package that has not been touched in years breaks the build.
TurboModules and JSI reduce native interop overhead by removing JSON serialization and allowing direct access. They do not make native work free. A synchronous call that performs an expensive native operation still blocks the thread that invoked it.
Migration effort depends on several variables:
The volume of custom native code
The number and condition of native dependencies
Changes required in CI and build infrastructure
New toolchain requirements, such as Node.js 22.11+ for React Native 0.84
The amount of regression testing required across the application
Practitioner reports range from a few days for a small, dependency-light app to several weeks for a large product. That range describes the shape of the work, not a reliable quote for a specific application.
React Native's support policy covers the latest stable release and the previous two minor versions. Remaining on an old version eventually means losing security updates as well as library compatibility.
Teams comparing an internal migration with external support may evaluate experienced React Native development companies against the cost of taking their own engineers away from roadmap work.
Some of the cheapest wins are in the data layer
A screen can render at 60 FPS and still feel slow because the data arrives late.
Over-fetching transfers fields that the interface never displays. Waterfall requests wait for one response before starting another even when the calls could run in parallel. Unbatched updates can trigger a new render as each piece of data arrives.
A caching and request-deduplication layer such as TanStack Query can collapse identical requests and show cached data while revalidating it in the background. Pagination keeps initial payloads manageable. Prefetch-on-intent can begin loading the next page as the user approaches it.
Optimistic updates provide visible feedback before the server confirms an action. Skeleton states show the structure of the incoming screen instead of an indefinite spinner. In offline-first flows, stale-while-revalidate allows the cached result to appear immediately while the network refresh runs in the background.
These decisions affect perceived performance because human response thresholds are short. Nielsen Norman Group's established limits are 0.1 second for an instantaneous response, 1 second for maintaining uninterrupted flow, and 10 seconds for retaining attention.
Perceived-performance techniques have a boundary. A skeleton screen cannot repair a frozen JS thread, and an optimistic update cannot make a blocked gesture respond. They improve how waiting is presented; they do not eliminate thread saturation.
A profiling workflow that avoids random optimization
Performance work is often wasted because the team applies the right technique to the wrong problem. Memoization cannot fix an unvirtualized list. Replacing FlatList will not shorten SDK initialization at launch.
Use a repeatable sequence:
-
Reproduce the problem in a release build on a physical, mid-tier device.
-
Capture a trace of the slow interaction.
-
Determine which thread or stage is responsible.
-
Isolate the component, function, native call, or request causing the delay.
-
Apply one targeted change.
-
Repeat the same measurement on the same device and build.
-
Move to the next issue only after confirming the result.
Diagnostic job
Tool
Platform
Useful signal
Render cost and wasted renders
React Native DevTools and React DevTools Profiler
Component render counts and flame chart
JS functions consuming the most time
Thread saturation and frame timing
Perfetto or Android Studio Profiler
Per-thread load and dropped frames
Allocations, leaks, and hang causes
Scripted device benchmarking
FPS and CPU measurements on physical hardware, including CI
Shopify react-native-performance
Time to interactive and visible blank area
Flipper is intentionally absent. First-party Flipper integration was deprecated in React Native 0.73 and later replaced by React Native DevTools, which has shipped as a stable built-in tool since 0.76.
Development measurements can also mislead. The development bundle is not minified, production Hermes optimizations are unavailable, and Strict Mode intentionally invokes some work twice. Profile in development to find likely causes, but confirm every performance result in a release build.
Once the problem is fixed, prevent it from returning. Reassure can run render-performance tests in CI and fail a build when a component's render cost regresses. Production real-user monitoring through Sentry, Firebase Performance Monitoring, or Datadog then shows how the app behaves across the actual device distribution rather than a small lab sample.
React Native performance triage matrix
A list of possible problems is not a plan. Prioritization requires a connection between the symptom, the profiler signal, the likely fix, and the way success will be verified.
Symptom
Likely source
Confirming signal
Primary fix
Effort
Verification
Taps lag and the interface feels sluggish
Components render without relevant prop or state changes
Extract components, narrow selectors, memoize measured hotspots, and evaluate React Compiler
Compare render counts and interaction latency
Scrolling janks, blank cells appear, and memory rises
Blank area and FPS drops during a scripted scroll
Virtualize the list; tune FlatList or adopt FlashList v2
Compare FPS curves and blank-area measurements
Cold start takes too long
JS and native startup path
Slow TTI in a release-build cold-start trace
Add inline requires, defer SDKs, use baseline profiles, and upgrade to Hermes V1
Compare TTI on the same mid-tier device
Gestures or animations drop frames
UI-thread FPS falls in a release trace
Move animation logic to Reanimated worklets and favor transform or opacity
Compare UI-thread frame timing
The app slows down over time or crashes from memory pressure
RSS increases through repeated navigation and never returns
Clean up effects, timers, listeners, and animation loops; cap caches
Run a repeated-navigation memory soak
Current libraries cannot be installed
Architecture and version debt
Build failures or New Architecture incompatibilities
Upgrade React Native and audit native dependencies
Produce a green build on a supported release
Content appears late despite smooth rendering
Network waterfall without thread saturation
Add caching, request deduplication, parallel loading, optimistic UI, and skeletons
Measure interaction and content latency
Clear version and architecture blockers first because other fixes may depend on current libraries. Establish the baseline next. Then take the high-impact changes that can be verified quickly — often re-renders and list behavior — before scheduling larger structural work.
Close the loop with a performance budget. Define targets for each important screen: cold start below a stated threshold, no sustained over-budget frames during scripted scrolling, and an ANR rate below Google Play's 0.47% line. Assign ownership and enforce the measurable parts in CI.
Sometimes the diagnosis is a capacity problem rather than a technical mystery. No one owns mobile performance, the team has no representative device lab, no baseline exists, and migration work keeps losing priority to product delivery. Under those conditions, a time-boxed audit and remediation engagement can establish the measurements, remove the upgrade blockers, and return the application with regression gates in place. A company can hire React Native developers for that defined period without turning a temporary migration need into permanent headcount.
Modern React Native has removed several framework-level bottlenecks that once dominated optimization work. The remaining challenge is diagnosis. A team that measures the failing interaction, maps it to the correct thread, and verifies each change will usually outperform one working through a generic checklist of performance tricks.
FAQ
Is React Native still slower than native in 2026?
For most conventional product applications, the difference is not meaningful enough to treat the framework as the first suspect. The New Architecture, Hermes V1, and disciplined application code have narrowed many of the historical gaps.
Native implementations still make sense for workloads dominated by heavy real-time graphics, some advanced camera pipelines, or demanding on-device machine learning. Those cases should be evaluated against their actual constraints rather than used as a general rule for every app.
How much performance improvement should we expect from migrating to the New Architecture?
There is no dependable universal percentage. The result depends on the application's code, native modules, startup path, and UI workload. Many headline figures do not disclose the test device, build configuration, or methodology.
The more predictable benefit is compatibility: migration keeps the app inside the support window and unblocks current versions of libraries such as FlashList and Reanimated. Measure performance on the same devices before and after the upgrade.
Do we need to replace FlatList with FlashList in every list?
No. FlatList remains appropriate for short and moderately sized lists with simple rows. FlashList v2 becomes more relevant when the app has hundreds or thousands of items, complex cells, visible blank areas, or measured frame drops.
FlashList v2 also requires the New Architecture, which may make migration the first task.
How long does a React Native performance audit take?
The duration depends on custom native code, dependency count, the number of important user flows, and the availability of representative test devices. A small app may take a few days to assess. A large product with native integrations and accumulated version debt can require several weeks.
A time-boxed audit is useful because it defines which flows will be profiled, which device classes will be tested, and which fixes are included.
Which device should we benchmark on?
Use a mid-tier Android phone running a release build. It is more likely to expose constrained CPU, memory, and storage behavior than an iPhone simulator or a current flagship device.
Flagships still belong in the test matrix, particularly for 120 Hz behavior, but they should not be the only benchmark.
Does React Compiler remove the need for manual memoization in React Native?
It removes much of the routine memoization for code patterns it supports, including some cases that are difficult to cover with manual hooks. It does not fix state subscriptions that are too broad, poorly structured Context data, expensive list cells, or business logic that blocks the JS thread.
Adopt it incrementally, pin the version, and verify the result with the profiler.
How do we stop performance regressions from shipping again?
Turn performance expectations into automated checks. Run render-performance tests with tools such as Reassure, define budgets for important interactions, and fail the build when a measurable regression crosses the agreed limit.
Pair CI checks with production monitoring. Real-user data across different device classes should determine which performance issue the team investigates next.