跨端RN 与 浏览器Web 的 长渲染性能 差异 与 底层 揭秘

背景

从入职以来,我一直在参与一个跨端项目的开发,主要技术栈是 React Native。在这期间,也不可避免地接触了一些原生能力,比如陀螺仪、手势、生命周期等。

最近,我们开始调研一个问题:
在 App 中,如何实现"持续长时间的动态渲染"?

这里说的"持续长渲染",并不是指一次性的动画效果,而是类似以下场景:

  • Keep 中的跑步轨迹持续绘制
  • 导航 App 中路线与视角的实时跟随
  • 地图、雷达、3D 角色、桌面宠物等长时间存在、持续运动的画面

这些场景的共同点是:

画面会在很长时间内持续更新,并且帧与帧之间存在状态延续。

一开始我并没有理解这个"状态延续"的原理,只是单纯觉得:
不就是一直画吗?


Skia

既然我们本身是 RN 项目,又不想引入 WebView,那最自然的选择就是找"原生渲染能力"。

在 RN 社区里,第一个跳出来的就是 Skia

Skia 是一个由 Google 主导的开源图形渲染库,主要用于 高性能 2D 图形绘制,底层使用 C++ 实现,被广泛应用在 Chrome、Flutter、Android 系统组件中。

在 RN 中,我们通常会通过引入 @shopify/react-native-skia 来使用它。

为什么一开始觉得它很合适?

说实话,Skia看起来几乎是"标准答案":

  • 原生级性能
  • GPU 加速
  • 不走 WebView
  • API 类似 Canvas

而且它的优化十分到位会把把绘制这件事直接下沉到 C++

在最开始的设想中,我甚至觉得:

"只要把动画逻辑写好,Skia 负责画,应该就很稳了。"

但真正开始往"长时间动态场景"上靠的时候,问题慢慢暴露了。

问题并不是性能,是匹配度

Skia 本质上是一个以 2D 图形管线为核心设计的渲染引擎

它非常擅长一件事:把你给它的东西画出来,而且画得很快

但它并不负责:

  • 场景管理
  • 相机系统
  • 空间关系
  • 世界状态的持续演进

换句话说:

Skia 的核心能力是"绘制",而不是"场景"。

当画面开始需要「持续运动」「围绕轨道旋转」「跟随某个路径」时:

  • 每一帧的位置要 JS 算
  • 每一帧的变化要 JS 推
  • 每一帧的绘制触发,还是 JS

只要 JS 线程被打断------

比如 RN 正在处理手势、列表滚动、setState------

画面就会立刻卡住。

到这里我才意识到:
Skia 并不是慢,它只是不解决我现在遇到的这个问题。


Expo GLView + Three.js

既然 Skia 偏 2D,那自然就会往 3D 方向走。

在 RN 生态里,最常见、也是最"官方"的 3D 组合基本就是:

  • expo-gl(GLView)
  • expo-three
  • three.js

这个方案是怎么工作的?

Three.js 在 Web 端已经非常成熟了。但真正梳理它的执行流程时,会发现它在expo的工作其实是这样的:

scss 复制代码
JS 创建 GLView  
⬇️  
JS 初始化 Three.js Scene / Camera / Mesh  
⬇️  
JS 启动 requestAnimationFrame  
⬇️  
每一帧:  
JS 计算动画  
→ renderer.render  
→ 手动调用 gl.endFrameEXP()

如果只是让 GLB 模型在场景中做位移、绕轨道运动,在刻意降低帧率(比如 10~20fps)的情况下,整体是"能看"的,甚至看起来还算流畅。 这类运动有一个共同点: 每一帧只是"位置在变",模型本身并没有发生结构或尺度上的变化。

但当我尝试对 同一个 GLB 模型直接做缩放(scale)动画 时,情况立刻变了。哪怕动画逻辑非常简单,只是不断放大 / 缩小,画面会出现非常明显的:抖动,掉帧,"一卡一卡"的感觉。而且这种卡顿,并不是偶发的,而是稳定复现的

什么原因导致这样的

后面我想都是一样的使用 RN + GLView + threeJS,认为交互的问题,或者是GLB文件的特性

位移动画为什么还算流畅?

模型位移时:

  • 只是更新 position
  • 变换矩阵相对简单
  • Three.js 内部状态变化较少
  • JS 每一帧传递的数据也很有限

低帧率 + 匀速运动 的情况下,人眼会帮我们"脑补连续性",

于是看起来还算顺。

scale 动画为什么这么容易卡?

scale 动画在 Three / GLView 这一套里,成本要高得多:

  • 每一帧都要重新计算变换矩阵
  • scale 会影响子节点、包围盒、裁剪判断
  • 渲染管线状态变化更频繁
  • JS → GL 的同步压力明显变大

这些变化都只能由 JS 在每一帧明确告诉底层怎么去渲染。只要 JS 线程被 RN 抢走一瞬间:手势 ,setState,Layout,Bridge调度在这一帧就丢失了。

因此这并不是交互本身的问题,也不是 GLB 模型复杂度导致的性能瓶颈。

本质原因在于 在 RN + GLView 架构下,动画状态的更新与帧的推进完全依赖 JS 线程驱动

一旦 JS 线程被其他任务占用,渲染帧就无法按时推进,画面便会直接卡顿。

为什么 Web 中的 scale 看起来这么轻松?

回答问题前我们先分别看一下web上的位移动画和Scale动画效果

差距有点大......都是 GL+threeJS的背景,那为什么造成了这个原因呢?

浏览器的优化

上面我们看到,在 RN 的跨端框架中:

渲染是否发生、何时发生、以及下一帧画什么,几乎完全依赖 JS 线程逐帧驱动。

而在 Web 环境中,即使我们使用的是 WebGL + Three.js渲染帧的推进并不完全依赖 JS 线程。 换句话说:浏览器并不是"每一帧都等 JS 算完再画",而是"只在必要时才叫 JS 介入"。

当一个动画被启动后:

  • Three.js 将物体的变换参数(position / scale / rotation)
  • 以及对应的矩阵更新逻辑
  • 提交给浏览器的渲染管线

只要动画逻辑本身是"连续的" (例如匀速位移、缩放、插值动画),

帧调度与合成始终由浏览器主导,JS 只是参与者,而不是驱动者。

  • JS 线程只负责 初始状态与关键参数变更
  • 帧调度、vsync 对齐、GPU 提交由浏览器统一完成

因此即便 JS 出现短暂阻塞,画面也不会立刻停住

RAF MDN地址 Rendering pipeline 这里面可以看到web中的RAF是由浏览器去调度的,即使 JS 线程短暂阻塞,浏览器仍可能复用上一帧的状态让 GPU 进行合成。

结尾

在 Web 环境中,即便动画是匀速移动或连续旋转,GPU 并不需要每一帧都等 JS 计算完成才能渲染下一帧。只要 JS 在动画开始时提供了初始状态、速度、旋转轴等信息,浏览器的渲染管线就能利用这些已有状态持续推进动画。换句话说:

  • Web / Three.js + WebGL
    JS 负责一次性设置参数 → GPU 自行渲染 → 即使 JS 暂时阻塞,动画也不会瞬间停住,而是继续使用已有状态进行渲染。就像导演指挥好演员动作后,摄影机自己拍摄电影。
  • RN / Expo GLView + Three.js
    GPU 并没有独立推进动画的能力,每一帧都需要 JS 告诉它物体的新位置和旋转。JS 线程一旦阻塞或计算过重,画面就会卡住或掉帧。像导演必须每一帧都手把手指挥演员,任何停顿都会影响拍摄。

核心差异:Web 的 GPU 可以"复用上一帧状态继续播放",而 RN 的 GPU 渲染完全依赖 JS 线程逐帧驱动。

这一点也解释了为什么同样的 GLB 模型、同样的 Three.js 代码,在浏览器里顺滑,但在 RN 里高 FPS 动画很容易卡顿。

相关推荐
佛系打工仔7 分钟前
绘制K线第三章:拖拽功能实现
android·前端·ios
cauyyl9 分钟前
react 项目检查国际化配置脚本
前端·react.js·前端框架
康一夏13 分钟前
React面试题,useRef和普通变量的区别
前端·javascript·react.js
前端 贾公子14 分钟前
Monorepo + Turbo (6)
前端
wayne21418 分钟前
ReactNative性能优化实战指南(2026最新版)
react native
冴羽34 分钟前
2025 年 HTML 年度调查报告公布!好多不知道!
前端·javascript·html
Apifox44 分钟前
Apifox CLI + Claude Skills:将接口自动化测试融入研发工作流
前端·后端·测试
wszy18091 小时前
rn_for_openharmony_空状态与加载状态:别让用户对着白屏发呆
android·javascript·react native·react.js·harmonyos
程序员Agions1 小时前
别再只会 console.log 了!这 15 个 Console 调试技巧,让你的 Debug 效率翻倍
前端·javascript