Skip to main content

Best Mobile Frameworks in 2026: React Native vs Flutter vs Capacitor

·PkgPulse Team

TL;DR

React Native (Expo) for web developers; Flutter for native-feel performance; Capacitor for web-to-mobile. React Native + Expo (~3M weekly downloads) gives web developers the fastest path to mobile with a massive npm ecosystem. Flutter (~8M downloads) from Google uses Dart and its own rendering engine — pixel-perfect UI across all platforms. Capacitor (~900K downloads) wraps a web app in a native shell — best for Progressive Web Apps going native.

Key Takeaways

  • Expo (React Native): ~3M weekly downloads — managed workflow, OTA updates, EAS build service
  • Flutter: ~8M downloads — Dart language, custom renderer, 60/120fps on all platforms
  • Capacitor: ~900K downloads — web app → native, same codebase as your PWA
  • React Native New Architecture — JSI + Fabric renderer, near-native performance in 2026
  • Expo Router — file-based routing for web + mobile unified

React Native + Expo (Web Devs)

// Expo — project setup (managed workflow)
// npx create-expo-app MyApp --template
// or: npx expo start

// app/(tabs)/index.tsx — Expo Router (file-based)
import { View, Text, StyleSheet, Pressable, FlatList } from 'react-native';
import { useRouter } from 'expo-router';
import { useQuery } from '@tanstack/react-query';

interface Post {
  id: number;
  title: string;
}

export default function HomeScreen() {
  const router = useRouter();
  const { data: posts, isLoading } = useQuery<Post[]>({
    queryKey: ['posts'],
    queryFn: () => fetch('https://api.example.com/posts').then(r => r.json()),
  });

  if (isLoading) return <ActivityIndicator />;

  return (
    <View style={styles.container}>
      <Text style={styles.title}>Latest Posts</Text>
      <FlatList
        data={posts}
        keyExtractor={(item) => String(item.id)}
        renderItem={({ item }) => (
          <Pressable
            style={styles.card}
            onPress={() => router.push(`/post/${item.id}`)}
          >
            <Text>{item.title}</Text>
          </Pressable>
        )}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 16 },
  title: { fontSize: 24, fontWeight: 'bold', marginBottom: 16 },
  card: {
    padding: 16,
    marginBottom: 8,
    backgroundColor: '#f5f5f5',
    borderRadius: 8,
  },
});
// React Native — native modules (bridging)
// Using Expo's pre-built native modules:
import * as Camera from 'expo-camera';
import * as Location from 'expo-location';
import * as Haptics from 'expo-haptics';
import * as SecureStore from 'expo-secure-store';
import * as Notifications from 'expo-notifications';

// Camera
async function takePhoto() {
  const { status } = await Camera.requestCameraPermissionsAsync();
  if (status !== 'granted') return;
  // ... use Camera component
}

// Location
async function getLocation() {
  const { status } = await Location.requestForegroundPermissionsAsync();
  const location = await Location.getCurrentPositionAsync({});
  return location.coords;
}

// Haptic feedback
await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);

// Secure storage (Keychain/Keystore)
await SecureStore.setItemAsync('auth_token', token);
const token = await SecureStore.getItemAsync('auth_token');
// Expo EAS — build and submit to App Store/Play Store
// eas.json
{
  "build": {
    "preview": {
      "distribution": "internal",
      "android": { "buildType": "apk" }
    },
    "production": {
      "android": { "buildType": "app-bundle" },
      "ios": { "resourceClass": "m-medium" }
    }
  },
  "submit": {
    "production": {
      "ios": { "appleId": "dev@example.com" },
      "android": { "serviceAccountKeyPath": "./play-store-key.json" }
    }
  }
}

// Commands:
// eas build --platform all --profile production
// eas submit --platform all --profile production
// eas update --branch production  (OTA update, no app store review)

Flutter (Native Performance)

// Flutter — Dart, custom renderer (no native views)
import 'package:flutter/material.dart';

void main() => runApp(const MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'My App',
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.blue),
      ),
      home: const HomeScreen(),
    );
  }
}

class HomeScreen extends StatelessWidget {
  const HomeScreen({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Posts')),
      body: FutureBuilder<List<Post>>(
        future: fetchPosts(),
        builder: (context, snapshot) {
          if (!snapshot.hasData) {
            return const Center(child: CircularProgressIndicator());
          }
          return ListView.builder(
            itemCount: snapshot.data!.length,
            itemBuilder: (context, index) {
              final post = snapshot.data![index];
              return ListTile(
                title: Text(post.title),
                onTap: () => Navigator.push(
                  context,
                  MaterialPageRoute(
                    builder: (_) => PostScreen(post: post),
                  ),
                ),
              );
            },
          );
        },
      ),
    );
  }
}

Capacitor (Web App → Native)

// Capacitor — add native to existing web app
// npm install @capacitor/core @capacitor/cli
// npx cap init MyApp com.myapp.app
// npx cap add ios
// npx cap add android

// Use Capacitor plugins alongside web APIs
import { Camera, CameraResultType } from '@capacitor/camera';
import { Geolocation } from '@capacitor/geolocation';
import { PushNotifications } from '@capacitor/push-notifications';

// Camera (falls back to input[type=file] on web)
async function takePicture() {
  const image = await Camera.getPhoto({
    quality: 90,
    allowEditing: true,
    resultType: CameraResultType.Uri,
  });
  return image.webPath;
}

// Geolocation
const coordinates = await Geolocation.getCurrentPosition();
console.log(coordinates.coords.latitude, coordinates.coords.longitude);

// Push notifications
await PushNotifications.register();
PushNotifications.addListener('registration', (token) => {
  console.log('Push token:', token.value);
});
// Capacitor — sync and run
// After building your web app (npm run build):
// npx cap sync        — copy web assets to native projects
// npx cap open ios    — open in Xcode
// npx cap open android — open in Android Studio
// npx cap run ios     — run on simulator

Comparison Table

FrameworkLanguagePerformanceWeb Dev FriendlyBundle Size
React Native + ExpoTypeScriptHigh (New Arch)~25MB
FlutterDartHighest⚠️ (Dart)~10MB
CapacitorTypeScriptMedium~5MB + Web
IonicTypeScriptMedium~8MB

When to Choose

ScenarioPick
Web developer, want mobile fastReact Native + Expo
Pixel-perfect UI across all platformsFlutter
Already have a web app, add nativeCapacitor
Maximum performance (gaming, intensive apps)Flutter
Shared codebase with Next.js web appReact Native (Expo Router)
Desktop + mobile + web (single codebase)Flutter
PWA that needs app store presenceCapacitor

Compare mobile framework package health on PkgPulse.

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.