由频繁创建3D火焰造成的内存泄漏问题

起因

收到业务方反馈,之前做的一个3d火焰的页面,刚打开的时候,火焰展示正常,也有动画。但是在页面停留一会,火焰的动画就会明显感到卡顿,再等会页面的tab 就无法关闭了。直接卡死了。 凭个人经验来看,这应该是内存泄漏造成的问题,但具体原因是什么,还需要具体进行分析。

查找原因

打卡Chrome的Devtools, 找到Memory面板,可以看到一个Heap Snapshot。点击面板左上角红色圆点,对当前的heap 做一个堆快照, 然后等1分钟后,再点击这个红色圆点,进行一个堆快照。像这样多执行几次,可以看到有几个snapShot, 点击一个snapShot,可以看到 上面有个summary, 点击后有个下拉选项,可以选择Compare, 就是选择对照的snapshot. 如下图

可以看到,短时间内的两个snapShot, 一个叫system/JsArrayBufferData的 constructor, 新增了28983个,造成内存短时间内上升了329M, 这种现象,页面不卡才奇怪呢。下面的这个Array 和 ArrayBuffer, 也在短时间内增了很多。

那么这个system/JsArrayBufferData到底是什么?为什么会占用这么大的内存。 搜了一下,JSArrayBufferData内存溢出问题通常与未正确管理ArrayBuffer对象有关。在JavaScript中,ArrayBuffer是一种用于表示固定长度的原始二进制数据缓冲区的对象。如果未正确管理其生命周期,就可能导致内存泄漏,进而引发内存溢出问题。

原因还要从代码中找,是不是哪些数据造成了循环引用导致内存没有释放。

这个页面的功能大致是这样的, 1. 从后台获取一个数组,数组中的每个item包含了x,y,z的坐标。前端拿到这一组坐标后,把这些坐标都绘制成点。2. 然后业务方希望的效果是,沿着这些点创建一系列的火焰,并让火焰有动画效果。3. 这需要拿到点的 坐标后,在点的周边随机生成几个大小不一的火焰,然后让火焰动起来。

回到代码层面, 是怎么做的? 调用createFiresInLineArea方法,传入point,也就是坐标点传进去。createFire方法根据坐标点,在周边随机的创建几个火焰,然后使用fire的animate方法,使火焰动起来。 animate方法是这样的:

js 复制代码
 const animate = () => {
   if (fireMaterial.uniforms.uTime) {
     fireMaterial.uniforms.uTime.value += frequency;
   }
   requestAnimationFrame(animate);
}

注意了,就是这个代码造成了严重的内存泄漏。为什么这样说呢? 每个火焰,都要调用这个animate方法,而这个animate方法引用了外部的fireMaterial, 这会导致什么? fireMaterial是一个Threejs 的 new THREE.ShaderMaterial 的对象,这里面包含大量的ArrayBuffer, 闭包的使用,导致这个fireMaterial永远会是可达的,也就是说永远不会被GC。1个火焰的情况下,会有一些内存。但是要知道,火焰是根据point的数量来创建的。如果1000个火焰的时候,就是1000个requestAnimationFrame,这是造成内存急剧增长的主要原因,大量的内存没有被释放,造成了页面的卡顿。

解决

找到原因后,就从这里下手开始进行解决。 创建一个FireAnimate管理器,这里面生成一个fire的数据,每次创建火焰,都push 进数组中,也提供remove方法,可以把fire 进行remove。 最后提供一个animate方法。 也就是生成了大量的火焰点之后,再进行animate方法的调用,只调用一次requestAnimationFrame,大大降低了内存占用。

kotlin 复制代码
// 全局动画管理器
class FireAnimationManager {
  private static instance: FireAnimationManager
  private animationId: number = 0
  private fires: Set<THREE.Mesh> = new Set()
  private isRunning: boolean = false

  static getInstance(): FireAnimationManager {
    if (!FireAnimationManager.instance) {
      FireAnimationManager.instance = new FireAnimationManager()
    }
    return FireAnimationManager.instance
  }

  addFire(fire: THREE.Mesh) {
    this.fires.add(fire)
    if (!this.isRunning) {
      this.startAnimation()
    }
  }

  removeFire(fire: THREE.Mesh) {
    this.fires.delete(fire)
    if (this.fires.size === 0) {
      this.stopAnimation()
    }
  }

  private startAnimation() {
    if (this.isRunning) return
    this.isRunning = true
    
    const animate = () => {
      if (this.fires.size === 0) {
        this.stopAnimation()
        return
      }

      // 更新所有火焰的时间
      this.fires.forEach(fire => {
        const material = fire.material as THREE.ShaderMaterial
        if (material.uniforms?.uTime) {
          material.uniforms.uTime.value += fire.userData.frequency || 0.02
        }
      })

      this.animationId = requestAnimationFrame(animate)
    }
    animate()
  }

  private stopAnimation() {
    if (this.animationId) {
      cancelAnimationFrame(this.animationId)
      this.animationId = 0
    }
    this.isRunning = false
  }

  // 清理所有火焰
  cleanup() {
    this.stopAnimation()
    this.fires.clear()
  }
}

每次创建火焰, 调用这个方法 FireAnimationManager.getInstance().addFire(fire);

另外,由于这个火线是频繁变动的,导致每次都要重新清理火焰,再创建火焰,这个过程其实也是内存的释放与占用。可以使用对象池模式,每次复用已经存在的火焰,而不是重新创建火焰。

kotlin 复制代码
// 复用火焰对象而不是频繁创建销毁
class FirePool {
  private pool: THREE.Mesh[] = []
  
  getFire(): THREE.Mesh {
    return this.pool.pop() || this.createFire()
  }
  
  returnFire(fire: THREE.Mesh) {
    this.pool.push(fire)
  }
}

最后我发现代码中,还有一个监听事件,监听resize后, 调用了update方法。但是在页面卸载时并没有移除这个方法。需要在页面卸载时,移除这个监听时间,避免造成内存泄漏。

相关推荐
北'辰27 分钟前
DeepSeek智能考试系统智能体
前端·后端·架构·开源·github·deepseek
前端历劫之路1 小时前
🔥 1.30 分!我的 JS 库 Mettle.js 杀入全球性能榜,紧追 Vue
前端·javascript·vue.js
爱敲代码的小旗2 小时前
Webpack 5 高性能配置方案
前端·webpack·node.js
Murray的菜鸟笔记2 小时前
【Vue Router】路由模式、懒加载、守卫、权限、缓存
前端·vue router
阿彬爱学习2 小时前
大模型在垂直场景的创新应用:搜索、推荐、营销与客服新玩法
前端·javascript·easyui
橙序员小站3 小时前
通过trae开发你的第一个Chrome扩展插件
前端·javascript·后端
Lazy_zheng3 小时前
一文掌握:JavaScript 数组常用方法的手写实现
前端·javascript·面试
是晓晓吖3 小时前
关于Chrome Extension option的一些小事
前端·chrome
MrSkye3 小时前
🔥从菜鸟到高手:彻底搞懂 JavaScript 事件循环只需这一篇(下)
前端·javascript·面试