AIGC标识 Three.js 页面别只看 FPS:首屏、启动与资源释放的验收清单

我的个人网站里有作品介绍、工程笔记和三维交互演示。这样的页面同时服务两类访问者:有人只想读内容,有人会继续启动场景、切换模型、查看设备。

如果只盯着三维场景运行时的 FPS,很容易漏掉前两段等待:正文什么时候能读,点击启动后什么时候真正能操作。另一个常被漏掉的问题,是场景关闭以后,资源和后台任务有没有跟着结束。

本文基于个人网站的三维页面性能验收笔记继续展开,整理一套适合 Three.js 页面联调的检查方法。这是一份工程验收方案,不是本站实测报告;文中的代码与操作次数用于说明方法,不代表已上线实现或优化收益。

一、把“页面加载完成”拆成三个完成条件

1. 阅读首屏

用户不启动三维,也能看到标题、说明、导航和启动入口。

这一阶段重点检查:首屏是否被大体积脚本阻塞,封面是否稳定,文字是否可读。LCP 可以帮助观察主要内容的呈现,但不能直接代表 canvas 里的三维场景已经可用。

2. 首次启动

用户点击启动后,目标模型、必要材质、初始相机与约定交互准备就绪。

这里要先写清楚“可用”是什么意思。例如:

  • 模型出现在预期位置,不是空白画布。
  • 必要材质已经就绪,不是临时占位效果。
  • 可以旋转视角,并选中一个约定对象。
  • 加载失败时,能看到原因和明确的重试入口。

下载结束、loader 回调触发、进度条到 100%,都不能单独证明上述条件成立。

3. 持续使用与退出

用户反复进入、退出、切换对象、切到后台再回来,页面仍然正确。

这段要看帧时间和交互卡顿,也要看渲染循环、监听器、资源占用是否持续累积。一次流畅运行,只覆盖了最顺利的路径。

二、先录一条“不启动三维”的路径

在改代码之前,先做一个对照:打开页面,只读内容,不点击任何三维入口。

在浏览器 Network 和 Performance 中记录:

  1. 哪些脚本在首屏被请求、解析和执行。
  2. 是否已经下载模型、纹理、环境贴图和解码器。
  3. 主线程长任务是否来自当前可见内容。
  4. 三维尚未启动时,是否已经创建持续运行的循环。

组件不可见,不等于依赖没有下载;资源没有显示,也不等于没有初始化。

如果预期采用按需加载,再沿着模块引用链查原因。公共工具文件、统一导出文件、预览组件或静态 import,都可能把三维依赖带回首屏。

动态 import 只是划分加载边界的一种方式。最终仍要以构建产物和浏览器实际请求为准。预加载也不必一概删除:应记录它服务的用户路径、占用的流量,以及对启动等待的影响。

对照测试至少区分冷缓存与热缓存。同一构建、同一设备、同一操作步骤下比较,才容易判断变化来自哪里。

三、给首次启动增加业务计时

通用网页指标之外,可以用 User Timing 记录应用自己定义的启动区间。

下面是一个简化的计时辅助函数。它从函数被调用时开始,记录一次成功、失败或取消;它不负责加载场景,也不会替你判断场景是否可用。

function beginSceneStartup() {
  const name = `scene-startup:${crypto.randomUUID()}`;
  const start = performance.now();
  let finished = false;

  return function finish(status) {
    if (finished) return null;
    finished = true;

    const entry = performance.measure(name, {
      start,
      end: performance.now(),
      detail: { status }
    });

    const record = {
      status,
      durationMs: entry.duration
    };

    performance.clearMeasures(name);
    return record;
  };
}

接入时,在启动处理函数入口调用 beginSceneStartup(),保存它返回的 finish。满足事先约定的就绪条件后调用 finish("ready");失败和退出分别调用 finish("failed")、finish("cancelled"),把返回记录纳入本轮测试结果。

这个计时从处理函数开始,不包含用户点击后、处理函数执行前的排队延迟。如果要研究点击响应,应另外记录输入事件与主线程情况,不能把两者混在一起。

还要注意三件事:

  • finish("ready") 应由明确的业务就绪条件触发,不能随手放进下载完成回调。
  • 一次 render 调用返回,不能自动当作像素已经显示给用户的证据。需要结合画面检查和约定交互验证。
  • 超时或一直卡住的启动不能从统计里消失。验收脚本需要设置观察截止点,并单独记录超时、失败和取消。

不要只报告成功样本的平均值。保留各轮原始结果、失败数量与测试条件,必要时再汇总中位数和范围。测试次数较少时,不宜把一个分位数包装成稳定结论。

四、把暂停和销毁写成两条路径

后台暂停和关闭场景的目的不同。

暂停路径可以停止非必要渲染、轮询与动画更新,同时保留恢复需要的状态。返回前台后,恢复原有任务,避免再次叠加一个渲染循环。

销毁路径则要结束本场景的生命周期:

  • 停止它拥有的渲染循环、计时器和订阅。
  • 移除事件监听,清理控制器与相关后处理对象。
  • 从场景或容器中移除不再需要的对象。
  • 按所有权释放几何体、材质、纹理和 render target。
  • 处理尚未返回的异步加载结果。

Three.js 官方文档明确说明:从 scene 中移除 mesh,不会自动释放 geometry 和 material;调用 material.dispose() 也不会顺便释放它使用的 texture。

这里不建议直接写一个“遍历场景,见到 dispose 就调用”的通用清理函数。原因是一个纹理、几何体或材质可能同时被多个对象使用,也可能由跨页面缓存管理。

更稳妥的做法,是在创建时就明确所有者:

  • 场景独占资源,由场景结束时清理。
  • 多场景共享资源,由缓存或资源管理器管理使用关系。
  • renderer 若被其他视图复用,不能因为一个视图关闭就直接销毁。
  • 使用 ImageBitmap 作为纹理数据源时,还要在确认不再共享后处理其 CPU 侧生命周期。

清理策略应与创建策略成对设计,而不是在组件卸载时临时补上一串 dispose()。

五、专门测试“加载一半就退出”

首次加载最容易暴露异步竞态。

一种典型路径是:启动 A 场景,资源还没回来,用户关闭窗口并启动 B 场景。随后 A 的回调返回,把旧模型挂进新视图,或者重新启动已经结束的循环。

可以为每次场景会话设置独立标识。异步任务返回时,先确认自己仍属于当前有效会话;过期结果不再挂载,同时按资源所有权清理已创建对象。

这类“忽略过期结果”的逻辑,并不等于网络请求已经被取消。具体 loader、底层请求和版本是否支持中止,需要分别确认。

建议把下面几条放进联调脚本:

  1. 启动后立即关闭,再等待原加载任务返回。
  2. 连续点击启动,检查是否创建多个实例。
  3. A 未完成时切换到 B,检查最终显示是否仍然是 B。
  4. 资源加载失败后重试,检查监听器与循环是否重复注册。
  5. 关闭后重新进入,检查相机、选中状态和资源是否符合约定。

这些测试既关注画面,也关注后台工作。页面看起来关了,不代表旧任务真的结束了。

六、用重复操作观察趋势,不要求内存归零

可以先设计一条短而固定的测试路线,例如:

打开场景 → 旋转视角 → 选中对象 → 切换模型 → 关闭场景。

把这条路线连续执行 10 轮,记录每轮启动时间、交互异常和资源统计。这里的 10 轮只是起步样例,实际次数与持续时间应按使用场景调整。

Three.js 的 renderer.info 可以辅助查看几何体、纹理等计数与渲染统计,但它不是完整的浏览器内存报告,也不是精确的显存字节数。JavaScript 堆、GPU 资源与浏览器进程占用,需要分别理解。

清理后统计没有回到零,不一定代表泄漏。Three.js 可能保留内部可复用资源,应用也可能保留明确设计的缓存。更值得追踪的是:

  • 执行同一路径后,数量是否在预热后仍无界增长。
  • 关闭后是否仍有不该存在的更新、监听或请求。
  • 反复进入以后,启动时间和交互表现是否明显退化。
  • 清理后重新进入是否出现材质缺失、控制失效或状态串场。

如果发现持续增长,应回到资源归属和引用关系定位,而不是仅凭一张内存截图就宣布发生泄漏。

一份可以直接放进测试单的清单

首屏优化、启动优化和运行优化,可能改善不同的体验。把它们分别验收,才能看清一次改动究竟省掉了哪段等待,又把成本移到了哪里。

个人网站与参考资料

个人网站:https://www.singmiao.com/
网站中集中整理了我的项目职责、工程笔记与交互演示。

内容说明:本文由 AI 基于个人网站公开笔记与上述技术资料整理扩展,属于个人工程方法分享,不包含本站性能实测或任职单位的成果承诺,不代表任职单位的官方立场。

posted @ 2026-11-12 01:13  邢淼  阅读(0)  评论(0)    收藏  举报