DevelopmentAll levels

React Native

React Native is a framework that runs React components in a JavaScript engine on the phone and turns them into real iOS and Android views rather than a web page. That split is why a web team can ship an app that feels native — and why its typical failure is a screen that stops responding while JavaScript is busy. The page opens with sixteen things a screen does and what keeps moving when the JavaScript thread is blocked, then covers the New Architecture, lists and images, Expo and over-the-air updates, releases up to 0.87 and the stores’ 2026 deadlines, what is really shared, five documented cases including Shopify’s return to native in September 2026, and where it degrades.

Current version
0.87 · Aug 2026; 0.88 due 12 Oct
Model
Your JS on one thread, real native views
Support
The latest three minors; a new one about every two months

What keeps moving when JavaScript is busy — sixteen things a screen does

A React Native screen is two programs talking to each other. Your React code runs on one JavaScript thread. The views the user touches belong to the platform’s thread, also called the main thread. Most of the time the difference is invisible. It becomes visible the moment the JavaScript thread is busy — a large response being parsed, a long list re-rendering, a heavy calculation — because anything that needs JavaScript waits, and anything that does not keeps moving. So the useful question about any part of a screen is not “is React Native fast?”. It is “which thread runs this, and what does the user see if that thread is busy for half a second?”. Here are sixteen things a screen does. Later sections cite the rows by number.

#What the screen doesWhere it runsIf JS is busy for 500 msThe control
1Scrolling a list that is already renderedUI thread — a native scroll viewKeeps scrolling smoothlyNothing: native scrolling is the default
2An animation started with useNativeDriver: trueUI thread, once startedKeeps runningThe native driver wherever it is supported; since 0.85 it can also animate layout properties (experimental)
3A gesture tracked by Gesture Handler and a Reanimated workletUI threadFollows the fingerGesture Handler with Reanimated for anything that follows a finger
4Decoding and drawing imagesNative image pipeline, off the JS threadImages keep loadingRequest images at the size they are shown — decoded, each pixel costs 4 bytes
5A native-stack screen transition already under wayUI thread (react-native-screens)Finishes smoothlyA native stack navigator rather than one animated from JS
6Typing into a TextInputThe field is native; onChangeText runs in JSCharacters keep appearing; a controlled value set on every keystroke can fight the native textKeep free-text fields uncontrolled (defaultValue) and the keystroke path cheap
7A tap on a PressableTouch detected natively; onPress and a pressed style run in JSThe action — and on iOS the press feedback — arrives lateShort handlers; native feedback (android_ripple, Gesture Handler buttons)
8A header that follows the scroll through onScroll and setStateJS thread, on every scroll eventLags behind the finger, then jumpsAnimated.event with the native driver, or Reanimated’s useAnimatedScrollHandler
9An animation driven by setState, or by Animated without the native driverJS thread, every frameFreezes, then jumps to the endRows 2 and 3
10New rows of a virtualised list appearing during a scrollJS thread renders themBlank cells where rows have not rendered yetFlashList, or a tuned FlatList (getItemLayout, windowSize), with cheap memoised rows
11A timer — setTimeout, setIntervalJS threadFires lateNever time an animation or a deadline with a JS timer
12A fetch response arrivingThe request goes over native networking; the callback runs in JSThe data reaches the screen late — the request itself was not slowerSmall responses, pagination, no parsing in the render path
13Re-rendering after a state change — React’s render plus Yoga layoutJS threadThe screen shows the previous state until both finishMove state down, memoise, React Compiler — the same rules as on the web
14A synchronous native-module call through JSIJS thread waits for the native codeBlocks JS for as long as the native call takesSynchronous only for microsecond work; disk and network are asynchronous
15Parsing a 5 MB JSON, sorting 10,000 items, a heavy reducerJS threadThis is the 500 msAsk the server for less; precompute; move the work into native code
16App start — loading the bundle and rendering the first screenJS threadThe splash screen stays upHermes bytecode (the default); less work before the first screen; screens loaded when opened
One illustrative stretch of 800 ms in which a 5 MB JSON.parse holds the JavaScript thread for half a second. Everything that runs on the UI thread carries straight through: scrolling a list that is already rendered, an animation started with the native driver, a gesture tracked by Reanimated. Everything that needs a round-trip through JavaScript stops: an animation driven from JavaScript freezes and then jumps, a tap gets its onPress 440 ms late, and a virtualised list shows blank cells because new rows are rendered in JavaScript. The fix is two moves — put what must stay smooth on the UI thread, and make the JavaScript work smaller.

Two threads and a crossing: the New Architecture

A <View> in React Native is not a div and not a drawing. It becomes a real UIView on iOS and a real android.view.View on Android, created and drawn by the platform. That is why scrolling has the right physics, why the keyboard behaves correctly, and why screen readers work without extra effort — none of that is reimplemented; the operating system is doing its own job. It is also why an operating-system redesign reaches a React Native app, component by component, as soon as the app is rebuilt — the Flutter page draws that difference for iOS 26.

Since React Native 0.82 the New Architecture is the only one. Your React code runs on the JavaScript thread, and in the common case so do rendering and layout: React builds a shadow tree in C++ and Yoga measures it there. The UI thread — the platform's main thread — only applies the resulting changes to real native views, and it owns native scrolling and gestures. The two sides talk through JSI, which calls across directly instead of the old bridge's batched JSON messages. Anything that needs the JavaScript thread waits while that thread is busy, which is why a gesture animated on the UI thread stays smooth while one animated from JavaScript stutters.

A change reaches the screen in three steps. First, render: React runs your components on the JavaScript thread and builds a tree of elements, and the renderer turns it into a shadow tree — a copy of the screen’s structure kept in C++. Second, layout: Yoga, a flexbox engine written in C++, computes the size and position of every node. In the common case both happen on the JavaScript thread; an urgent update, such as the response to a touch, can be rendered synchronously on the UI thread instead. Third, mount: the UI thread compares the new tree with the one on screen and applies only the difference — create this view, update that one, remove the third. The UI thread never runs your React code. It only applies the result.

The original design connected the two sides with a “bridge”: every message was serialised to , queued, and delivered in batches, asynchronously. It worked, and it produced the framework’s signature failure — a busy JavaScript thread sent its instructions late and frames were dropped. The New Architecture replaces it with three parts. JSI, the JavaScript Interface, lets JavaScript hold a direct reference to a C++ object and call it synchronously, with no JSON in between. Fabric is the renderer described above, built on JSI. TurboModules are native modules loaded when first used rather than all at start-up, with their types generated by Codegen from a typed specification, so a mismatch between the JavaScript and the native signature fails at build time.

VersionReleasedWhat changed
0.68Mar 2022The New Architecture available as an opt-in
0.74Apr 2024Bridgeless mode on by default under the New Architecture
0.76Oct 2024The New Architecture on by default; React Native DevTools ship
0.79Apr 2025JavaScriptCore moves to a community package; Chrome remote debugging removed
0.80Jun 2025Legacy Architecture frozen: no fixes, no testing, warnings in DevTools
0.82Oct 2025New Architecture only: newArchEnabled=false is ignored
0.84Feb 2026Hermes V1 by default; legacy code removed from iOS builds; precompiled iOS binaries
0.85Apr 2026A new animation backend for Animated and Reanimated, built with Software Mansion
0.87Aug 2026Strict TypeScript API by default; deep imports removed; Swift Package Manager (experimental)

The JavaScript engine is Hermes, built by Meta for React Native. It compiles JavaScript to when the app is built, so the phone does not parse source code at start-up — that is row 16’s first control. Hermes V1, a rebuilt compiler and virtual machine, became the default in 0.84; on the Expensify app the React Native team measured total time to interactive 7.6% faster on Android and 2.5% faster on iOS. JavaScriptCore, the engine Safari uses, left the core in 0.79 and lives on as a community package.

Lists, re-renders and images: where “React Native is slow” comes from

Almost every “React Native is slow” report resolves to rows 10, 13 or 15 — a list, an unnecessary re-render, or too much work on the JavaScript thread — or to row 4, an image nobody resized. Lists come first because they are on nearly every screen. A long list built with .map() inside a ScrollView mounts every row, visible or not. A FlatList is virtualised instead: it mounts a window of rows around the screen and unmounts the rest. By default it renders 10 rows first, then adds 10 per batch every 50 ms, and keeps a window 21 screens tall — ten above, ten below, and the visible one. When the user flings faster than JavaScript can render new rows, the list shows empty space where rows should be. The documentation calls these blank areas.

Rendering a 1,000-item list with .map() inside a ScrollView mounts all 1,000 rows, whether or not anyone scrolls to them. A FlatList mounts a window around the screen — by default 21 screens' worth, ten above and ten below — and renders new rows in JavaScript as the window moves, so a fast fling can outrun it and show blank cells. FlashList v2 keeps roughly one screen of cells and re-binds a cell that leaves the top to the next item at the bottom, so it creates almost nothing while scrolling. The strips are not to scale.
SettingDefaultWhat it trades
keyExtractorindexA stable id per item keeps a row’s state with the row when items are inserted
getItemLayout—Rows of known height skip measuring; scrolling to item 500 becomes arithmetic
windowSize21Larger: fewer blank areas, more memory. Smaller: the reverse
initialNumToRender10Enough to fill the first screen and no more — it is on the start-up path
maxToRenderPerBatch10Larger batches fill faster and hold the JS thread longer per batch
removeClippedSubviewstrue on AndroidDetaches off-screen views from the native hierarchy; saves work, can cause glitches

FlashList, Shopify’s list, takes a different approach: it recycles. Instead of mounting and unmounting rows, it keeps roughly one screen of cells plus a small buffer and, as a cell scrolls off the top, re-binds it to the item entering at the bottom. Version 2, released in 2025, runs only on the New Architecture and no longer needs size estimates. It is downloaded about two million times a week. Since 10 September 2026 it also carries a lesson of its own: Shopify is leaving React Native and says it is “in discussions with several companies about taking on long-term stewardship” — so a library you depend on now has a sponsor you need to watch. Whatever the list, the row is still the unit of cost. A list cannot make a slow row fast; memoise the row component, keep it cheap, and give it a stable key.

Row 13 follows the same rules as React on the web — the React page covers them: keep state as low in the tree as it can live, memoise what receives unchanged props, let the React Compiler do the memoising where it is enabled. One rule is specific to mobile. A navigation stack keeps the screens behind the current one mounted, so they keep their scroll position and state — and they also keep re-rendering whenever a shared store changes. React Navigation’s freezeOnBlur stops re-renders of screens the user cannot see; Coinbase built the same thing by hand in 2020, and the cases section says what it cost them not to have it.

Row 4 is memory, not speed. An image is decoded before it is drawn, and a decoded image costs width × height × 4 bytes, whatever its file size. A 12-megapixel photo — 4,000 by 3,000 pixels — is 48 MB in memory even if the JPEG was 2 MB; a feed of ten of them is close to half a gigabyte, and on a mid-range Android phone the system kills the app long before that. Ask the server or the CDN for the size you actually display, and use an image component with a disk and memory cache, such as expo-image.

Expo, native code and over-the-air updates

Expo used to be a sandbox you eventually had to leave. It is not any more, and React Native’s own documentation now tells you to start a new app with a framework and names Expo. It gives you a build service, over-the-air updates, a large set of maintained native modules, a file-based router, and — the part that decides how a project ages — config plugins. A config plugin describes a change to the native projects as code: add this permission, register that . The ios and android directories are then generated rather than edited by hand, so an upgrade regenerates them and reapplies your changes. A project whose native directories were edited by hand turns every upgrade into a merge against its own edits; those are the projects still three versions behind two years later.

Expo ships an SDK release for each React Native version it adopts, after it has tested the libraries together. SDK 57, released on 30 June 2026, runs React Native 0.86. SDK 58 has been in beta since 15 September 2026 on the 0.88 and becomes stable shortly after 0.88 does. For a team that is the practical meaning of “upgrade one version at a time”: one SDK at a time, with npx expo install --fix aligning every Expo library to it.

Over-the-air updates and their exact limit

Because the app’s logic is JavaScript, you can send a new bundle to installed apps without going through store review — an over-the-air (OTA) update. A fix reaches users in minutes instead of days. Both stores allow it, within written limits. Apple’s Developer Program License Agreement, section 3.3.1(B), says interpreted code may be downloaded only if it does not change the app’s primary purpose, does not bypass signing, the sandbox or other security features, and does not create a store for other apps. Google Play forbids downloading executable code such as .dex, JAR or .so files from anywhere but Play, and exempts code that runs in a virtual machine or an interpreter, such as JavaScript — which must still obey every other Play policy.

An over-the-air update replaces the JavaScript bundle and assets of an installed app, but not its native code. Build 1.5 added a camera module that build 1.4 does not have, and an update written against 1.5 calls that module. Sent everywhere, it crashes every phone still on 1.4. Expo's runtime version — computed automatically by the fingerprint policy from everything that affects native code — ties each update to the builds it was made for, so 1.4 simply does not receive it and waits for the store release. Changing a native module, a permission, the React Native version or the app icon always needs a new binary and a new review.

The technical limit is stricter than the legal one. An update replaces JavaScript and assets — images, fonts, text — and nothing else. Anything that changes native code needs a new binary and a new review: adding a library with a native module, changing a permission, upgrading React Native, changing the icon. The dangerous case is the one in between: an update written against a newer build reaching phones that still run an older one. The JavaScript calls a native module that is not there, and the app crashes at launch. Expo prevents this with a runtime version — a label for the native layer that every build and every update carries, so an update is offered only to builds with the same label. The fingerprint policy computes the label from everything that can affect native code, which is safer than bumping it by hand and remembering to.

Releases, upgrades and the stores’ 2026 deadlines

React Native ships a minor version — 0.86, 0.87 — about every two months, and the release crew maintains the latest three. The newest is “Active” and gets frequent patches; the oldest of the three is in “End of Cycle” and gets fixes only for important regressions; everything older is “Unsupported”. In practice a version is supported for about six months. Since 0.83, in December 2025, roughly every second release is planned to break nothing users can see — 0.83 and 0.86 were — to make the cadence easier to keep.

VersionReleasedStatus on 2 Oct 2026
0.89planned 7 Dec 2026Future
0.88due 12 Oct 2026Release candidate
0.8710 Aug 2026Active
0.869 Jun 2026Active
0.856 Apr 2026End of Cycle — moves out when 0.88 ships
0.84 and older—Unsupported

Upgrading one minor version at a time is dull and takes an afternoon. Upgrading four at once takes a fortnight, because the framework, the navigation library, every native module and both platform SDKs move together, and a crash after the upgrade could come from any of them. The React Native Upgrade Helper shows the exact diff of the template between any two versions, which is most of the native-side work in a bare project. The release table above is also a budget: six months of support means a team that upgrades twice a year is never on an unsupported version, and a team that upgrades once a year always is.

Which React Native are you looking at

What you see in the codeWhat it tells you
newArchEnabled=false in gradle.propertiesSomebody opted out of the New Architecture; since 0.82 the flag is ignored
import … from 'react-native/Libraries/…'A deep import from before the stable API of 0.80; it stops compiling on 0.87
InteractionManager.runAfterInteractionsRemoved in 0.87 — schedule deferred work another way
react-native-code-pushApp Center’s CodePush, shut down on 31 March 2025 — the OTA channel is gone
JavaScriptCore configured as the engineA community package since 0.79; Hermes is the default and the tested path
Debugging through Chrome’s remote debuggerRemoved in 0.79; React Native DevTools replaced it
Hand-edited ios/ and android/ and no config pluginsEvery upgrade is a merge; budget for it or move to generated native projects

The stores’ deadlines — inherited from both platforms

A cross-platform app is not exempt from either platform’s calendar; it has both. Each store raises its minimum build requirements on a fixed date, and the change reaches React Native through the native projects and through every native dependency. These are the ones in force or arriving around this page’s date.

DeadlineWhat it requiresWhat it means for a React Native app
Apple, from 28 Apr 2026Uploads built with Xcode 26 and the iOS 26 SDKNative components take on the Liquid Glass look, JS-drawn ones do not; React Native 0.87 itself requires Xcode 26
Apple, apps built with the iOS 27 SDKThe scene-based (UIScene) lifecycle, or the app does not launch; the Liquid Glass opt-out key is ignoredThe app template and every native module that hooks the app lifecycle must support scenes; Expo SDK 58 is built for iOS 27
Google Play, from 1 Nov 2025 (extensions to 31 May 2026)Apps targeting Android 15 or later support 16 KB memory pagesEvery .so file in every native dependency must be built aligned to 16 KB; React Native core supports it since 0.77
Google Play, from 31 Aug 2026 (extensions to 1 Nov 2026)New apps and updates target Android 16 (API level 36)Edge-to-edge can no longer be switched off; predictive back is on by default and onBackPressed is not called; on screens 600 dp or wider, orientation locks are ignored (a temporary opt-out ends with API 37)

How much you actually share — and the native work that stays

The promise people hear is one codebase for two platforms. The honest version is that business logic, networking, state and most screens are shared, and the rest is not. Navigation patterns differ, the back gesture exists on one platform and not the other, permission dialogs behave differently, and each store has its own rules about what you must show. React Native gives you Platform.select and .ios.tsx / .android.tsx files precisely because the last part is expected to diverge.

ConcernShared?What that means in practice
Business logic, API, stateYes, entirelyThe bulk of what you write, and the reason the approach pays off at all
Screens and componentsMostlySame code, with platform branches for spacing, headers and back behaviour
Navigation feelPartlyThe library abstracts it; users still expect their platform’s conventions
Permissions, push, deep linksNoConfigured per platform, and each store reviews them on its own terms
Widgets, watch apps, App Intents, Live ActivitiesNoWritten natively; React Native does not run there
Release and reviewNoTwo pipelines, two sets of certificates, two review queues — and both deadline calendars above

When a capability has no JavaScript , you write a native module — a small piece of Swift or Kotlin exposed to JavaScript. This is normal, not a failure of the approach, but it is the moment a team needs somebody who reads the platform’s documentation. Shopify, after five years of moving every app to React Native, wrote in 2025 that “100% React Native should be an anti-goal”: home-screen widgets, watch apps, on-device models and Siri shortcuts were better built natively. A React Native team with no one who understands the native side eventually stalls on a problem no amount of JavaScript can reach.

Five cases, and what each one was missing

Each case is documented by the people it happened to — engineering blogs, a security advisory, a vendor’s shutdown notice. Between them they cover the decision to adopt, the cost of re-renders, a hosted dependency disappearing, the development tools as an attack surface, and the decision to leave.

CaseWhat happenedWhat was missing
Airbnb, 2016–2018React Native inside two large native apps; three platforms to support instead of two; new React Native work halted in June 2018A decision about scope — a whole app or a bounded flow — and a team that owned the bridge
Coinbase, Nov 2020Tab switches took over a second; every mounted screen re-rendered on every API responseRender counts measured as features landed; screens out of view frozen
CodePush, 31 Mar 2025Microsoft retired App Center, and the OTA service many React Native apps used went with itAn exit plan for a hosted dependency
Metro4Shell, Nov 2025 – Feb 2026CVE-2025-11953: the dev server listened on the network and passed a request to the shell; exploited from 21 Dec 2025Dev servers bound to localhost; dev tools patched like production dependencies
Shopify, 2020 – Sep 2026Five years moving every app to React Native, then a move back to Swift and KotlinNothing — the assumption under the decision changed, and they re-checked it

The first case is the one every React Native discussion still cites, usually wrongly. In June 2018 Gabriel Peal published Airbnb’s account of two years with React Native. The headline was performance; the substance was structure. Airbnb had added React Native to two large native apps that kept moving fast: 80,000 lines of product code across 220 screens, plus 40,000 lines of JavaScript infrastructure, inside apps with about ten times the code and four times the screens on each platform. To make product work possible, the infrastructure team wrapped networking, experiments, internationalisation and shared-element transitions for React Native — and kept those wrappers in step with native code that never stopped changing. In their words, they “wound up supporting code on three platforms instead of two”. They also carried about fifty commits patched on top of React Native, which made every upgrade painful, and the start-up time of the JavaScript runtime hurt launch screens and deep links. New React Native work stopped, and the most-used screens were scheduled to return to native by the end of 2018. Most engineers who used it still rated it well. The lesson is about scope, not speed.

The third case is a service that simply ended. CodePush, Microsoft’s over-the-air update service, was the default way to ship JavaScript fixes to React Native apps for years. It lived inside App Center, and Microsoft retired App Center on 31 March 2025 — CodePush with it. The standalone server Microsoft offered for self-hosting was archived two months later. Teams that had treated OTA as part of their incident response lost it on a fixed date, and an app whose installed binaries pointed at the old service could no longer receive fixes at all; only a store release could move them. The fix was always available — Expo’s updates, or a compatible service — but it needed a native change, and so a review. An exit plan for a hosted dependency is cheap to write while the service still works.

The fourth case attacked developers, not users. On 3 November 2025 JFrog disclosed -2025-11953, rated 9.8 out of 10, in @react-native-community/cli-server-api — the package behind the Metro development server that npx react-native start runs. The server listened on all network interfaces rather than only the developer’s own machine, and one of its endpoints passed part of a request to a function that opens files and URLs through the operating system. Anyone who could reach the port could run commands on the developer’s computer; on Windows, with arguments of their choosing. Version 20.0.0 fixed it. VulnCheck saw it exploited from 21 December 2025 to deliver malware, and CISA added it to its catalogue of exploited vulnerabilities on 5 February 2026. A development server is a server: bind it to localhost, keep it off untrusted networks, and update the tooling as promptly as the app’s own dependencies.

Where it degrades

React Native is generous where a web team is strong and silent where it is weak. Components, state and data fetching transfer straight from React, so a screen is easy to build. What does not transfer is the second thread, the native projects and the store calendar — and almost every failure is one of these:

  • Animations and scroll effects driven from JavaScript, which stutter the moment the thread has real work (rows 8–9).
  • Long lists built with .map(), or virtualised lists with expensive, unmemoised rows (row 10).
  • A global store that re-renders every mounted screen on each change (row 13).
  • Full-resolution photos decoded into memory for thumbnails (row 4).
  • Hand-edited ios and android directories, which turn every upgrade into a merge conflict.
  • A version more than three minors behind, and native libraries that never migrated to the New Architecture.
  • Over-the-air updates without runtime versions, or a rollback.
  • A team with no native skill, which stalls the first time a feature needs a module, a widget or a store deadline.
  • Testing only on a recent iPhone, where the JavaScript thread is fast enough to hide every mistake above.
  • A development server reachable from the network.

The honest limit is the second thread and the second platform. A product whose core is continuous custom graphics — a game, a drawing tool, a camera pipeline — fights the architecture, because its hot path wants to live entirely on one side of the crossing. A product built around a capability that has no JavaScript surface and changes with every OS release lives in native code anyway. And the economics are not fixed: React Native pays off when sharing one implementation saves more than the framework layer costs, and Shopify’s 2026 decision shows that the balance can move. For forms, lists, feeds, commerce and workflow apps built by a team that already writes React — most of what gets built — it is still a very good trade.

The practical check takes ten minutes. Read the React Native version in package.json and find its row in the release table. List the native dependencies and check each one’s New Architecture support and last release. Open the slowest screen in a release build on a mid-range Android phone with the performance monitor on. Search the code for useNativeDriver: false, onScroll handlers that call setState, and .map( inside a ScrollView. Check that over-the-air updates carry a runtime version. And confirm the Android target API and the Xcode version against the deadline table.

When to use it

Use it when

  • A product that needs both platforms, built by a team that already writes React and .
  • Content, commerce and workflow apps — forms, lists, navigation, network — which is most of what gets built.
  • Products that gain from shipping fixes without a review queue, inside the limits of over-the-air updates.
  • Teams that want real platform views — and the platform’s own redesigns — rather than a rendering of them.

Avoid it when

  • Games, drawing tools and anything built on continuous custom graphics — that fights the architecture.
  • Apps built around a platform capability with no JavaScript surface that changes with every OS release.
  • A single-platform product, where the abstraction costs you and buys nothing back.
  • Teams with no capacity for two release pipelines and two store calendars — the code is shared, the shipping is not.

Sources and method

What this page stands on

  • 15 sources
  • 2 documented failures
  • Checked October 13, 2026

Researched with Claude. Finding and cross-checking the primary sources was done with Claude (Anthropic). Selection, writing and fact-checking: the Anatomy of IT Product editors.

Found this useful?

Share it with someone who is working on the same problem.