React Native vs Flutter in 2026: a practical comparison

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

Introduction

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

The short version

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.

The fundamental difference

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 NativeFlutter
UI layerNative platform widgetsCustom-rendered canvas
Cross-platform parityClose, with platform quirksPixel-perfect
"Looks native"Yes — uses actual native componentsApproximated (very well)
Native component accessDirectVia PlatformView (has overhead)
LanguageTypeScript / JavaScriptDart
CompilationJS + native modulesFully compiled to native

React Native in 2026: the New Architecture story

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:

  • RN 0.76 (October 2024): New Architecture became the default for new projects
  • RN 0.82 (October 2025): Legacy Paper renderer fully removed - New Architecture is the only option
  • RN 0.84 (February 2026): Hermes V1 becomes default; precompiled binaries on iOS; legacy architecture code fully deleted
  • RN 0.85 (April 2026): Improved animation backend, DevTools improvements, Metro TLS support
  • Expo SDK 55 (February 2026): Legacy architecture support removed from Expo

The New Architecture ships four interconnected pieces:

  • JSI (JavaScript Interface) - replaces the JSON bridge with direct C++ bindings. JS can now call native methods synchronously and hold native object references, enabling patterns that were impossible before (like react-native-reanimated's UI-thread animations and react-native-vision-camera's frame processors).
  • Fabric - new concurrent renderer. Supports React 18/19 concurrent features: Suspense, transitions, startTransition. Layout calculations run on multiple threads instead of blocking the JS thread.
  • TurboModules - lazy-loaded native modules. The old architecture loaded every registered native module at startup regardless of whether you used it. TurboModules load on first access, improving startup time meaningfully.
  • Codegen - auto-generates type-safe C++ binding code from TypeScript specs. Write your native module interface in TypeScript, Codegen produces the platform glue code.

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.

Start with Expo

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:

  • Use Expo's managed workflow with full native code access via config plugins - no ejecting required for most customisations
  • Run EAS Build (Expo Application Services) for CI/CD, without maintaining your own macOS build agent for iOS
  • Ship over-the-air updates via EAS Update without app store review for JS-layer changes
  • Use Expo Router for file-based routing that mirrors Next.js conventions - if your team already knows Next.js, the mental model is identical

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.

Code comparison

VIRTUALISED LIST WITH PULL-TO-REFRESH

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
33
import { 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
27
class 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.

FORM WITH VALIDATION

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
45
import { 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
32
final _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.

REST API WITH LOADING STATE

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
28
import { 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
24
Future<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)),
    );
  },
)

FILE-BASED NAVIGATION (EXPO ROUTER)

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
8
app/
  _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.

Performance

THE HEADLINE: THE GAP IS MOSTLY CLOSED

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.

APP SIZE

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.

Ecosystem

REACT NATIVE HAS THE DEEPER ECOSYSTEM

The React Native ecosystem benefits from a decade of JavaScript library development. Almost anything you need has a well-maintained package:

Use caseReact NativeFlutter
Animationreact-native-reanimated v4.3.1Built-in AnimationController
Camera + MLreact-native-vision-camera v5.0.10 (frame processors via worklets)camera plugin (less capable)
Navigationexpo-router (file-based) / @react-navigation v7GoRouter (officially recommended)
State managementzustand v5, jotai, reduxRiverpod, Provider, Bloc
Server state / caching@tanstack/react-query v5Riverpod + custom (no direct equivalent)
Formsreact-hook-form v7 + zodBuilt into SDK
Canvas / 2Dreact-native-skiaBuilt-in (Flutter is Skia-native)
OTA updatesEAS Update (Expo)Code Push (third-party, or Shorebird)
CI/CDEAS BuildCodemagic, 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.

Real pain points

REACT NATIVE

  • Platform divergence adds up. Even with a shared codebase, you will write Platform.OS === 'ios' branches. Keyboard behaviour, shadow rendering, font rendering, scroll physics, StatusBar behaviour — iOS and Android handle all of these differently. In a real production app, 15–25% of your UI code will have platform-specific paths. This is rarely discussed in comparison articles.
  • Metro bundler slows down on large projects. Cold start of the Metro bundler on a project with 1,000+ modules can take 30–60 seconds. Hot reload is fast once running, but that initial start is a daily tax on developer experience. There is active work on this (faster resolver, lazy bundling) but it has not been fully solved.
  • The New Architecture migration was painful. RN 0.82 removed the legacy bridge entirely. Teams that relied on third-party native modules found that many packages were slow to ship TurboModule support. The findNodeHandle API was removed silently, breaking code that used it. Shadow thread removal changed layout timing in ways that caused subtle animation bugs. If you have a brownfield RN app older than 2024, budget 2–8 weeks for the migration depending on how much custom native code you have.
  • Native crash debugging is immature. When Hermes or JSI crashes at the native layer, symbolication — mapping the crash back to readable source — requires extra tooling and is less reliable than the native iOS/Android crash reporters you would get with Swift or Kotlin.

FLUTTER

  • Code Push does not exist natively. React Native can ship JS-layer updates over the air without an app store review via EAS Update. Flutter has no official equivalent - Shorebird is the main third-party option, but it is not yet as mature or widely adopted. If OTA deployment is a requirement, this matters.
  • Dart is a hiring constraint. The Dart talent pool is smaller than JavaScript's by a large margin. Flutter developers exist, but if you need to hire quickly or integrate with a team of existing web developers, the onboarding cost is real.
  • PlatformView jank is a known issue. Embedding native components (Google Maps, WebView, video players) inside Flutter layouts triggers PlatformView, which renders through a separate texture layer. On Android in particular, this causes visible frame drops when animating around a PlatformView or scrolling it in a list. The Flutter team has worked on this for years and it has improved, but it is not fully solved.
  • "Looks native" requires effort. Flutter renders its own widgets. The default Material 3 widgets look polished and consistent, but they look like Material, not like iOS. Building a truly platform-native feel (Cupertino on iOS, Material on Android) requires maintaining two widget trees or a library that handles it. React Native's use of actual native components gives you platform-native appearance for free.

The language question: TypeScript vs Dart

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:

  • No npm. Pub.dev is Flutter's package registry. The ecosystem is smaller and there is no path to reusing your existing JS libraries.
  • No shared code with your web stack. If your web app is in Next.js and your mobile app is in Flutter, you maintain two separate codebases in two languages.
  • Your React knowledge does not transfer. Component model, hooks, context, state management - all of it needs to be relearned in Flutter's widget/State/Riverpod model.

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.

Choosing between them

React Native + Expo is the right choice when:

  • Your team writes TypeScript and knows React
  • You want to share developers, tooling, and potentially code between your web and mobile products
  • You need the depth of the npm ecosystem - particularly for data fetching, state management, or device integrations
  • OTA updates matter (shipping JS fixes without app store review)
  • You want file-based routing that mirrors your Next.js conventions
  • You are building a business application where "good enough performance" is genuinely good enough

Flutter is the right choice when:

  • You are building from scratch with no existing React investment
  • Your app has intensive custom animation requirements or needs consistent 120fps on all supported devices
  • Pixel-perfect cross-platform parity is a hard requirement
  • You are targeting desktop or web in addition to mobile (Flutter's multi-platform story is more mature than RN's)
  • You are comfortable hiring specifically for Dart and building a Flutter-specialised team

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.

What's coming

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.

Agency take

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

Related posts

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