网页三维同时加载多个 BIM、机械装配或工厂模型时,把 URL 交给 Promise.all() 往往只是把问题推迟到后面的解码和 GPU 上传阶段。真正需要控制的不是“同时发多少个请求”,而是整条加载链路的工作集。
一、先把加载拆成四个阶段
建议至少分别观测:
- I/O:网络或本地文件读取;
- CPU:解压、解析、三角化和属性整理;
- GPU:buffer、texture、pipeline 与 bind group 准备;
- Publish:把完整结果切换进场景,并建立结构树、拾取和业务映射。
每个阶段都记录排队数、活动数、平均耗时和取消数。这样才能知道瓶颈在带宽、Worker、主线程还是 GPU 队列,而不是看到 FPS 下降就机械地把所有并发调小。
二、用背压控制下游预算
如果解码阶段不断生产结果,而 GPU 上传已经饱和,继续增加请求只会制造更长的等待队列。可以同时约束解码任务数和预计上传字节数:
class LoadBudget {
constructor({ maxDecodeJobs, maxUploadBytes }) {
this.maxDecodeJobs = maxDecodeJobs;
this.maxUploadBytes = maxUploadBytes;
this.decodeJobs = 0;
this.uploadBytes = 0;
}
canDecode() {
return this.decodeJobs < this.maxDecodeJobs;
}
canUpload(bytes) {
return this.uploadBytes + bytes <= this.maxUploadBytes;
}
}
工程模型还应该按视角和业务优先级调度:当前可见块、用户刚选中的构件和结构树展开对象优先;屏幕外的远距离 LOD、隐藏专业和预加载任务延后。优化目标是让当前工作集尽快进入可交互状态,而不是把整个文件一次性塞进显存。
三、取消必须贯穿网络、Worker 和 GPU 上传
用户切换项目后,上一批任务已经没有业务价值。只取消 fetch 不够,Worker 可能仍在解码,主线程也可能已经排队创建 GPU 资源。
为每次查看会话分配 generation,所有异步结果进入公共缓存或正式场景前都检查当前代际:
const generation = ++viewerGeneration;
const decoded = await decodeInWorker(source, signal);
if (signal.aborted || generation !== viewerGeneration) {
decoded.dispose();
return;
}
await uploadWithBudget(decoded, generation);
旧代际结果不能回写当前场景;已经创建的 buffer、texture、ImageBitmap 和临时数组要按所有权完整释放。共享纹理或材质则用引用计数,避免误删仍在使用的资源。
四、GPU 上传也要分帧
即使解码放进 Worker,大批 writeBuffer、纹理上传、pipeline 创建和场景挂载仍可能集中造成长任务。可以按每帧时间预算或字节预算分批提交,并在批次之间让出事件循环。
验收时同时记录:
- 首个可见块和首个可交互视图的时间;
- 每帧上传字节、长任务和最差帧时间;
- 活动 Worker、等待上传的 CPU 数据与 GPU 资源数量;
- 连续切换模型后,旧会话队列是否归零、资源基线是否稳定。
如果只统计文件下载完成时间,会漏掉用户真正感受到的等待和卡顿。
五、别让调度破坏对象身份
为了减少 Draw Call 而合批、为了省显存而切换 LOD,都不能让结构树节点、属性、选择和协同状态失去对象映射。源对象 ID、查看缓存 ID、GPU 索引和业务资产 ID 应分层管理;加载调度改变的是几何表示和当前工作集,不应该改变业务对象身份。
我们做 ModelView 时,把首次源格式转换、可复用查看缓存、分块/LOD、WebGPU 调度和稳定对象身份放在同一条工程链路。说明一下,我是项目开发者。Windows 标准版可以用真实 BIM、机械 CAD 或工业装配模型验证多模型加载与连续切换:
参考:
- MDN AbortController:developer.mozilla.org/en-US/docs/…
- Three.js Loading 3D Models:threejs.org/manual/en/l…
- Three.js Cleanup:threejs.org/manual/en/c…