Better

Ethan的博客,欢迎访问交流

场景卡顿问题优化

随着应用的不断迭代,场景的渲染开始越来越卡,数据量稍微大一点就卡顿明显,十分影响用户体验,记录一次场景性能优化过程,渲染时间从 200ms 下降到 20ms,效果明显。

问题描述

从产品给的反馈,将当前卡顿问题分类为如下三种情形

  • 首次渲染时:首次加载方案并渲染时
  • 正常渲染时:方案渲染完成后,缩放或平移时卡顿
  • 交互渲染时:主要表现在框选对象时
  • 数据更新时:框选大面积物体,进行批量更新时

本次重点关注首次渲染和正常渲染卡顿问题,交互渲染卡顿优先级比数据更高,数据更新卡顿问题的 ROI 较低,优先级最低。

问题定位

测试案例单层约有 2700 个车位,16 个塔楼,通过 chrome 开发者 Performance 工具进行 profiling 分析。

首次渲染

从 profiling 的结果来看

  • Plan 421ms:其中 cleanup 42ms, Slot 245ms, Column 55ms, loadPlan 40ms, event setup 20ms
  • layerVisible 12ms

渲染方案耗时总结

  • cleanup 也比较耗时,可以做成一个空闲时任务
  • event 和 visible 设置耗时主要花在对象查找上,看是否可以优化
  • loadPlan 主要是索引重建和 slot geometry 计算
  • Slots 解析过程主要是三维对象的创建,后面详细分析

关于对象查找的优化,发现对象查找耗时竟然这么久,一开始觉得很困惑,为啥会这么慢,比如查找某个 group

1280X1280.PNG

原因是 three.js scene 对象查找走的深度优先遍历,加上 NextFloorPlanGroup 在比较靠后的位置,所以特别慢,于是手动新增一个广度优先的遍历方式,优化后 event setup+visible control 总体耗时变化 32ms->2ms。

再分析一下 loadPlan 的耗时,索引重建感觉优化空间不大了,倒是 slot 需要进行变换以计算最终坐标,原变换过程是对每个点进行旋转+平移两步操作,于是尝试使用将旋转+平移合并成一个矩阵变换,优化后耗时变化 40ms->22ms。

最后重点分析一下 slots 创建耗时(column 本质上是 slot 的简化版,跳过)

  • 完整 slots 178ms
  • 移除 mesh 143ms
  • 移除 edges 107ms
  • 移除 polygon compute 146ms

本以为是车位轮廓坐标计算导致,但测试后发现影响并不大,换成模版+变换的形式,耗时也差不多。

关于解析 slots 中,除了关闭 edges 创建渲染,其他步骤提速均不明显,仔细查看 edges 步骤感觉也无从下手了。似乎创建这么多个 polygon 就是要这么长时间,尝试创建 polygon 时复用一个公共 Material,以避免创建那么多对象,结果耗时几乎没差别。

只是发现创建 slots 的过程中,会频繁触发 GC,每次 GC 都耗时 1-2ms,有待研究。

7c734bac-6ff6-4c00-bfb8-8948e891e1ae.png

正常渲染时

具体性能指标如下:

c96d701c-755d-4299-8682-2b02766bf795.png

观察缩放平移时,会不断进行对象查找,耗时在 10-30ms 不等,但其实此时计算是没有必要的,可以优化一下。

8300387c-1ff4-41ee-bc49-502e06180b73.png

具体 render 耗时统计

截屏2024-11-15 16.53.31.png

当三个 render 同时使用时,其实 scene/camera 的矩阵没必要重复更新

  • 查看源码发现其内部有做性能优化,会通过 matrixWorldNeedsUpdate 拦截掉重复的更新
  • 那为什么还会慢呢,是数量太大循环导致的?还是 updateMatrix 导致的呢?答案是矩阵更新导致
  • 关闭 matrixAutoUpdate 变得巨快,发现场景渲染为空,原因是 camera 的矩阵更新需要开启

如果选择直接关闭 matrix 自动更新,则 CSS3DRenderer 和 CSS2DRenderer 可以保持不变,否则需要想办法关闭 CSS3DRenderer/CSS2DRenderer 关于 scene/camera 的矩阵更新,提升会很大,目前并没有直接的 API 供使用。在 github 提针对 CSS3DRenderer/CSS2DRenderer 关闭更新特性的 PR 被拒,告知可以选择关闭更新,然后自行在 render 函数中手动更新。

矩阵更新性能测试 gl.render 执行时间

  • 关闭matrixAutoUpdate 70ms-100ms
  • 关闭matrixWorldAutoUpdate 60ms-80ms
  • 全部关闭 50ms-80ms

数据不一定准确,体验下来就是关闭 matrixWorldAutoUpdate 比 matrixAutoUpdate 提速更明显一点。但全部关闭后 gl.render 大概能稳定在 60ms 左右,相比最初的 134ms,已经提升一倍多。

同时观察到一个现象,如果仅 render source 速度是飞快的,一旦加入 plan 后性能下降明显,因此统计一下是 plan 哪个数据导致性能急剧下降。

为了让感受更直观,保持 matrixAutoUpdate 默认开启状态下测试。

截屏2024-11-15 17.01.32.png

Note:其中设置 visible 为 false 逻辑中,updateWorldMatrix 时间不会减少,但 projectObject 和 renderScene 的时间会显著减少

做完如下优化后

  • 镜头交互过程中不做对象选取
  • 关闭 css2d/css3d renderer 的 matrix update
  • 优化 polygon:尽可能使用 merge、simple line,同时全透明 mesh 设置 visible 为 false

此时性能指标如下,此时每次渲染时间稳定在了 40-60ms 之间(214ms->50ms),但观察发现超过 50ms 时 task 会被 chrome 标记为长任务,依旧存在部分长任务。

有没有办法可以继续优化,使得整体渲染稳定维持在 30ms 左右呢。

  • 避免无效步骤:从 profile 结果来看,已经都是必须步骤了,除非彻底关闭 matrix auto update,但这会导致日常开发需要很小心,但确定不会修改的静态对象,可以选择手动关闭 matrix 更新
  • 减少渲染对象
    • 产品侧移除某些效果
    • 技术侧尽可能 merge/instance 等方式合并/复用对象,但会存在两个问题,第一目前产品的编辑对象均可自由编辑,复用难度大,且大规模的合并/复用,需要重构现有的选取逻辑,工作量大,影响面广
  • 减少数据量:如减少顶点数
  • 提升关键部分计算性能

走到这一步时,突然想起用一下 scene.overrideMaterial 属性,发现前后性能没有差别,可见性能瓶颈主要还是卡在 CPU 上。

对象选取时

具体性能指标-包含选择

becccb01-252f-453c-9ff1-1559a49fb215.png

具体性能指标-交叉选择

a25b1882-05e4-47ba-b410-c186fbd125ca.png

以上观察到当选择量增多时,box select 性能急剧下降。详细审查后发现,最大的瓶颈不在于计算框选对象,而是样式修改。

7e7047f1-6109-4981-9440-575686745f60.png

由于当前对于 Polygon 线条的样式修改,采用简单粗暴的方式,直接新建新的 edges 的方式,成本很高,于是改为修改 materials 的方式,修改后结果如下。

bd6e3c99-70cf-4de9-95e7-776d172f0db2.png

hover 和 select 性能均有很大提升,但为啥 select 比 hover 耗时长这么多呢。原因是因为

  • 当确定对象选择时,平移/旋转/复制控件部分逻辑会被调用,主要做一些准备工作。选择将其优化成真正开始启用功能时再进行准备工作,直接减少 60ms 耗时。
  • 右侧渲染当前选中物体的信息面板

98a51992-2463-464d-9f99-a8b0c1d842a5.png

右侧具体耗时花费在 Slot 组件上,主要原因是从框选的车位 id 集合中,再去 floorPlans.slots 中找到完整数据的查找过程耗时,因为是 O(n^2) 的复杂度,转成 O(n) + memo 后,32ms->1.5ms,柱子同样优化后 5ms->0.5ms。

c909537a-c726-4743-936a-77d78b05df43.png

任务分片:框选相对比较耗时,但更严重的是任务的拥挤,光标移动过程中,会不断触发查找,会出现上一个还没处理完下一个就来的情况,从而导致触发越多越来越卡。任务分片的好处是过期的未完成的任务可以丢弃,且通常不会卡住主线程,缺点就是响应变成异步了,不是所见立即得。

总结

日后开发注意事项

  • 一切以 profile 结果为主,不要去猜,重点关注非必要步骤、重复步骤、警惕 O(n^2) 的复杂度。
  • 警惕对象查找的耗时,根据情况选择广度遍历还是深度遍历。
  • 场景渲染优化的关键还是减少 draw calls 次数,大量小的对象是造成性能下降的元凶。
  • 经过反复测试后,FatPolyline3D 性能不如 Polyline3D,如非必要,选 Polyline3D。
  • 某些特殊情况,需要设置全透明对象,此时可以将 visible 设置为 false,此时并不影响选取,但能提升性能。
  • 全局关闭 matrix auto update,但建议仅作为保留手段,除非你通过 profile 后发现花费了大量时间,且其他优化手段都用了之后,因为这会带来开发的不便利。但是局部手动关闭还是可考虑的,如静态对象,以及当父元素才是可操作单元,此时子元素关闭 matrixAutoUpdate 是安全的,注意不包括matrixWorldAutoUpdate。
  • 任务分片,避免卡住主线程

Next

  • 如果 slot 和 column 可以明确几种规格,而不是用户自由发挥,此时可考虑使用 instanced mesh。
  • 对框选使用空间索引 quadtree

Event

部署到线上 dev 时,发现线上比本地要慢(50ms ->110ms),但同事电脑是好的。

很是费解,尝试将 chrome 版本升级后(v121->v123),线上直接起飞,甚至比之前的本地还要快 50ms->20ms。



留言