Maciej Samborski
Mobile Developer
2026-06-02
#Development
Time to read
12 mins
In this article
Introduction
The short version
The fundamental difference
React Native in 2026: the New Architecture story
Start with Expo
Code comparison
Performance
Ecosystem
Real pain points
The language question: TypeScript vs Dart
Choosing between them
What's coming
Agency take
Share this article
React Native's New Architecture has closed most of the performance gap that made Flutter the go-to for animation-heavy apps - and if your team already writes TypeScript and React, the case for switching languages has never been weaker. We cover the architecture shift, real pain points on both sides, and side-by-side code for the patterns you will actually build.
Versions covered: React Native 0.85.3 · Expo SDK 56 · Flutter 3.44.0 Last verified: May 2026
If your team writes JavaScript or TypeScript and already knows React: React Native. The knowledge transfer is direct, the ecosystem is enormous, and the New Architecture has erased most of the performance arguments that used to favour Flutter. You are building mobile apps with the same language, the same mental model, the same tooling, and - increasingly - the same people who build your web product.
If you are starting from zero with no existing React knowledge, care deeply about animation consistency at 120fps, or are building an app where pixel-perfect parity across iOS and Android is a hard requirement: Flutter. Dart is a faster learning curve than many engineers expect, and Flutter's rendering model removes an entire class of "why does this look different on Android" problems.
This is not a close race decided by benchmarks. It is a question of team fit and project requirements. For most product companies and agencies building TypeScript web stacks, React Native is the natural extension. Flutter is the right call when you are specifically optimising for what it is best at.
This is the one thing to understand before anything else.
React Native runs your JavaScript code in a JS engine (Hermes, by default since RN 0.70) and renders actual native UI components - UIButton on iOS, android.widget.Button on Android. Your code is JavaScript. Your UI is native. The New Architecture (mandatory as of RN 0.82) replaced the old async JSON bridge with JSI - direct C++ bindings that allow JS to call native code synchronously and hold references to native objects.
Flutter compiles Dart code to native ARM and x86 binaries and renders every pixel itself using the Skia/Impeller graphics engine. There are no native platform widgets involved. A Flutter Button is drawn on a canvas, not an iOS UIButton. This is why Flutter has perfect cross-platform pixel parity - and why it has to work harder to look native on each platform.
Neither approach is wrong. They have different trade-offs:
| React Native | Flutter | |
|---|---|---|
| UI layer | Native platform widgets | Custom-rendered canvas |
| Cross-platform parity | Close, with platform quirks | Pixel-perfect |
| "Looks native" | Yes — uses actual native components | Approximated (very well) |
| Native component access | Direct | Via PlatformView (has overhead) |
| Language | TypeScript / JavaScript | Dart |
| Compilation | JS + native modules | Fully compiled to native |
For several years, the main argument for Flutter over React Native was performance. The async bridge at the heart of the old architecture caused frame drops during complex animations and made synchronous native access impossible. That argument is no longer valid in 2026.
The timeline:
The New Architecture ships four interconnected pieces:
The practical result: React Native's animation and interaction performance on New Architecture is genuinely close to Flutter for the vast majority of application patterns. The gap remains at the extremes - very heavy custom animations, games, frame-by-frame camera processing - but for business apps, the New Architecture is good enough to make performance a non-issue in the RN vs Flutter decision.
In 2026, the right way to start a new React Native project is Expo. The old distinction between "Expo managed" and "bare workflow" has largely dissolved. You can now:
Expo SDK 56 targets React Native 0.85 and React 19.2.3. Legacy architecture support was removed in SDK 55 - there is no managed path back to the old bridge.
The only reasons to skip Expo in 2026: you have a highly custom native module that Config Plugins cannot accommodate, or you are integrating into an existing native app that already has its own build infrastructure.
React Native:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33import { useState } from 'react'; import { FlatList, RefreshControl, Text, View, StyleSheet } from 'react-native'; type Item = { id: string; title: string }; export default function ItemList({ items }: { items: Item[] }) { const [refreshing, setRefreshing] = useState(false); const handleRefresh = async () => { setRefreshing(true); await fetchItems(); // your data fetch setRefreshing(false); }; return ( <FlatList data={items} keyExtractor={(item) => item.id} renderItem={({ item }) => ( <View style={styles.row}> <Text>{item.title}</Text> </View> )} refreshControl={ <RefreshControl refreshing={refreshing} onRefresh={handleRefresh} /> } /> ); } const styles = StyleSheet.create({ row: { padding: 16, borderBottomWidth: 1, borderBottomColor: '#eee' }, });
Flutter:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27class ItemList extends StatefulWidget { final List<Item> items; const ItemList({required this.items, super.key}); @override State<ItemList> createState() => _ItemListState(); } class _ItemListState extends State<ItemList> { Future<void> _handleRefresh() async { await fetchItems(); // your data fetch } @override Widget build(BuildContext context) { return RefreshIndicator( onRefresh: _handleRefresh, child: ListView.builder( itemCount: widget.items.length, itemBuilder: (context, index) { final item = widget.items[index]; return ListTile(title: Text(item.title)); }, ), ); } }
Note: Flutter's ListView is not virtualised by default - ListView.builder is the lazy equivalent of RN's FlatList. For short, static lists ListView is fine; for long or dynamic lists always use ListView.builder.
React Native - no built-in form handling; react-hook-form + zod is the community standard:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45import { useForm, Controller } from 'react-hook-form'; import { z } from 'zod'; import { zodResolver } from '@hookform/resolvers/zod'; import { TextInput, Text, Pressable, View } from 'react-native'; const schema = z.object({ name: z.string().min(1, 'Name is required'), email: z.string().email('Enter a valid email'), }); type FormData = z.infer<typeof schema>; export default function SignUpForm() { const { control, handleSubmit, formState: { errors } } = useForm<FormData>({ resolver: zodResolver(schema), }); const onSubmit = (data: FormData) => console.log(data); return ( <View> <Controller name="name" control={control} render={({ field: { onChange, value } }) => ( <TextInput placeholder="Name" onChangeText={onChange} value={value} /> )} /> {errors.name && <Text>{errors.name.message}</Text>} <Controller name="email" control={control} render={({ field: { onChange, value } }) => ( <TextInput placeholder="Email" onChangeText={onChange} value={value} keyboardType="email-address" /> )} /> {errors.email && <Text>{errors.email.message}</Text>} <Pressable onPress={handleSubmit(onSubmit)}> <Text>Sign up</Text> </Pressable> </View> ); }
Flutter - form validation is built into the SDK:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32final _formKey = GlobalKey<FormState>(); final _nameController = TextEditingController(); final _emailController = TextEditingController(); Form( key: _formKey, child: Column( children: [ TextFormField( controller: _nameController, decoration: const InputDecoration(labelText: 'Name'), validator: (value) => (value == null || value.isEmpty) ? 'Name is required' : null, ), TextFormField( controller: _emailController, decoration: const InputDecoration(labelText: 'Email'), keyboardType: TextInputType.emailAddress, validator: (value) => (value != null && value.contains('@')) ? null : 'Enter a valid email', ), ElevatedButton( onPressed: () { if (_formKey.currentState!.validate()) { // submit } }, child: const Text('Sign up'), ), ], ), )
Flutter's built-in Form + TextFormField + validator is a genuine ergonomic win. React Native requires three separate packages (react-hook-form, zod, @hookform/resolvers) to achieve equivalent functionality. Both patterns work well; Flutter's requires less setup and fewer dependencies.
React Native with TanStack Query (v5 - production standard for client-side data fetching):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28import { useQuery } from '@tanstack/react-query'; import { FlatList, ActivityIndicator, Text } from 'react-native'; type Post = { id: number; title: string }; async function fetchPosts(): Promise<Post[]> { const res = await fetch('https://jsonplaceholder.typicode.com/posts?_limit=10'); if (!res.ok) throw new Error('Network response failed'); return res.json(); } export default function PostsScreen() { const { data, isLoading, error } = useQuery({ queryKey: ['posts'], queryFn: fetchPosts, }); if (isLoading) return <ActivityIndicator />; if (error) return <Text>Error: {error.message}</Text>; return ( <FlatList data={data} keyExtractor={(post) => String(post.id)} renderItem={({ item }) => <Text>{item.title}</Text>} /> ); }
Flutter with FutureBuilder (built-in) or Riverpod for more complex state:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24Future<List<Post>> fetchPosts() async { final response = await http.get( Uri.parse('https://jsonplaceholder.typicode.com/posts?_limit=10'), ); if (response.statusCode != 200) throw Exception('Failed to load posts'); final List data = json.decode(response.body); return data.map((e) => Post.fromJson(e)).toList(); } FutureBuilder<List<Post>>( future: fetchPosts(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const CircularProgressIndicator(); } if (snapshot.hasError) { return Text('Error: ${snapshot.error}'); } return ListView.builder( itemCount: snapshot.data!.length, itemBuilder: (context, i) => ListTile(title: Text(snapshot.data![i].title)), ); }, )
One of the biggest React Native improvements in recent years is Expo Router - file-based routing that works exactly like Next.js App Router:
1 2 3 4 5 6 7 8app/ _layout.tsx # root layout (NavigationContainer, providers) (tabs)/ _layout.tsx # tab bar definition index.tsx # Home tab → / profile.tsx # Profile tab → /profile post/ [id].tsx # Dynamic route → /post/123
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19// app/(tabs)/_layout.tsx import { Tabs } from 'expo-router'; export default function TabLayout() { return ( <Tabs> <Tabs.Screen name="index" options={{ title: 'Home' }} /> <Tabs.Screen name="profile" options={{ title: 'Profile' }} /> </Tabs> ); } // app/post/[id].tsx import { useLocalSearchParams } from 'expo-router'; export default function PostScreen() { const { id } = useLocalSearchParams<{ id: string }>(); return <Text>Post {id}</Text>; }
If your team already works with Next.js, this is almost zero learning curve.
Flutter still has a raw advantage in sustained 60/120fps animations. Because it draws every pixel itself via Impeller (the newer Skia replacement, default on iOS since Flutter 3.10 and Android since 3.16), there is no negotiation with the platform's native rendering pipeline. Frame timing is consistent.
React Native with New Architecture is very close for typical application patterns. The JSI bridge is effectively zero-cost for most operations. react-native-reanimated v4 (the animation library) runs entirely on the UI thread via worklets — it does not touch the JS thread during animation. For the ~95% of apps that do not involve custom particle systems or frame-by-frame video processing, React Native's performance is not a limiting factor.
Where Flutter still pulls ahead: applications with long lists of complex custom-rendered items, multi-step transitions with many animated properties, or intensive canvas work. Where React Native has the edge: anything that embeds native platform components - WebViews, native maps, video players. Flutter wraps these in a PlatformView, which renders through a separate texture layer and can cause jank or visual glitches in complex layouts.
Release build sizes vary by app complexity, but published benchmarks put minimal apps at:
| Android (AAB) | iOS (after App Thinning) | |
|---|---|---|
| React Native (bare, Hermes) | ~7-10 MB | ~20-25 MB |
| Flutter | ~5-8 MB | ~16-22 MB |
Flutter edges ahead on baseline size because there is no JS bundle and no Hermes runtime to ship. In practice, as app complexity grows both frameworks converge - the difference becomes negligible against the size of your own code, assets, and libraries.
React Native's Hermes engine compiles JavaScript to .hbc bytecode at build time rather than at runtime. This means zero JS parsing on the device at startup - a significant improvement to cold start time and a 3–7 MB reduction in effective bundle size compared to shipping raw JS.
The React Native ecosystem benefits from a decade of JavaScript library development. Almost anything you need has a well-maintained package:
| Use case | React Native | Flutter |
|---|---|---|
| Animation | react-native-reanimated v4.3.1 | Built-in AnimationController |
| Camera + ML | react-native-vision-camera v5.0.10 (frame processors via worklets) | camera plugin (less capable) |
| Navigation | expo-router (file-based) / @react-navigation v7 | GoRouter (officially recommended) |
| State management | zustand v5, jotai, redux | Riverpod, Provider, Bloc |
| Server state / caching | @tanstack/react-query v5 | Riverpod + custom (no direct equivalent) |
| Forms | react-hook-form v7 + zod | Built into SDK |
| Canvas / 2D | react-native-skia | Built-in (Flutter is Skia-native) |
| OTA updates | EAS Update (Expo) | Code Push (third-party, or Shorebird) |
| CI/CD | EAS Build | Codemagic, GitHub Actions |
Flutter's ecosystem is smaller but growing fast. The packages that exist are generally well-maintained and backed by Google. The gap that matters most in practice: server-state caching (TanStack Query has no Flutter equivalent with comparable maturity) and camera/ML processing (vision-camera's frame processor worklets are a genuinely unique capability with no Flutter match).
Flutter's built-in advantage: Form, TextFormField, DatePicker, Drawer, and the full Material 3 and Cupertino widget libraries are part of the SDK with zero third-party dependencies.
Dart is not a difficult language. Engineers with JavaScript backgrounds typically become productive in Dart within a week. It is strongly typed, garbage collected, has async/await, and its syntax is closer to Java/TypeScript than to anything exotic.
That said, the language switch has real costs for a JavaScript team:
React Native's answer to this is direct: your TypeScript, your React patterns, your npm packages, and increasingly your team. A Next.js developer can be productive in a React Native + Expo Router project within hours. The mental model is the same; only the output primitives differ.
React Native + Expo is the right choice when:
Flutter is the right choice when:
The choice that requires more thought: a company with a large React/TypeScript web team that wants to add mobile. On paper React Native is obvious - leverage the existing team. In practice, if the mobile product has heavy animation requirements or the team is willing to invest in learning Dart, Flutter may deliver a better long-term result. Neither answer is wrong, but it is worth making the decision deliberately rather than defaulting to RN because JavaScript is familiar.
React Native 0.85's improved animation backend and RN 0.84's Hermes V1 default continue the performance trajectory. The next significant milestone to watch: bridgeless mode becoming fully stable (the New Architecture can now also run without any bridge code compiled in, reducing binary size further).
Flutter's 2026 roadmap includes three more releases: 3.47 (August), 3.50 (November). The most watched open issue is code push / OTA updates (#14330, the most upvoted open issue in the Flutter repo) — if Flutter ships a first-party OTA solution, it removes one of React Native's clearest practical advantages.
For most of our clients - product companies and agencies building TypeScript web stacks that need a mobile app - React Native with Expo is the recommendation. The engineering team already knows the language and the framework. The tooling is production-ready. Expo's managed workflow has removed most of the native build headaches that plagued RN projects for years. And the New Architecture has made the performance arguments against RN largely obsolete for business apps.
Flutter earns a serious look when the client's requirements push to the edges of what React Native handles well: sustained heavy animation, a dedicated mobile-only team willing to invest in Dart, or a product that genuinely needs pixel-perfect cross-platform rendering.
The old answer - "Flutter for performance, React Native for JavaScript teams" - is less accurate in 2026 than it used to be. The New Architecture closed the gap. What remains true: React Native fits into a JavaScript organisation without friction. Flutter requires commitment. Both can build excellent apps. Choose the one your team can maintain confidently five years from now.
Maciej Samborski
Mobile Developer
Share this post
Want to light up your ideas with us?
Kickstart your new project with us in just 1 step!
Prefer to call or write a traditional e-mail?
Dev and Deliver
sp. z o.o. sp. k.
Address
Józefitów 8
30-039 Cracow, Poland
VAT EU
PL9452214307
Regon
368739409
KRS
94552994
Our services
Proud Member of