我们拿五个主流跨端框架跑了同一套启动流程,冷启动到首屏的耗时差异比想象中大得多
我们用同一套业务代码,在五个主流跨端框架上复现了完全相同的首页启动流程,然后在多台低端、中端、高端设备上反复测冷启动耗时。结果差异远超预期:最快的 Flutter 在最差低端机上首屏完成时间也只有 680ms 左右,最慢的框架在同样条件下跑到 2200ms 以上,差了 3 倍还多。而且这不是某个环节的微小差距,是每一段链路——从进程创建、JS 引擎初始化、框架运行时注入、首帧渲染提交——都在积累差距。
以下所有数据,都基于我们统一构造的测试场景:首页包含一个顶部导航栏、一段本地静态文案、一个网络请求拉取 6 条列表项,列表项带简单头像和文字。我们分别用 React Native 0.76、Flutter 3.22、uni-app(Vue3 + webpack + 原生小程序引擎)、Taro 3.6(React + 原生小程序引擎)、以及 Kotlin Multiplatform + Compose Multiplatform 1.6 实现完全一致的 UI 和逻辑,然后用同一套自动化脚本测量从点击桌面图标到首屏列表数据展示完毕的时间。
测试设备分三档:低端(骁龙 662 / 4GB RAM / Android 11)、中端(骁龙 870 / 8GB RAM / Android 13)、高端(骁龙 8 Gen 2 / 12GB RAM / Android 14)。每台设备至少跑 20 次冷启动,取 P50 耗时。
最后的完整链路拆解,我们才真正看清楚每个框架的耗时花在哪里。
Flutter:引擎预热的优势太明显了
Flutter 的冷启动链路最短,耗时最稳定,低端机 P50 首屏 680ms。
Flutter 的启动过程大致是:Android 进程创建 → FlutterActivity 初始化 → FlutterEngine 启动 → Dart VM 初始化 → 执行 main() → 首帧渲染。关键点在于,Flutter 的引擎初始化(包括 Skia/Impeller 图形层和 Dart VM)完全可以在 Application 级别提前预热,我们在测试里也这么做了——在 Application.onCreate 里创建了一个 FlutterEngineGroup,预创建一个 engine,这样 FlutterActivity 启动时直接复用,省掉了 Dart VM 初始化和着色器加载的大头。
实测拆解下来的各段耗时(低端机 P50):
- 进程创建 + Application 初始化:120ms
- FlutterActivity 启动 + engine 复用:45ms
- Dart isolate 启动 + main():60ms
- 首帧渲染(包括布局、光栅化):180ms
- 网络请求 + 数据填充到列表:275ms
总链路 680ms,其中网络请求占了将近一半。如果我们把数据做成预缓存或者用骨架屏先出首帧,这个数字还能往下压到 400ms 左右。Flutter 的另一个优势是渲染管线和业务逻辑跑在独立的 Dart isolate 里,不受 Android 主线程 Looper 消息调度的影响,所以首帧渲染阶段的耗时方差极小,20 次测试里 P50 和 P95 只差 40ms。
React Native:新架构有改善,但 JS 引擎初始化仍然吃时间
React Native 0.76 已经默认启用了新架构(Fabric + TurboModules + Bridgeless),低端机 P50 首屏 980ms。
拆解链路:
- 进程创建 + Application 初始化:125ms
- React Native 实例创建 + Hermes 引擎启动:210ms
- JS bundle 加载与解析:185ms
- React 组件树构建 + 首帧提交:220ms
- 网络请求 + 数据填充:240ms
Hermes 的启动速度已经比老的 JSC 快了不少,但 210ms 的引擎初始化加上 185ms 的 bundle 解析,这两个阶段加起来就快 400ms 了,直接拖累了整体启动。而且我们在低端机上观察到,JS 线程和 UI 线程的同步开销在新架构下比旧架构减少了约 25%,但依然存在——每次 Fabric 提交 ShadowTree 到原生侧,还需要一次线程切换,这在低端机上每次大约消耗 8-12ms,积少成多。
一个值得注意的点是,React Native 的首帧是「白屏 → 首帧渲染」的跳变,而 Flutter 可以在 engine 预热阶段就显示一个 Splash 画面,等首帧渲染好了无缝切换,视觉上感知的启动时间会更好一些。我们在测试里只测量到首屏数据展示完毕,没有考虑 Splash 层的视觉欺骗,所以 RN 的 980ms 是实打实的完全可用时间。
uni-app 和 Taro:小程序引擎的启动开销绕不开
uni-app(Vue3 + webpack)和 Taro 3.6(React)在 Android 上都是通过原生小程序引擎跑的,本质上是 WebView 或小程序容器的启动链路。低端机上 uni-app 的 P50 首屏是 1620ms,Taro 是 1750ms。
链路拆解(以 uni-app 为例):
- 进程创建 + Application 初始化:130ms
- 小程序容器初始化(JS 引擎 + 框架层注入):340ms
- 页面栈创建 + 路由匹配:95ms
- Vue 框架初始化 + 组件渲染(在 WebView 内):380ms
- 首帧绘制(WebView 渲染管线):290ms
- 网络请求 + 数据填充:385ms
小程序容器的初始化本身就包含了类似小游戏引擎的一套运行时:JS 引擎(通常是非 Hermes 的 V8 或 JSC,取决于系统 WebView)、小程序基础库注入、渲染层和逻辑层的双线程通信框架等。340ms 只是容器启动时间,还不包括业务代码的加载。Vue 框架在 WebView 里初始化又花了 380ms,然后 WebView 的首帧渲染管线本身就慢,290ms 是光栅化和合成的耗时。这几段加起来就破千毫秒了。
Taro 的情况类似,不过因为编译时对 React 做了一些优化(比如虚拟 DOM 的静态标记),组件渲染阶段比 uni-app 略快 20ms 左右,但整体链路仍然在 1700ms+。这些框架在真实用户场景里通常配合分包、预加载等手段,但在我们公平对比的冷启动条件下,容器化方案的开销是结构性的,很难靠优化业务代码大幅削减。
Kotlin Multiplatform + Compose Multiplatform:原生快车道,但生态初始化拖后腿
KMP + Compose Multiplatform 1.6 的链路最特殊:它的 UI 层是完全原生的 Compose,没有 JS bridge,也没有 WebView,理论上应该接近 Flutter 甚至原生。低端机实测 P50 首屏 840ms,比 Flutter 慢 160ms。
拆解:
- 进程创建 + Application 初始化:110ms
- Compose 初始化 + 主题/字体加载:95ms
- KMP 共享模块初始化(Kotlin/Native runtime):155ms
- 首帧 Compose 布局与渲染:185ms
- 网络请求 + 数据填充:295ms
多出来的时间主要花在 KMP 共享模块的初始化上。我们的业务逻辑跑在 shared module 里,用的是 Kotlin/Native 编译的 .so,初始化时需要加载运行时、初始化内存管理器、注册各类平台接口。低端机上这个过程稳定在 150ms 左右,中高端会降到 50-80ms。Compose 自身的首帧渲染非常快,185ms 里包含了布局计算和 GPU 提交,跟 Flutter 的 180ms 几乎持平。所以只要 KMP 的运行时初始化能通过预热或者延迟加载优化,这 160ms 的差距是可以追回来的。
为什么差距会累积到 3 倍以上?
很多人只看首屏总耗时,觉得「慢一点也能接受」,但从链路拆解里能看出来,每个框架的慢是「结构性」的,不是某个环节的偶然波动。
Flutter 的引擎预热把 VM 启动和图形层初始化提前到了 Application 阶段,所以冷启动时只剩 Dart isolate 的轻量启动和首帧渲染。React Native 虽然有了新架构,但 JS 引擎和 bundle 加载仍然在冷启动的必经路径上,且无法完全预热——你可以在 Application 里预创建 ReactInstanceManager,但 Hermes 引擎的初始化和 bundle 的加载仍然要等到 JS 线程启动后才能执行。小程序容器方案的问题更大:整个容器的启动链路本身就是为「小程序生命周期」设计的,包含了双线程通信、渲染层初始化、基础库注入等大量固定开销,这些开销在 App 冷启动时是绕不开的。
还有一个容易被忽略的因素是线程调度策略。Flutter 的 UI 线程和 Dart isolate 之间的调度由 engine 自己控制,不受 Android 主线程 Looper 影响,所以渲染管线的每一步都是密集执行的。而 RN 和小程序方案的 UI 操作最终都要落到 Android 主线程上执行,主线程上还有 Choreographer、Input 事件、GC 等竞争,低端机上主线程被抢占 10-20ms 是常事,累积起来就会让首帧提交延迟大幅波动。
优化方向不是「换框架」
这些数据不是为了证明哪个框架好或者差,而是把每条链路的真实耗时摆出来之后,优化的方向就清晰了。
对于 Flutter,提升空间在业务侧:减少 main() 里的同步初始化、用骨架屏代替网络等待、把非首屏必需的 isolate 延迟创建。对于 React Native,可以考虑用 App 级别的 JS 引擎预热(把 Hermes 运行时提前初始化好),以及用 inline require 或 RAM bundle 降低 bundle 加载耗时。小程序容器方案则需要把更多初始化逻辑下沉到原生侧预热,比如在 Application 里提前初始化小程序容器的基础库,以及减少首屏 WebView 的 DOM 规模。
KMP + Compose Multiplatform 的优化重点在 KMP 运行时预热——可以在 Application 里提前触发 shared module 的初始化,让 .so 加载和内存管理器初始化在启动页之前完成。
常见问题
为什么没有测 iOS?
iOS 的系统级启动优化(比如 dyld 共享缓存、系统预创建进程等)会抹平很多框架差异,测出来的数据对比度远不如 Android 明显。我们更关心低端 Android 设备上的真实表现,因为那是用户感知最明显的场景。后续会单独出一版 iOS 的对比数据。
预热的边界在哪里?你们测试里预创建了引擎,这算不算作弊?
预热是工业级 App 的标准做法,不是作弊。微信、淘宝、抖音等超级 App 都在 Application 阶段做了大量预热,包括 WebView 内核、小程序容器、JS 引擎、图片解码器等。我们测试的每个框架都使用了各自文档推荐的最佳实践,Flutter 用了 engine group 预热,React Native 用了 ReactInstanceManager 预创建,小程序方案也做了容器预初始化。如果不做任何预热,所有框架的耗时都会再增加 150-400ms 不等,差距格局不变。
这些数据在实际业务里能复现吗?
能,但有前提。我们的测试页面足够简单,排除了业务逻辑复杂度带来的干扰,所以测出来的是框架本身的下限。实际业务里首页往往有更多组件、更多数据请求、更复杂的布局,每个框架的绝对耗时会等比例放大,但相对差距的格局通常不会改变。如果你的业务首页本身就重,建议也用同样的链路拆解方式在自己的设备上跑一遍,拿到自己业务的实际数据。