React Native 动画的真实代价:逐方案基准测试
- 原文链接:expo.dev/blog/the-re…
- 原文作者:Janic Duplessis(客座)
2026 年 4 月 28 日 · React Native · Development
本文为客座文章,作者 Janic Duplessis——他是 App&Flow 的咨询负责人,也是长期的 React Native 贡献者。
……
想象一个登录页:背景缓缓漂移,那种细微动效会让产品显得精致,而不是「凑合能用」。听起来很简单。我们用 Reanimated 实现并上线了。但每隔一阵子,你偶尔会察觉到掉帧——刚好足以让你觉得不对劲,一旦注意到就很难忽略。
根本原因在于:Reanimated 每一帧都会在 UI 线程上运行。当同一帧里应用还要做大量其他工作(比如触发重渲染、列表滚动、输入更新),动画预算就会被挤压,原本缓慢的背景平移就会变成缓慢的卡顿。
在 App & Flow,我们服务的对象是那些愿意在细节上较真的产品团队,并为他们交付 React Native 应用与配套工具;界面要流畅、交互要有原生般的顺手质感,是我们一贯看重的一环。也正因如此,我们不愿意只靠权宜式的绕行做法把问题糊弄过去,而是转向去寻求更干净、更可持续的解决路径。
在 iOS 上,Core Animation 会把动画交给操作系统渲染服务器(render server),之后就不再触碰你的线程。一旦你提交了 CAAnimation,系统接管驱动,你的应用就完全置身事外。我们希望在 React Native 里也能这样。于是有了 react-native-ease:一套声明式动画库,完全走平台原生 API(iOS 用 Core Animation,Android 用 ObjectAnimator),没有 JS 循环、没有 worklet,也没有每一帧对 shadow tree 的提交。
但在构建它的过程中,出现了一个我们想坦诚回答的问题:动画库的选择究竟有多大影响?
因此我们做了测量。跨四种方案、两个平台,并在高端与中端设备上追踪每一帧的 UI 线程开销。本文分享我们的发现,并尝试回答真正重要的问题:帧代价有多大?在哪些类型的应用里它会要紧?以及你在选择动画库时应该优先考量什么?
Ease 与 Reanimated
💡 Reanimated 侧的掉帧是模拟出来的。我们注入了人工的 UI 线程压力,以复现繁忙应用中的情形。真实世界的卡顿取决于你的工作负载、设备,以及同一时刻 UI 线程上还有多少其他事情在发生。
受测的四种 React Native 动画方案
所有基准测试均在 2026 年 4 月进行,环境为 Expo SDK 55、React Native 0.83、Reanimated 4.3.0、react-native-ease 0.7.0。
我们对比了四种动画方案:
- Ease: react-native-ease,直接使用平台 API。动画在 JS 侧以 props 描述,由原生侧驱动,没有任何逐帧的 JS 参与。
- Reanimated(Shared Values): 标准的基于 worklet 的路径。数值在 UI 线程上由 C++ worklet 运行时驱动,但每一帧仍会通过 shadow tree 更新 props。
- Reanimated(CSS Animations): Reanimated 较新的 CSS 动画 API。声明式风格类似 Ease,但底层仍是 Reanimated 的动画引擎。
- RN Animated: React Native 内置的
AnimatedAPI,配合useNativeDriver: true。数值由原生驱动,但具体实现因平台而异。
我们还测试了在启用 Reanimated 静态特性开关时的表现,具体为 ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS 与 IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS:当仅更新非布局相关的 props(例如 transform 与 opacity)时,它们允许 Reanimated 跳过 shadow tree 提交。这是一项值得单独强调的优化。
注: RN 0.85 引入了新的 Shared Animation Backend,最终会让上述特性开关不再必要。Reanimated 的接入仍在进行中,尚未发布。
基准测试如何度量每一帧的开销
我们在示例应用里做了一个基准屏:在循环中同时动画化 N 个视图(translateX,2 秒,线性,重复)。并通过自定义 Expo 原生模块来度量每一帧的开销:
- iOS: 我们在运行时替换
CADisplayLink的工厂方法,拦截任意框架注册的 display link 回调,再按帧时间戳聚合统计每次回调的墙钟时间。 - Android: 使用
Window.OnFrameMetricsAvailableListener,它会上报来自平台帧度量系统的ANIMATION_DURATION、LAYOUT_MEASURE_DURATION与DRAW_DURATION。
每次测试采集 5 秒窗口,并运行多种配置,以同时展示 Reanimated 最差与最好的情况。
基准结果:iOS 与 Android 上的逐帧 UI 线程成本
安卓设备(Moto G8 Plus)
在安卓上,这是各库之间最「苹果对苹果」的比较。所有方案都跑在 UI 线程上,因此你看到的是各动画引擎每一帧到底加了多少活的直接度量,没有花招,也没有捷径。
构建配置有多大影响?(50 个视图,平均毫秒)
对 Reanimated 性能影响最大的单一变量,并不是你选哪一种动画 API,而是你在 调试构建还是发布构建下测试。
| 配置 | Ease | Reanimated SV | Reanimated CSS | RN Animated |
|---|---|---|---|---|
| 调试版,未开特性开关 | 6.18 | 28.62 | 27.41 | 9.38 |
| 发布版,未开特性开关 | 3.14 | 11.87 | 11.20 | 8.78 |
| 发布版,全开特性开关 | 3.43 | 10.57 | 9.06 | 8.82 |
红线是 60fps 下 16.67ms 的帧预算。在调试构建下,仅 50 个视图,Reanimated SV 与 CSS 就会双双超出预算,直接掉帧;而同样的动画在发布构建下大约在 11ms。调试构建会骗人。如果你在开发阶段看到动画卡顿,请先在发布构建里复现,再开始慌张。
这些特性开关通过绕开非布局 props 的 shadow tree 提交,又带来约 11%–19% 的收益。它们可能在部分应用中引发界面异常,因此是可选开启的;但若你在关注开销,值得尝试。
开销如何随视图数量扩展?(发布构建,特性全开,平均毫秒)
| 视图数 | Ease | Reanimated SV | Reanimated CSS | RN Animated |
|---|---|---|---|---|
| 10 | 2.34 | 7.25 | 5.56 | 5.13 |
| 100 | 3.92 | 11.98 | 10.26 | 9.93 |
| 500 | 6.63 | 39.94 | 23.29 | 21.85 |
💡 500 个视图是压力测试,不是现实目标。如果你同时在动画 500 样东西,动画库也许已经不是你最大的问题了。
在 10–100 个视图时,所有方案平均而言都还在帧预算之内,不过在 100 个视图时 Reanimated 与 RN Animated 距离预算只剩不到 5ms,留给同一帧里其他工作的余量很小。在 500 个视图时,只有 Ease 仍在预算内。Reanimated SV 达到 39.94ms,超过帧预算一倍还多——而且这还是在已优化的配置下。
iOS 设备(iPhone 15 Pro)
iOS 上架构差异大到无法忽视。在安卓上,所有库共享 UI 线程,比较是公平的。而在 iOS 上,Ease 可以「合理地作弊」:Core Animation 跑在独立的 OS 渲染服务器进程里,完全在你的应用之外。一旦 Ease 注册了 CAAnimation,系统接管,你的线程就能去做别的事情。这就是 Ease 全程显示约 0.01ms 的原因:UI 线程上每一帧真的没有额外动画工作。代价是:Core Animation 动画无法在半途中从 JS 读取或打断——也正因如此,手势驱动的动画仍然属于 Reanimated。
每一帧显示链路回调耗时(毫秒,发布构建)
| 视图数 | Ease | Reanimated SV | Reanimated SV (FF) | Reanimated CSS | Reanimated CSS (FF) | RN Animated |
|---|---|---|---|---|---|---|
| 10 | 0.01 | 1.33 | 1.08 | 1.06 | 0.63 | 0.83 |
| 100 | 0.01 | 3.72 | 3.33 | 2.71 | 2.48 | 3.32 |
| 500 | 0.01 | 6.84 | 6.54 | 4.16 | 3.70 | 4.91 |
绝对数值低于安卓侧,因为该度量只捕获 UI 线程回调时间。但结论不变:在 iOS 上,无论有多少视图在动画,Ease 几乎不增加 UI 线程成本;而其他方案每一帧仍在持续工作。
为何不同 React Native 动画库的逐帧成本不同
影子树带来的额外开销
每一帧,Reanimated 的 worklet 会计算新数值,并通过 shadow tree 提交 props 更新。这次提交会跑 Yoga 布局、属性 diff 与视图变更。当你动画化的是 transform 或 opacity(对布局毫无影响的属性)时,这些工作全是浪费:你付出完整布局路径的代价,只为把某个图形向左挪三像素——Yoga 根本不需要知道。
特性开关(ANDROID/IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS)通过对 UI 层直接推送视觉属性更新来短路这一过程,完全跳过布局路径。在 Moto G8 Plus、50 个视图场景下,它们把 Reanimated SV 从 11.87ms 降到 10.57ms(-11%),把 CSS 从 11.20ms 降到 9.06ms(-19%)。它们是可选的,因为可能在部分应用中引发界面异常;但若你在追开销,它们应是第一站。
RN Animated
useNativeDriver: true 的 RN Animated 同样不会每一帧占用 JS 线程,但它通过单独的原生动画模块驱动,并对每个被动画化的节点持有簿记开销。在低到中等视图数量下表现尚可,但随着动画视图增多,扩展性比 Reanimated CSS 更差——部分原因在于它缺少特性开关所启用的那些 shadow tree 优化。
在生产应用中,何时该认真对待动画库选择
它最影响长时间或慢速动画:骨架屏、漂移背景、氛围型 UI 动效。五秒动画里只要掉一帧都很显眼,而且几乎总会有其他工作同时进行(拉数、重渲染、用户交互)。列表场景也同样敏感:屏幕上很容易出现成百上千个同时在动画的条目。在低端设备上,微小的逐帧开销会迅速累积,用户往往比你更早察觉。
对短促的一次性过渡(按钮反馈、轻提示、弹窗)而言,开销可以忽略,任意库都够用。
值得注意的是:Ease 只覆盖本文讨论的这类场景。手势驱动的动画(与滚动联动、拖拽、滑动)以及任何会改动布局属性(宽、高、内边距等)的需求,仍需要 Reanimated 或 RN Animated。Ease 专为声明式、由触发器驱动的视觉属性动画而生。
React Native 0.85 与 Shared Animation Backend
React Native 0.85 附带实验性的 Shared Animation Backend——由 Meta 与 Software Mansion 直接做进渲染器的统一动画引擎。一旦 Reanimated 完成接入,SYNCHRONOUSLY_UPDATE_UI_PROPS 将不再必要,因为绕过 shadow tree 会成为默认路径,「默认 Reanimated」与「优化版 Reanimated」之间的差距也会基本消失。
但架构差异仍在:Ease 根本没有逐帧动画引擎。即便后端更快,Reanimated 仍会每一帧计算数值并推送 props——开销不会消失,只是变小。我们会在集成落地后更新基准测试。
自行运行 React Native 动画基准
基准已内置在示例应用中。克隆仓库后运行 yarn example ios 或 yarn example android,在演示屏打开基准测试页。源码位于 example/src/demos/BenchmarkDemo.tsx,原生模块在 example/modules/frame-metrics/。
请使用发布构建。调试模式会显著抬高 Reanimated 的数值。如果你的数字看起来吓人,多半是这个原因。
yarn example ios --configuration Release
yarn example android --variant release
react-native-ease 由 App & Flow 构建——这是一家位于蒙特利尔的 React Native 工程团队,亦获 Expo 推荐。
术语表(本篇命中)
| 术语 | 英文 | 释义 |
|---|---|---|
| 渲染服务器 | render server | iOS 上 Core Animation 所在的系统进程,与应用进程分离 |
| 特性开关 | feature flags | 编译期或静态配置,用于启用 Reanimated 等库的优化路径 |
| 帧预算 | frame budget | 在给定刷新率下每帧可用的时间上限(如 60fps 约 16.67ms) |
| Shadow tree | shadow tree | React Native 在原生侧维护的布局/属性树,提交会触发 Yoga 等流程 |
| Shared Animation Backend | Shared Animation Backend | RN 0.85 引入的实验性统一动画后端 |
| Worklet | worklet | Reanimated 在 UI 线程运行的 JS 片段 |
| Yoga | Yoga | React Native 使用的布局引擎 |