【翻译】React Native 动画的真实代价:逐方案基准测试

9 阅读11分钟

React Native 动画的真实代价:逐方案基准测试

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 线程开销。本文分享我们的发现,并尝试回答真正重要的问题:帧代价有多大?在哪些类型的应用里它会要紧?以及你在选择动画库时应该优先考量什么?

视频来源:YouTube 嵌入

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 内置的 Animated API,配合 useNativeDriver: true。数值由原生驱动,但具体实现因平台而异。

我们还测试了在启用 Reanimated 静态特性开关时的表现,具体为 ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPSIOS_SYNCHRONOUSLY_UPDATE_UI_PROPS:当仅更新非布局相关的 props(例如 transformopacity)时,它们允许 Reanimated 跳过 shadow tree 提交。这是一项值得单独强调的优化。

注: RN 0.85 引入了新的 Shared Animation Backend,最终会让上述特性开关不再必要。Reanimated 的接入仍在进行中,尚未发布。

基准测试如何度量每一帧的开销

我们在示例应用里做了一个基准屏:在循环中同时动画化 N 个视图(translateX,2 秒,线性,重复)。并通过自定义 Expo 原生模块来度量每一帧的开销:

  • iOS: 我们在运行时替换 CADisplayLink 的工厂方法,拦截任意框架注册的 display link 回调,再按帧时间戳聚合统计每次回调的墙钟时间。
  • Android: 使用 Window.OnFrameMetricsAvailableListener,它会上报来自平台帧度量系统的 ANIMATION_DURATIONLAYOUT_MEASURE_DURATIONDRAW_DURATION

每次测试采集 5 秒窗口,并运行多种配置,以同时展示 Reanimated 最差与最好的情况。

基准结果:iOS 与 Android 上的逐帧 UI 线程成本

安卓设备(Moto G8 Plus)

在安卓上,这是各库之间最「苹果对苹果」的比较。所有方案都跑在 UI 线程上,因此你看到的是各动画引擎每一帧到底加了多少活的直接度量,没有花招,也没有捷径。

构建配置有多大影响?(50 个视图,平均毫秒)

对 Reanimated 性能影响最大的单一变量,并不是你选哪一种动画 API,而是你在 调试构建还是发布构建下测试。

对比图表

配置EaseReanimated SVReanimated CSSRN Animated
调试版,未开特性开关6.1828.6227.419.38
发布版,未开特性开关3.1411.8711.208.78
发布版,全开特性开关3.4310.579.068.82

红线是 60fps 下 16.67ms 的帧预算。在调试构建下,仅 50 个视图,Reanimated SV 与 CSS 就会双双超出预算,直接掉帧;而同样的动画在发布构建下大约在 11ms。调试构建会骗人。如果你在开发阶段看到动画卡顿,请先在发布构建里复现,再开始慌张。

这些特性开关通过绕开非布局 props 的 shadow tree 提交,又带来约 11%–19% 的收益。它们可能在部分应用中引发界面异常,因此是可选开启的;但若你在关注开销,值得尝试。

开销如何随视图数量扩展?(发布构建,特性全开,平均毫秒)

开销如何随视图数量变化?

视图数EaseReanimated SVReanimated CSSRN Animated
102.347.255.565.13
1003.9211.9810.269.93
5006.6339.9423.2921.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。

每一帧显示链路回调耗时(毫秒,发布构建)

每一帧显示链路回调耗时(毫秒,发布构建)

视图数EaseReanimated SVReanimated SV (FF)Reanimated CSSReanimated CSS (FF)RN Animated
100.011.331.081.060.630.83
1000.013.723.332.712.483.32
5000.016.846.544.163.704.91

绝对数值低于安卓侧,因为该度量只捕获 UI 线程回调时间。但结论不变:在 iOS 上,无论有多少视图在动画,Ease 几乎不增加 UI 线程成本;而其他方案每一帧仍在持续工作。

为何不同 React Native 动画库的逐帧成本不同

影子树带来的额外开销

每一帧,Reanimated 的 worklet 会计算新数值,并通过 shadow tree 提交 props 更新。这次提交会跑 Yoga 布局、属性 diff 与视图变更。当你动画化的是 transformopacity(对布局毫无影响的属性)时,这些工作全是浪费:你付出完整布局路径的代价,只为把某个图形向左挪三像素——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: trueRN 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 iosyarn example android,在演示屏打开基准测试页。源码位于 example/src/demos/BenchmarkDemo.tsx,原生模块在 example/modules/frame-metrics/

请使用发布构建。调试模式会显著抬高 Reanimated 的数值。如果你的数字看起来吓人,多半是这个原因。

yarn example ios --configuration Release
yarn example android --variant release

react-native-easeApp & Flow 构建——这是一家位于蒙特利尔的 React Native 工程团队,亦获 Expo 推荐。

术语表(本篇命中)

术语英文释义
渲染服务器render serveriOS 上 Core Animation 所在的系统进程,与应用进程分离
特性开关feature flags编译期或静态配置,用于启用 Reanimated 等库的优化路径
帧预算frame budget在给定刷新率下每帧可用的时间上限(如 60fps 约 16.67ms)
Shadow treeshadow treeReact Native 在原生侧维护的布局/属性树,提交会触发 Yoga 等流程
Shared Animation BackendShared Animation BackendRN 0.85 引入的实验性统一动画后端
WorkletworkletReanimated 在 UI 线程运行的 JS 片段
YogaYogaReact Native 使用的布局引擎