跨端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 动画很容易卡顿。

相关推荐
咬人喵喵5 小时前
18 类年终总结核心 SVG 交互方案拆解
前端·css·编辑器·交互·svg
不想秃头的程序员5 小时前
JS继承方式详解
前端·面试
Mapmost5 小时前
【高斯泼溅】从“看清”到“看懂”,3DGS语义化让数字孪生“会说话”
前端
指尖跳动的光5 小时前
防止前端页面重复请求
前端·javascript
luquinn5 小时前
用canvas切图展示及标记在原图片中的位置
开发语言·前端·javascript
巧克力芋泥包5 小时前
前端vue3调取阿里的oss存储
前端
AAA阿giao5 小时前
React Hooks 详解:从 useState 到 useEffect,彻底掌握函数组件的“灵魂”
前端·javascript·react.js
RedHeartWWW5 小时前
Next.js Middleware 极简教程
前端
饼饼饼5 小时前
从 0 到 1:前端 CI/CD 实战 ( 第一篇: 云服务器环境搭建)
运维·前端·自动化运维