Three.js 对象释放(dispose):别让 WebGL 资源悄悄拖垮应用
原文出处:Three.js Manual – How to dispose of Objects
本文基于官方手册「How to dispose of Objects」章节整理,用通俗方式带你看懂:为什么场景里的物体删了内存却没降、怎么正确释放几何体/材质/贴图,以及
renderer.info怎么帮你抓泄漏。
写在前面
你有没有遇到过这种情况:一个 Three.js 应用跑久了越来越卡,切换场景后内存不降反升,DevTools 里 GPU 内存一路往上爬?
根因几乎都指向一件事——没有正确释放 WebGL 资源。在 Three.js 里,new一个几何体或材质时,引擎会在 GPU 侧偷偷分配缓冲区、着色器程序等「不归 JS 管」的资源,而这些不会因为对象没人引用就自动回收。这一篇就把「怎么正确 dispose」讲清。
一、为什么 Three.js 不帮你自动回收
这是社区被问爆的问题。结论是:Three.js不知道你创建的几何体、材质到底还用不用。举例来说,某个材质这一帧没被渲染,但下一帧可能又要上场——引擎没法替你判断「该不该删」。所以规则很明确:你的应用负责决定对象的生命周期,决定要删了,就显式调用对应的dispose()通知引擎。
划重点:把对象从场景里
scene.remove(mesh)移除,并不会连带释放它的geometry和material。该 dispose 的还是得 dispose。
二、Geometry:调用 BufferGeometry.dispose()
几何体本质上是顶点属性(attributes)的集合。Three.js 会为每个 attribute 在底层创建一个WebGLBuffer。这些缓冲区只有调用dispose()才会被删除:
geometry.dispose();当某个几何体在应用里不再需要时,立刻执行它,释放所有相关 GPU 资源。
三、Material:调用 Material.dispose()
材质定义了「物体怎么被渲染」,Three.js 会据此编译一个着色器程序(shader program)。这个程序只有对应的材质被 dispose 时才能删。
有个性能细节值得记住:Three.js 会尽量复用已有的着色器程序。所以一个 shader program 只有在「所有用到它的材质都 dispose 了」之后才会真正删除。释放材质:
material.dispose();四、Texture:调用 Texture.dispose()(与材质相互独立)
这里有个常见误解:dispose 一个材质,并不会释放它用到的贴图。贴图是单独管理的——因为一张贴图可能被多个材质同时引用。每创建一个Texture,底层就生成一个WebGLTexture,同样只有显式 dispose 才会删:
texture.dispose();⚠️ 两个额外陷阱:
- 如果你用
ImageBitmap当贴图数据源,还要在应用层手动调ImageBitmap.close()释放 CPU 侧资源。Texture.dispose()里不能自动帮你关,因为引擎不知道这张 bitmap 是否还被别处使用。- 在图片还没加载完时就调
texture.dispose(),什么都不会发生——因为底层资源要等图片完全加载后才分配,没分配自然也没什么可清理的。
五、Render Target:调用 WebGLRenderTarget.dispose()
WebGLRenderTarget不只分配WebGLTexture,还会分配WebGLFramebuffer和WebGLRenderbuffer(用来实现自定义渲染目标)。这些只有调用它的dispose()才会一起释放:
renderTarget.dispose();六、Skinned Mesh:别忘 Skeleton.dispose()
蒙皮网格(Skinned Mesh)用骨骼(Skeleton)来表达骨骼层级。不再需要某个蒙皮网格时,记得对它的 skeleton 调dispose()释放内部资源:
skinnedMesh.skeleton.dispose();注意:骨骼可以被多个蒙皮网格共享。只有确认它没有被其他还在用的网格引用时,才能调
dispose(),否则会误伤。
七、其他提供 dispose 的类
examples 目录里很多辅助类——比如控制器(controls)、后期处理通道(post-processing passes)——也提供dispose(),用来移除内部事件监听器或渲染目标。建议养成习惯:拿到一个新类,先查 API 有没有dispose(),有就在清理时调用。
八、实战清理套路:遍历场景一次性释放
手册本身没给完整代码,但这里给你一个能直接套用的清理范式:移除场景里所有 mesh 的几何体、材质及其贴图。
// 遍历场景,释放所有 mesh 的几何体和材质scene.traverse((object)=>{if(!object.isMesh)return;object.geometry?.dispose();constmaterials=Array.isArray(object.material)?object.material:[object.material];materials.forEach((material)=>disposeMaterial(material));});functiondisposeMaterial(material){material.dispose();// 顺手释放材质上挂的所有贴图for(constvalueofObject.values(material)){if(value&&value.isTexture){value.dispose();}}}什么时候该 dispose?没有统一标准,得看业务。一个典型例子:一个有多关卡的 3D 游戏,切关卡就是个好时机——遍历旧场景,把过时的材质、几何体、贴图一次性清掉。记住:误 dispose 一个还在用的对象通常不会报运行时错误,最坏情况只是当前帧有性能抖动(尤其 shader 要重新编译)。
九、用 renderer.info 抓内存泄漏
想确认有没有泄漏,别靠猜,看renderer.info。它是 renderer 上的一个属性,提供显卡内存和渲染过程的统计,能告诉你当前内部存了多少贴图、几何体、着色器程序:
console.log(renderer.info.memory);// { geometries: 12, textures: 8, ... }应用变卡时,盯住这个对象,很容易定位到「该释放却没释放」的泄漏点。
一个常见困惑:
renderer.info.memory在遍历场景 dispose 完所有贴图/几何体后,却仍然报告有几何体或贴图。这通常是因为 Three.js 内部还有一些你遍历场景图「够不到」的资源——比如用到的material.envMap、scene.background、scene.environment等,引擎会为内部用途自己建贴图/几何体。它们是被复用、不泄漏的,报告里留着属于正常现象。
十、FAQ 高频问答速览
- dispose 后还能再用那个对象吗?几何体、材质、贴图、渲染目标、后期通道——底层资源可以被引擎重建,不会报错,但可能当前帧掉帧(尤其 shader 重编译)。控制器和 renderer 是例外:
dispose()后就不能再用了,得新建实例。 - 为什么不让 Three.js 自动 dispose?引擎不知道你这些对象的生命周期和用途。
- 从场景移除 mesh 会连带 dispose 几何体和材质吗?不会,必须显式 dispose,而且几何体和材质可能被多个对象共享。
总结
| 资源类型 | 释放方法 | 备注 |
|---|---|---|
| 几何体 | geometry.dispose() | 释放底层 WebGLBuffer |
| 材质 | material.dispose() | shader program 在所有引用它的材质都释放后才删 |
| 贴图 | texture.dispose() | 与材质独立;ImageBitmap还要close() |
| 渲染目标 | renderTarget.dispose() | 同时释放 framebuffer / renderbuffer |
| 骨骼 | skeleton.dispose() | 注意是否被多网格共享 |
| 控制器/后期通道 | xxx.dispose() | 释放事件监听/渲染目标 |
延伸阅读
- three.js docs – BufferGeometry.dispose
- three.js docs – Material.dispose
- three.js docs – Texture.dispose
- three.js docs – WebGLRenderer.info
- three.js example – WebGL / test / memory