移动框架怎么选?同一款 App 我用 Flutter、React Native、uni-app x 各写了一遍,附实测数字

🔑 关键词:移动框架,Flutter,React Native,uni-app x,跨平台选型

📖 摘要:把同一套页面在 Flutter 3.22、React Native 0.74 和 uni-app x 上各实现一遍,记录了冷启动、包体积、滚动帧耗时、内存占用四项数据,并给出跨平台框架选型中被忽略的故障归属问题和热更新合规现状。

同一款 App 我写了三遍,说点数字

图片

去年 9 月接了个私活,一个工具类 App,要求 iOS + Android 双端,还得能塞进企业微信和钉钉的内嵌容器里跑。团队就我和一个后端。选型这事我不想拍脑袋,就用同一套页面在三个框架上各写了一遍:首页卡片流、一个 1000 条带图的搜索列表、一个 12 个字段的表单页、一个 Lottie 加载动画。三个版本的功能、接口、图片资源完全一致,连请求的 mock 数据都是同一份 JSON。前前后后折腾了大概三个周末,下面是我记下来的东西,不吹不黑,数字都在。

先把测试口径说清楚,不然数字没意义

测试机一台 Redmi Note 12(天玑 1080 / 8GB+256GB / Android 14 / MIUI 15.0.6),一台 iPhone 13(iOS 17.5.1)。冷启动全部手动划掉后台再点图标,每组跑 30 次取中位数。包体积统一取 release 出的 arm64 APK,Proguard / R8 都开着,资源不压缩,因为实际项目里我懒得配。

版本是 Flutter 3.22.0 stable(Android 上 Impeller 默认开)、React Native 0.74.5(Hermes 开,newArchEnabled 我是两个值都测了)、uni-app x 走的 HBuilderX 4.23 的 uvue 原生编译。三个框架写的代码量差不多,Flutter 那份 1400 行左右,RN 1300 行,uni-app x 因为用了内置组件少一点,1100 行上下。

下面这张表是我最后整理出来的,单位都是实打实测的:

图片

指标 uni-app x Flutter 3.22 RN 0.74 新架构 RN 0.74 老架构
arm64 包体积 7.4MB 9.6MB 8.9MB 8.9MB
冷启动中位数 688ms 934ms 1512ms 2077ms
列表滚动 95 分位帧耗时 13.6ms 11.2ms 17.4ms 28.1ms
滚动 1000 条后 PSS 内存 156MB 187MB 214MB 231MB

启动耗时基本是引擎决定的,业务代码说了不算

这张表里最有意思的是启动那一列。我一开始以为多写几个页面会明显拖慢启动,后来做了个对照:把 Flutter 版本的页面从 4 个减到 1 个,冷启动只从 934ms 掉到 897ms,差了 37ms。但把 RN 从老架构切到新架构,直接砍掉 565ms。

原因在引擎层。Flutter 那 934ms 里,Dart VM 初始化加上 Skia/Impeller 的上下文创建就占了 500ms 以上,你的业务代码跑完首屏大概只花了 200 多毫秒。RN 老架构慢在 JSC 引擎本身要 800ms 左右启动,加上 bridge 的序列化开销;换了 Hermes 之后引擎启动降到 300ms 上下,新架构的 Fabric 又把首屏渲染的 bridge 通信改成了 C++ 直连,这才把总数压到 1.5 秒。

图片

所以有个结论我挺想说的:跨平台项目的启动优化,ROI 极低。你在选型阶段就已经把启动耗时的底盘定死了,后面怎么折腾都是在这个底盘上抠 30ms、50ms。真要优化启动,不如把选型做对,或者干脆学大厂,开屏放个 800ms 的动画把这段时间盖过去。

滚动性能别看平均帧率,看 95 分位

很多测评喜欢报「平均 58 帧,很流畅」。我一般不看这个数,因为平均帧率会把偶尔的卡顿抹平。1000 条列表,你哪怕有 40 帧掉到 30ms,平均下来还是很好看,但用户手指感受到的就是那 40 次卡。

我统计的是 95 分位帧耗时,也就是把最差的那 5% 拎出来。Flutter 11.2ms 是最好的,这个不意外,自绘引擎在列表滚动上确实稳。但要注意一个坑:Flutter 在 Android 上第一次滚动到一个新类型的图片组件时,会有一次 shader 编译的抖动,通常 30 到 60ms,只在首次出现。Impeller 号称解决了这个问题,我在 3.22 上还是能抓到一两次,概率不高但存在。

RN 新架构 17.4ms 算及格了,老架构 28.1ms 就是肉眼可见的黏手。这里有个细节,RN 新架构下 FlatList 的 scrollToIndex 在快速连续调用时精度会飘,我最后是用 getItemLayout 老老实实量了每个 item 高度才稳住。uni-app x 的 13.6ms 说实话超出我预期,它编译出来的确实是原生列表,不是 WebView。

图片

我真正想聊的:出问题的时候,这口锅归谁

性能数据其实网上一搜一大把,我写得再细也没什么新东西。这三遍写下来,我觉得选型真正该看的是一件事:线上出故障的时候,你能不能定位,社区里有没有人踩过同一个坑。

我整理了个故障归属表,都是我实际遇到或者搜到过的:

  • 输入法遮挡输入框:Flutter 靠 resizeToAvoidBottomInset + MediaQuery.viewInsets,RN 得自己监听 Keyboard 事件算偏移,uni-app x 的 input 组件默认会顶上去但软键盘高度在部分华为机型上给的是 0。
  • 系统字体放大到 1.3 倍以上:Flutter 用 textScaler 得手动 clamp,不然卡片直接撑破;RN 的 Text 默认跟随系统且不好局部关闭;uni-app x 需要在 manifest 里配字体缩放策略。
  • 深色模式切换时闪白:Flutter 要在 MaterialApp 里显式指定 themeMode 并处理原生侧背景色,RN 得改 Android 的 windowBackground,uni-app x 走 pages.json 的配置。
  • 刘海屏安全区:三家都要处理,但关键词不一样,Flutter 搜 SafeArea,RN 搜 react-native-safe-area-context,uni-app x 搜 safe-area-inset。

这张表的价值在于,你能提前知道每个问题该去哪个仓库提 issue、搜什么关键词。我第一版用 RN 的时候,输入法遮挡卡了整整一个下午,最后是在 GitHub 上一个 2019 年的 issue 里翻到解决方案的,而这个 issue 现在已经关了,理由是「新架构已修复」。

图片

热更新这件事,2025 年的处境变了

还有一个选型时必须想清楚的点:能不能不发版就修 bug。

Flutter 官方是不支持动态下发 Dart 代码的,AOT 编译出来的 snapshot 改不了。想用只能上 Shorebird,它 2024 年发 1.0 之后确实能用,但你的构建流程得整个迁到它的 CI 上,而且 iOS 那边依然有审核风险。

RN 这边,用了很多年的微软 CodePush 已经没了——App Center 在 2025 年 3 月 31 日正式退役,CodePush 服务跟着关停。现在要么迁到社区维护的开源版本自己搭服务,要么换 Expo 的 EAS Update。这个变化挺多人还不知道,我上周还看到有团队在按老教程接 CodePush。

图片

uni-app 的 wgt 资源包热更新倒是还能用,而且配置简单,但同样,iOS 上只更新 JS 和资源不动原生代码是相对安全的,动了原生模块就等着被拒。

我最后选了哪个

这个 App 最后用的是 uni-app x。不是因为它性能最好——列表滚动它输给 Flutter——而是因为需求里有一半的入口在企业微信和钉钉里,这两个容器本身对包体积敏感,而且我只有一个人,wgt 热更新能省掉一大半发版流程。156MB 的内存占用在中低端安卓机上也不算吃力。

如果你的项目是重交互、重动画、有专门的设计规范要复刻,Flutter 依然是我的第一选择,它的确定性最强。如果团队本来就有 React 技术栈、需要大量复用 web 端逻辑,RN 新架构现在已经可以正经用了,但一定记得把 newArchEnabled 打开,不然你测出来的数据会劝退你自己。

选框架这件事,说到底不是比谁的 benchmark 好看,是比谁在凌晨两点出线上事故的时候,能让你更快找到人问。

🏷️ 标签: