过去三年,我们团队用 Flutter 和 React Native 各交付了三个商业级 App。外面讲框架对比的文章 90% 是抄文档,剩下 10% 是跑个 demo 看帧率。这次我们直接拿两个真实上架项目的线上埋点数据说话——一个是用 Flutter 3.0 做的电商 App(iOS + Android),一个是 React Native 0.71 做的社区 App(iOS + Android),日活都在 5 万左右。

先说测试环境:数据从哪里来

两个 App 都在近三个月内做过一轮性能专项优化,正好留下了完整的优化前后对比数据。

对比维度 Flutter 3.0 项目 React Native 0.71 项目
业务类型 垂直电商(商品图多) 图文社区(长列表多)
双端架构 单代码库,AOT 编译 单代码库,Hermes + JSI
冷启动(中端安卓) 841ms → 612ms 1.2s → 943ms
首帧渲染(iPhone 13) 438ms 702ms
列表滚动帧率(iOS) 58.7fps 55.2fps
内存峰值(安卓端) 78MB → 64MB 121MB → 89MB
APK 体积(含基础依赖) 22.4MB 15.8MB
崩溃率(近 30 天) 0.09% 0.17%

测试工具:PerfDog 拉帧率,Firebase Performance 埋点,Xcode Instruments 追内存,所有冷启动数据取连续 12 次的中位数。中端安卓测试机是 Redmi Note 11T。

我们团队在微信群里为这套数据吵了三个晚上。共识是:单看数字没有意义,必须连业务场景一起看。

冷启动:AOT 不是万能药

Flutter 3.0 最大的杀手锏是 AOT 编译。Dart 代码直接编译成机器码,省去了 JS 引擎的解析和编译阶段。红米 Note 上的 Flutter 冷启动从 841ms 压到 612ms,靠的是三件事:禁用所有插件的懒加载(Day 1)、把首屏静态资源合并到 assets 清单里(第 3 天)、手工预创建首个路由的 Element(第 5 天)。612ms 这个成绩在安卓中端机上已经接近原生性能。

RN 0.71 引入了 Hermes 引擎的预编译字节码。说实话,这算是一次实质性的进步,不像以前那样启动时还要现场 parse 一堆 JS。优化后的 943ms 仍然比 Flutter 多了 331ms。差距从哪来的?

两个原因。Hermes 虽然能预编译,但 JS 代码和原生模块之间还是有一层桥接,启动时创建 JSI Runtime 的耗时跑不掉。RN 的启动链路更长,从 Application 入口到 JS Context 可用,中间要等待的异步回调更多。

不过呢,我们团队确实遇到过一个例外——需要植入大量第三方 SDK 的 App。RN 可以完全跳过 UI 渲染、先让 SDK 在原生层完成初始化;Flutter 的引擎启动和 Dart 虚拟机初始化是绑定的,躲不开。在这个场景下,RN 的“劣后渲染”策略反而能更快进主界面。

冷启动的结论:纯比拼速度,选 Flutter;逻辑复杂、SDK 众多的 App,RN 可以接受。

帧率与滚动:GPU 管线的差异

列表滚动是最常见的性能试金石。Flutter 用 Skia GPU 引擎直接在画布上画 UI,滚动过程中不需要跨越框架边界去同步原生视图属性。

我们实测下来,Flutter 的 58.7fps 在 iPhone 13 上接近满帧,而且滚动全程帧率曲线很平稳,没有出现明显的卡顿尖峰。在 500 条商品的双列瀑布流场景下,Flutter 3.0 的 re-layout 耗时被控制得极好,Impeller 渲染后端确实帮了大忙。

RN 的 55.2fps 也不算难看,但曲线有规律性的掉帧尖峰。一个典型场景:社区信息流里的图片懒加载触发时,JS 线程要同时处理布局计算和图片 URL 解析,两个任务抢占同一个线程,就出现了肉眼可见的轻微跳动。

我们团队在 Flutter 项目里做过一个极端测试(第 2 周):在滚动列表里嵌套 6 层 Container + 阴影 + 半透明遮罩,Flutter 依然能保持 52fps 以上。相同结构的 RN 页面直接掉到 38fps。阴影和透明度在 RN 里需要每帧同步给原生视图,复杂度一上去就撑不住。

坑爹。RN 的长列表复用机制(VirtualizedList)比 Flutter 的懒加载复杂得多。Flutter 用 Viewport + Sliver 天然只能渲染可视区域,RN 还要自己关注 onScroll 触发更新的节流。

不过要反过来看,Flutter 的 GPU 管线在旧款安卓机(骁龙 7 系以下)上有反效果。我们在百元机上实测 Flutter 滚动帧率反而比 RN 更低,因为 Skia 在低端 GPU 上的驱动兼容性没有 iOS 好。中高端设备上 Flutter 完胜,低端机各有各的难过。

内存与体积:真相与误解

内存是两边差距最明显的地方。Flutter 优化后 64MB 峰值,RN 89MB,差了 25MB。RN 的 JS 引擎在运行时需要一个堆,加上 Hermes 的快照和 JSI 的跨线程通信开销,内存天生不占优势。

我们团队在 Flutter 里用了简单的图片尺寸降级策略,把 2000px 宽的电商商品图在加载前统一转成 1080px,内存直接下降 14MB。RN 那边搞了同样的策略,只降了 6MB——因为图片解码后还绕了一层 JS 对象的引用,多出来的内存基本都被 JS 引擎自己吃了。

APK 体积对比出乎意料:Flutter 的 22.4MB 比 RN 的 15.8MB 大了 42%。原因在于 Flutter 每个平台架构都会打包一份机器码(arm32 + arm64),而 RN 的 Hermes 字节码体积比机器码要小得多。我们为了省体积启用了 --split-debug-info 和 --obfuscate,只把体积压到 22.4MB。如果想再小,官方支持按 ABI 拆包,但这会提高上架配置的复杂度。

说实话,体积差距对国内用户影响不大。国内大厂 App 都走动态化更新,APK 基础包控制在 30MB 以内就算合格。22.4MB 和 15.8MB 的差距,在应用商店里不会成为用户卸载的理由。

工具链与开发效率:隐性成本

性能对比不能只看运行时。Day 1 开工时,工具链的顺畅程度直接决定项目第一个月的进展。

Flutter 3.0 的命令行工具集和热重载非常稳定。我们团队 Flutter 项目迭代周期平均 2 周一个版本,RN 项目要 3 周。差距集中在哪里?两个地方。

Flutter 的 Hot Reload 在改动 UI 结构时可以保持状态不变,而 RN 的 Fast Refresh 遇到 hook 更新时会经常整页刷新,调样式的时候烦死人。Flutter 的编译报错信息比 RN 的堆栈清晰得多。RN 报错经常给你一串经过 JSI 转译后的内存地址,追踪问题耗时严重。

我们团队统计过数据:同一个页面(商品详情页)从零到功能完成,Flutter 团队用时 3.5 天,RN 团队 5 天。差距 30% 左右。这 1.5 天的差距主要是在调试工具和报错的定位上。

但我也要承认 Flutter 的调试工具不如 RN 的地方:Dart DevTools 的内存快照功能没有 React DevTools 的 React Profiler 直观。RN 的耗时图可以直接看到每个组件重渲染的成本,Dart DevTools 只能手工分析 timeline 的 CPU 时间片。做性能分析时,RN 比 Flutter 更友好。

选型决策树:别只看跑分

给团队选框架,性能只是其中一个维度。我们这次对比基于两个实际业务,综合周边因素给出明确的选型建议:

  • 如果你做的是 UI 密集型电商 / O2O / 工具类 App,选 Flutter。这类 App 的痛点是页面复杂、交互多、需要高度定制 UI,Flutter 的渲染优势和热重载效率能直接转化为人天成本。
  • 如果你做的是内容社区 / 社交 / 资讯类,选 RN。内容型产品的核心不是 UI 细节,而是快速迭代拉新、频繁发版、跟运营配合上 H5 页面。RN 的动态化和 JS 生态优势比那 25MB 内存更重要。

从决策树角度说:

  1. 团队主力技术栈是 JS/TS → RN,学习成本低
  2. 团队熟悉 Java/Kotlin/iOS → Flutter
  3. 项目周期紧,UI 稿复杂 → Flutter
  4. 项目会有大量原生 SDK 接入(支付/推送/地图)→ RN 更省事
  5. 对包体积有硬性要求(比如微信小程序内嵌)→ RN

我们团队内部的新项目默认选了 Flutter。不是因为 Flutter 快 200 毫秒,而是因为我们花了 5 天时间做一个商品详情页,Flutter 的代码量比 RN 少 18%。

说实话,这些选型建议只看重性能的人会觉得我们不够硬核。但做技术选型的人往往忽略了一点:项目上线后 80% 的时间花在修 bug 和迭代功能上,每次 UI 改版都是重新渲染一整个页面。热重载快一次,团队成员一天就多两次有效调试。

长远隐患与版本漂移

任何选型都必须想半年后的情况。

Flutter 3.0 的问题在于 Dart 语言的生态仍然薄。我们遇到过一个真实需求:App 内嵌一个富文本编辑器,Flutter 的第三方库成熟度明显不如 RN 的 Slate/RichEditor 体系,最后自己手写了两周。RN 的 npm 生态有 22.9 万个包,而 pub.dev 只有 2.8 万个 —— 差了 8 倍。

RN 的迭代反而更麻烦。0.71 到 0.72 的升级就破坏了我们的自定义原生模块接口。每次大版本升级都像是拆炸弹,双端原生代码 + JS 层三方库的版本匹配可能要花 3 天时间挨个测试。

第 2 周的升级事故尤其值得说:RN 0.71 的 JSI 接口从 0.68 开始就一直变,每次升级都要重新编译原生模块。Flutter 3.0 的 pub 包兼容性好太多,很少碰到 API 断裂的更新。

从版本演进看,Flutter 的性能会继续吃 AOT+Impeller 的红利,RN 则要把性能赌在 New Architecture(Fabric + TurboModule)全面铺开之后。我们的 RN 项目在 0.72 上依然关闭了 New Architecture,因为开启后崩溃率从 0.17% 攀升到 0.33%,稳定性扛不住。

局限在哪里

我必须诚实说明:这次对比不是实验室条件,不能直接套用到所有项目。两个 App 的业务差异导致数据天然被带偏——Flutter 项目是电商,RN 项目是社区,这两个业务对 UI 渲染路径和并发模型的要求完全不同。

我们团队的 Flutter 项目有 6 名成员,RN 项目只有 4 名,团队规模上的差异也影响了迭代周期和代码质量。要是把两个项目的成员互换,数据可能是另一番光景。

但我也要承认 Flutter 不如 RN 的地方:Flutter 对已有原生代码库的项目是灾难。

如果你已经有了一个做了两年的原生 App,想用跨端框架拯救它,Flutter 的迁移成本远高于 RN。RN 可以按页面渐进接入,原生页面和 RN 页面可以同时存在。Flutter 3.0 的混合栈方案虽然出了官方支持,但加壳后在低端机的内存开销又涨回 85MB,性能优势被削弱一半。RN 的渐进接入在这类项目里几乎是唯一解。

对已有项目做跨端改造:选 RN。
从零搭建新产品:选 Flutter。

这一条判断比任何性能数据都重要。

这组对比做下来,我们的结论只有一个:Flutter 的硬件利用率更高,RN 的软件生态更厚。你团队缺的是哪种优势?

我们团队在红米 Note 11T 上复现了 Flutter 612ms 冷启动的同一套配置,把 APK 拆包方案公开成了一份 wiki。下个月我们打算跑一遍 React Native 的 New Architecture,看看 FPS 能不能反超。到时候见分晓。