近日,知名开源3D图形库 Three.js 被曝存在一个严重的内存泄漏问题:当开发者使用 Scene.background 属性设置场景背景时,相关纹理或颜色对象所占用的内存无法被正常释放,导致应用在长时间运行或频繁切换场景时内存持续飙升,最终可能引发浏览器崩溃或性能严重下降。
问题重现:一个简单的操作引发的内存灾难
据多位开发者在 GitHub 及技术社区反馈,该漏洞在 Three.js 的多个版本(包括最新的 r154 及之前的 r150、r152 等)中均存在。具体表现为:当开发者通过 scene.background = texture 或 scene.background = color 为场景设置背景后,即使后续将 scene.background 置为 null 或删除场景对象,V8 引擎的垃圾回收机制也无法回收这些背景资源。
一位来自游戏开发团队的工程师在技术博客中详细描述了重现步骤:创建一个仅包含一个立方体的场景,设置背景纹理,然后在每帧中创建新场景并销毁旧场景。仅仅运行数十秒后,浏览器 DevTools 中的内存快照就显示 WebGLTexture 和 Color 对象数量持续增加,且 never GC 标记始终存在。更令人担忧的是,当重复调用 renderer.dispose() 和 scene.clear() 后,内存依然无法回落。
技术根源:内部引用链的“隐形的锁”
经过社区贡献者和 Three.js 核心维护者的初步排查,该问题的根源指向了渲染器内部对背景资源的引用机制。当开发者通过 scene.background 赋值后,Three.js 渲染器(WebGLRenderer)在内部会将背景纹理/颜色对象注册到一个全局的_background 缓存或材质管理器列表中。即使开发者主动清除了场景引用,渲染器内部依然持有对该资源对象的强引用(例如在 WebGLBackground 类中),导致垃圾回收器无法将其标记为可回收。
有开发者通过 Chrome DevTools 的 Retainers 面板发现,泄漏的纹理对象被 WebGLRenderLists 或 WebGLState 所引用,而这些对象本身又被 WebGLRenderer 实例持有。只要渲染器实例存在(哪怕已不可见),泄漏的资产就会一直存在。如果应用采用单渲染器实例反复切换场景(这是大多数 Three.js 应用的常见模式),内存就会像“滚雪球”一样不断累积。
影响范围:从官网 Demo 到工业级应用
该漏洞的影响面相当广泛。Three.js 是目前 Web 端最主流的 3D 图形库,被广泛应用于在线游戏、产品展示、数据可视化、教育软件甚至医疗影像系统。凡是需要动态更换背景或频繁重建场景的应用,都可能受到内存泄漏的困扰。
例如,一个在线汽车配置器应用允许用户切换不同颜色的背景,每次切换都会创建一个新的 Color 对象并赋值给背景。如果不加以处理,浏览器内存可能在几分钟内从几十 MB 飙升至 1GB 以上。再如,一些使用 Three.js 构建的元宇宙或虚拟展厅产品,在用户漫游不同房间时会重建场景并设置新背景,长期运行后极易导致页面无响应。
社区反应:开发者“炸锅”与临时补丁
该问题在 Three.js 的 GitHub Issues 中迅速成为热点,目前已获得超过 200 个“+1”和大量讨论。不少开发者表示“困扰已久”,之前只是习惯性手动调用 texture.dispose() 或 renderer.dispose(),但发现效果有限。
一位 ID 为“mikkoh”的贡献者提出了一个临时解决方案:在创建渲染器实例后,手动将 renderer.background 属性赋值为 null,并调用 renderer.compile(scene, camera) 强制清除内部缓存。但这一方法在部分场景下仍然无效。另一位用户则发现,如果不使用 Scene.background,而是通过 renderer.setClearColor() 来设置背景色,则不会出现泄漏——但这样无法使用纹理背景,功能受限。
目前 Three.js 核心团队已确认复现该问题,作者“mrdoob”表示将在下一个版本(预计 r155 或 r156)中修复,同时鼓励开发者提交更详细的调试信息。在一份未公开的讨论中,维护团队表示可能需要对 WebGLBackground 类进行重构,引入显式的 dispose() 方法,并确保背景资源在场景切换时被正确移除引用。
临时应对措施
对于正在使用 Three.js 且深受内存泄漏困扰的开发者,社区总结了数种临场缓解方案:
- 使用 renderer.setClearColor 代替 Scene.background:当仅需要纯色背景时,此方法不涉及纹理对象,不会引发泄漏。
- 手动清理:在销毁场景前,显式调用
scene.background = null、texture.dispose(),并尝试调用renderer.forceContextLoss()重置 WebGL 上下文(代价较大)。 - 使用单场景 + 更换背景元素:避免频繁新建场景,而是在同一个场景中动态替换背景对象(如使用网格背景或自定义着色器)。
- 利用 Worker 隔离:对于极端情况,可将渲染器置于独立的 Web Worker 中,每次切换场景时终止 Worker 并新建,强制释放所有资源。
展望:开源生态的警示
此次内存泄漏事件再次提醒广大 Web 开发者:即使是成熟的开源库,也可能在看似简单的 API 后隐藏深层的内存管理问题。Three.js 社区发展迅速,但 WebGL 底层的复杂性使得内存审计难度较高。对于生产环境,开发者必须养成定期做内存快照的习惯,并警惕任何看似“无害”的赋值操作。
截至发稿时,Three.js 官方尚未发布正式修复版本。我们建议所有依赖 Three.js 且频繁切换背景的应用开发者密切关注 GitHub 仓库更新,并在必要时实施上述临时方案。后续我们将持续追踪该问题的修复进展,并第一时间带来详细报道。