← ALL NOTES
METHOD·2026.08·5 MIN READ
3D 场景中 HTML 浮层的性能优化
3d-visualization3d-design
问题

3D 场景里经常会叠加跟随模型移动的 HTML 浮层,例如标注、笔记、提示框。这些元素看起来只是普通的 DOM,但如果它们的位置和可见性判断都放进每一帧里处理,相机一动就可能明显拖慢画面。

我之前做过一个笔记功能 demo。场景里只有 7 条笔记,每条由一个锚点、一条引线和一段文字组成。功能完成后简单自测并没有觉得特别异常,但真人连续旋转相机时,第一反馈就是"非常卡顿"。

这次排查的重点不是先怀疑模型,而是看相机运动期间每一帧到底做了什么。

两个主要开销

排查下来,真正持续发生在每一帧里的高成本操作有两类。

第一类:React 每帧重新渲染浮层。 原来的实现会在每一帧计算完位置后调用一次 setOverlay(list),让 React 更新 7 个浮层组件。每个组件里还有文字描边、图标阴影等 CSS 效果,因此相机持续旋转时,React 重渲染和浏览器重绘也会持续发生。

问题不在浮层数量本身,而在于位置变化这种高频状态,被放进了 React 的渲染链路。

第二类:每条笔记都对整个模型做射线遮挡检测。 原来的遮挡判断使用 raycaster.intersectObjects(modelMeshes)。模型本身是一个压缩后仍有约 11MB 的网格,也没有建立 BVH 一类的加速结构。单条射线的成本会随着几何复杂度上升;7 条笔记乘上持续帧率,这部分开销会在相机运动时反复出现。

这两个问题叠在一起,主线程就会持续被占用。

最终方案

最终没有继续在原来的每帧流程里"做得更省",而是把两类工作分别换成更适合这个场景的路径。

1. 浮层位置更新移出 React。 React 只负责真正的内容和状态变化,例如笔记增删改、选中状态变化、进入或退出移动状态。相机运动时的每帧定位,则直接更新 DOM:

JAVASCRIPT
el.style.transform = `translate(${x}px, ${y}px) translate(-50%, -100%)`

这里用 transform,而不是 left / top。这样位置变化可以尽量走浏览器的合成路径,不需要每帧把浮层重新送回 React 渲染。对这个功能来说,React 负责"浮层是什么",每帧逻辑只负责"浮层现在在哪里"。两者分开以后,相机旋转时不再产生连续的 React 重渲染。

2. 精确遮挡改成朝向剔除。 第二个调整是降低遮挡判断的精度要求。原来的目标是精确判断"这条笔记是否被模型挡住",因此用了射线和模型求交。实际再看这个场景,笔记是贴在模型表面上的,很多情况下只需要知道它所在表面是否背向相机。

因此改成基于表面法线和相机方向做朝向判断:表面背对相机时隐藏笔记,朝向相机时显示。这不是严格的几何遮挡,但对当前功能已经足够,而且成本远低于每帧对复杂模型做多次射线求交。如果以后场景变得更复杂,确实需要精确遮挡,再考虑加入 BVH 等加速结构。

为什么第一轮优化没有解决问题

这次优化其实做了两轮。

第一轮加的是"脏标记门控":只有相机移动、笔记变化等情况下才重新计算;画面完全静止时,整帧可以跳过。这个调整确实减少了静止状态下的空转,但真人测试后依然觉得卡。

原因是用户真正感受到性能问题的场景,本来就是持续旋转相机。在这个过程中,脏标记一直处于激活状态,原来那些昂贵的每帧工作仍然会持续发生。

所以"减少无效帧"和"降低有效帧里的单帧成本"是两个不同的问题。前者可以解决静止时浪费资源,后者才决定连续交互时是否顺滑。

验证方式

这次也让我重新区分了两种测试方式。

自己开发或设计完以后,常见的自测是点一下、转一下、确认功能有没有反应。这种测试足够发现功能错误,但不一定能发现性能问题。

性能验证需要刻意维持操作:持续旋转相机、连续缩放、反复拖动,让每帧逻辑真正跑一段时间。

第一轮优化在简单试用时看起来已经"有改善",但放到连续旋转这个真实使用方式里,问题仍然存在。最终是否优化成功,不能只看代码结构是不是更合理,也要看用户真正会怎么操作。

经验总结
  1. 先检查高频状态是否被放进 React 渲染链路。 3D 场景中的跟随型 DOM,内容变化和每帧位置变化通常不需要走同一条更新路径。
  2. 每帧几何计算先确认精度是否真的必要。 如果近似判断已经满足交互需求,就不必默认使用完整的射线求交。
  3. 减少空转不等于降低交互成本。 脏标记可以优化静止状态,但连续操作时的单帧开销仍然需要单独处理。
  4. 性能测试要模拟持续操作。 旋转、缩放、拖动这类问题,需要连续运行一段时间才能暴露真实瓶颈。
关于性能优化

这次技术诊断和具体修法主要来自 Claude Code,我负责确认问题场景、验证修改结果,以及判断优化是否真的改善了用户体验。

对我来说更重要的一点,是性能问题不能只按"代码更优化了没有"来判断。第一轮方案在技术上确实减少了无效计算,但没有解决用户最在意的持续旋转卡顿。真正有效的优化,必须回到那个具体操作里验证。

CONTINUE READING
从 Figma 组件属性到代码 Props2026.09Figma 设计系统的工程化落地2026.09