纯 React Native、Expo 或 Flutter:根据团队情况选择合适的移动开发框架
React Native、Expo与Flutter的实测对比:三者的代码实现方式、各自存在的缺陷,以及用于辅助选择的三者间的五题评估框架。
过去选择跨平台移动开发框架时,人们往往需要在不同方案之间的妥协中做出抉择。如今,React Native、Expo 和 Flutter 都能够生成具有原生体验的应用,运行流畅且可覆盖海量用户群体,因此单纯的速度已很难成为决定性因素。真正起决定作用的是适配性:即你的团队已具备的技能、现有的代码,以及你多快能将应用上架到应用商店。本文将 Expo 视为一种独立的选项,而非 React Native 的附属品,详细介绍了每种方案在实际应用中的表现,并在文末提供了一组简短的问题,帮助你做出选择。
这三种框架都发生了哪些变化
那些曾在早期对比中影响决策的若干结构性缺陷已被彻底解决:
- React Native 的新架构现已成为默认选项。它摒弃了旧版的异步桥接机制,使得 JavaScript 与原生代码能够更直接地交互。因此,对原生模块的调用成本大幅降低。
- Flutter 在移动端已用 Impeller 渲染器取代 Skia 作为默认选项。Impeller 会提前编译着色器,从而避免了首次运行时因着色器编译而导致的卡顿问题,并使帧率更加稳定。
- Expo 已成为开发 React Native 应用的标准方式。它不再只是初学者的测试环境,而是已被大型企业采用的正式开发工具链。
现有的测试结果通常显示,在动画密集的界面中,Flutter在渲染性能方面略占优势;而React Native则在启动时间、内存占用以及原生I/O操作方面表现更好。对于这些具体数据应保持谨慎,因为它们会因应用和设备的不同而有很大差异。对于大多数产品而言,这种差距已不再能决定最终效果。如果您想了解那些长期使用这两种技术团队的观点,我们可以参考文章《只有在实际使用中才会显现的Flutter与React Native选择》,其中探讨了长期使用的种种影响。
纯React Native:最大的控制权,也意味着最大的责任
React Native允许你使用JavaScript或TypeScript结合React来编写界面,而框架则会在iOS和Android上渲染真正的平台组件。这里没有WebView也没有自定义画布:View会变成原生视图,Text则变为原生文本组件。
下方的计数器展示了使用Fabric渲染器时的新架构下应用的基本结构。状态存储在useState钩子中,按钮为Pressable类型,样式则通过StyleSheet.create一次性定义,以便进行验证和重复使用。
// App.tsx — React Native (New Architecture, Fabric)
import React, { useState } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';
export default function App() {
const [count, setCount] = useState(0);
return (
<View style={styles.container}>
<Text style={styles.counter}>{count}</Text>
<Pressable style={styles.button} onPress={() => setCount(c => c + 1)}>
<Text style={styles.buttonText}>Tap me</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
counter: { fontSize: 48, fontWeight: '700', marginBottom: 24 },
button: { backgroundColor: '#2563eb', paddingHorizontal: 24, paddingVertical: 12, borderRadius: 12 },
buttonText: { color: 'white', fontSize: 16, fontWeight: '600' },
});
这段代码片段中没有任何内容是针对新架构特有的;相同的组件代码在两种架构下都能运行。架构上的变化体现在渲染器与原生模块与JavaScript交互的方式上。
优势
- 真正的原生组件。你使用的是平台自带的控件、无障碍功能及文本渲染方式,而非近似实现。
- 最庞大的人才库。JavaScript、TypeScript和React相关技能的拥有者远多于其他选项所对应的技术。
- 完全的原生访问权限。你可以直接使用
ios/和android/目录,需要时随时切换到Swift、Kotlin、Objective-C或Java进行开发。 - 庞大的生态系统。npm提供的第三方包数量超过pub.dev,而且大多数用于支付、地图和分析功能的移动SDK都提供了官方的React Native封装。
- 更快的原生调用速度。Fabric、TurboModules和JSI消除了旧式的异步桥接瓶颈,使得对原生代码的调用几乎能立即完成。
缺点
- 你需要自行管理原生构建工具。Xcode、Gradle 和 CocoaPods 的版本不匹配以及原生依赖冲突仍是常见的难题。
- 跨平台一致性需要付出努力。由于该框架刻意将代码映射到原生控件上,除非进行一致性设计,否则 iOS 和 Android 的界面表现会有所不同。
- 交付基础设施需自行搭建。CI/CD、代码签名以及空中更新等功能从零开始构建并非易事,而这正是 Expo 所填补的空白。
- 第三方模块质量参差不齐。原生模块的维护质量从优秀到被完全废弃不等。
实用建议
- 除非有明确理由,比如需要特定的原生 SDK 或者要逐步迁移现有的原生应用,否则避免直接使用纯 React Native 创建新应用。建议从 Expo 开始,仅在需要时生成原生项目。
- 对于对性能要求较高的交互操作,可使用
react-native-reanimated和react-native-gesture-handler。这些库会在 UI 线程上处理动画和手势操作,因此不会因 JavaScript 线程繁忙而导致帧率下降。 - 请保持 Hermes 启用状态,它已是默认引擎。该引擎会提前将 JavaScript 编译为字节码,相比 JavaScriptCore 能显著减少启动时间和内存占用。
Expo:内置基础设施的 React Native
Expo 是建立在 React Native 之上的框架及服务集合。它曾因只能运行在受限环境中且无法访问原生代码而声名不佳,但这种情况已不复存在:通过预构建技术,也就是所谓的持续原生生成(CNG),Expo 能够与完整的原生模块生态系统协同工作。
下方的界面使用了 Expo Router,它以与 Next.js 处理网页路由相同的方式,将 app/ 目录中的文件映射为对应的路由。app/index.tsx 是首页路由,而 router.push('/profile') 则会导航到定义 /profile 路由的文件。深度链接和网页 URL 均遵循相同的结构。
// app/index.tsx — Expo Router (file-based routing)
import { View, Text, Pressable } from 'react-native';
import { useRouter } from 'expo-router';
export default function HomeScreen() {
const router = useRouter();
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center', gap: 16 }}>
<Text style={{ fontSize: 24, fontWeight: '600' }}>Welcome</Text>
<Pressable onPress={() => router.push('/profile')}>
<Text style={{ color: '#2563eb' }}>Go to Profile</Text>
</Pressable>
</View>
);
}
创建并运行一个项目只需三条命令。第一条命令用于搭建应用框架,而 npx expo start 则会启动开发服务器,这样你就可以在设备或模拟器上打开该应用。
# Spin up a new project in under a minute
npx create-expo-app@latest my-app
cd my-app
npx expo start
优势
- 没有可用于起步的原生开发工具。在大部分开发过程中无需使用Xcode或Android Studio;可以通过Expo Go或在自定义的开发客户端上于实体设备上进行测试。
- EAS Build。iOS和Android版本的构建可在云端完成,且不受操作系统限制,因此即使使用Windows或Linux系统也能生成iOS版本。
- EAS Update。只要没有原生代码的改动,即可通过空中更新立即将JavaScript代码及资源变更推送给用户,无需等待应用商店审核。
- Expo Router。开箱即用支持基于文件的路由功能,具备深度链接能力,并能为网页应用和原生应用提供统一的导航体验。
expo-camera、expo-notifications、expo-location 和 expo-image 等组件已被整合、附上文档说明,并确保版本匹配,从而能够协同工作。app.json 以声明式方式修改 Info.plist 和 AndroidManifest.xml 等原生项目文件,而无需手动编辑,这有助于保持持续集成构建的稳定性。npx expo prebuild 即可生成相应的原生文件夹。缺点
- 专用 SDK 需要额外工作。部分专业的原生 SDK 仍然要求用户自行编写配置插件或原生模块,这比使用纯 React Native 需要更多的工作量,不过这一差距正在逐渐缩小。
- Expo Go 的隐患。过度依赖 Expo Go 会让人忽视这样一个事实:除非创建开发版本,否则自定义原生模块是无法运行的。这常常让新手措手不及。
- 服务成本。EAS Build 和 Update 提供免费套餐,但规模较大的开发团队通常会选择付费方案,而纯 React Native 配合自托管的 CI 可以避免此类成本。
- 固有的限制。作为 React Native 之上的框架,Expo 仍存在其缺陷:不同平台之间的 UI 差异,以及在高负载计算情况下可能成为瓶颈的 JavaScript 线程。
实用建议
- 当需要 Expo 未覆盖的本地模块时,运行
npx expo prebuild。该命令会根据需求创建ios/和android/文件夹,从而确保始终能够使用本地代码。我们的文章 使用 Expo prebuild 和 CNG 将本地文件夹视为构建输出 对相关流程进行了详细说明。 - 请使用 EAS Update 进行热修复,而非用于改变本地行为的新增功能。应用商店的规定限制了可下载代码的修改范围,若以 OTA 更新的形式推送新功能,很可能会被拒收;在依赖此方法之前,请先了解苹果和谷歌的现行政策。
- 在新项目启动时就采用 Expo Router。若要在现有的导航架构上添加基于文件的路由功能,将会非常繁琐。
expo doctor。它能检测出依赖项和版本不匹配的问题,否则这些问题会表现为难以理解的本地构建失败。Flutter:掌控每一像素
Flutter采取了截然不同的方式。它不通过平台控件来呈现界面,而是利用 Impeller 引擎直接绘制整个界面,同时将 Dart 代码提前编译为 ARM 或 x86 体系的本地机器码。
下方的 Dart 计数器示例与 React Native 的方式类似。MyApp 用 MaterialApp 包装整个应用,CounterScreen 是一个 StatefulWidget,其状态类中保存着 _count 的值,点击按钮后会调用 setState 方法,从而指示 Flutter 使用新数值重新构建该子界面。
// main.dart
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(home: CounterScreen());
}
}
class CounterScreen extends StatefulWidget {
const CounterScreen({super.key});
@override
State<CounterScreen> createState() => _CounterScreenState();
}
class _CounterScreenState extends State<CounterScreen> {
int _count = 0;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$_count', style: const TextStyle(fontSize: 48, fontWeight: FontWeight.bold)),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () => setState(() => _count++),
child: const Text('Tap me'),
),
],
),
),
);
}
}
命令行工作流涵盖了整个生命周期:创建项目,在开发过程中通过热重载运行它,同时为 Google Play(应用包)和 App Store(IPA 文件)生成发布版本。
flutter create my_app
cd my_app
flutter run # hot reload in under a second
flutter build appbundle --release # Android
flutter build ipa --release # iOS
优势
- 各平台界面完全一致。由于 Flutter 会自行渲染每个组件,因此在 iOS、Android、网页和桌面端上的行为与外观都保持一致,不会出现因平台差异导致的问题。
- 出色的动画性能。借助提前编译的着色器以及通过 Impeller 实现的对 GPU 的直接访问,Flutter 在处理复杂且包含大量动画的界面时,其帧率表现通常优于其他框架。
- 一个代码库可支持六种平台。iOS、Android、网页、Windows、macOS 和 Linux 都能从相同的 Dart 代码中构建应用。
缺点
- Dart的生态系统规模较小。招聘难度高于JavaScript或TypeScript,即便是经验丰富的开发者通常也需要几周时间才能高效工作。
- 使用体验可能略显生硬。由于不使用平台原生组件,细心的用户可能会注意到一些行为上的细微差异,不过这种情况已大幅改善。
- 更大的二进制文件。由于每个应用都内置了Flutter引擎,因此其打包后的体积通常会比同类型的React Native应用更大。
- 专用包较少。pub.dev虽然不错,但相比npm而言功能较为有限,尤其是在处理特定原生SDK的封装包时。
- 无法复用Web端的React代码。那些已有成熟React Web代码库的组织,在开发Web应用时无法复用任何现有代码。
实用建议
- 从第一天起就启用带有严格代码检查规则的
flutter analyze。Dart的空值安全特性确实具有优势,但前提是不要到处使用dynamic类型来削弱这一特性。 - 在原型之外,应选择Riverpod或Bloc等状态管理方案;仅依靠
setState是无法支撑大型应用的。
帮助做出选择的五个问题
按顺序思考这些问题。通常第一个就有明确答案的问题就能决定方向。
- 您的团队已经在使用 React 和 JavaScript 吗? 如果是,就继续留在 React Native 生态系统中,并默认使用 Expo。如果不是且可以自由选择,Flutter 和 Expo 都是不错的选择;请选择团队更愿意学习的语言。
- 您是否需要像素级一致的设计或复杂的自定义动画,比如在游戏、创意工具或可视化功能较强的应用中?Flutter 是更优的选择。
- 您是否希望与现有的 React 网页应用共享代码或组件?通过 React Native Web,React Native 具有明显优势;而 Flutter 在网页端则需要从零开始。
- 您是否需要在不经过商店审核的情况下发布仅基于 JavaScript 的修复,或在没有 Mac 的情况下开发 iOS 应用?Expo 的 EAS Update 和 EAS Build 能直接解决这些问题。
- 您是否需要在现有的大型原生应用中嵌入跨平台界面?相比全新的 Expo 项目,纯 React Native 或 Flutter 的应用内扩展功能更为合适。
推荐方案
- 对于正在开发新应用的 React 或 JavaScript 团队,建议选择 Expo。它解决了 React Native 在原生工具、CI/CD 及 OTA 更新方面的大部分历史问题,同时在需要时仍能提供完整的原生功能。
- 如果界面精美度、动画性能以及在移动端、桌面端和网页端的兼容性比复用 JavaScript 生态系统更为重要,或者团队没有明确的语言偏好并打算从零开始,那么请选择 Flutter。
- 只有在必须直接管理原生项目的情况下,才应选择纯 React Native,通常是因为已有庞大的原生代码库或特殊的原生集成需求。
总结
这三种技术栈在性能和成熟度方面已足够接近,框架能力几乎不再成为瓶颈。决定性因素在于团队成员、现有的代码以及产品上线的速度。可以将 Expo视为使用 React Native 的默认方式,仅在需要直接掌控原生项目时才选择纯 React Native,而当渲染控制能力和跨平台覆盖范围比继续使用 JavaScript 更重要时,则选择 Flutter。无论选择哪种技术,都应在项目初期就对应用中最具风险的部分进行测试,无论是原生 SDK、复杂动画还是网页版本,而非等到发布时才处理。