问小白 wenxiaobai
资讯
历史
科技
环境与自然
成长
游戏
财经
文学与艺术
美食
健康
家居
文化
情感
汽车
三农
军事
旅行
运动
教育
生活
星座命理

前端内存分析、优化、检测泄露

创作时间:
作者:
@小白创作中心

前端内存分析、优化、检测泄露

引用
CSDN
1.
https://blog.csdn.net/qq_28838891/article/details/133488656

在前端开发中,内存管理是一个非常重要但又容易被忽视的领域。随着应用功能的不断增加和数据量的持续增长,内存问题可能会导致应用性能下降甚至崩溃。本文将从内存检测、分析、泄露原因以及优化方法等多个维度,帮助开发者更好地理解和解决前端内存问题。

用户端检测

日志记录

在应用程序中添加详细的日志记录,特别是关于内存使用和释放的信息。通过日志可以观察内存占用是否持续增加而不减少,这可能是内存泄漏的迹象。

异常/性能监控

通过浏览器的性能监控工具,可以实时监测应用的内存使用情况,及时发现异常。

用户反馈现象

内存占用濒临极限:卡顿

当应用的内存占用接近系统分配的上限时,浏览器会变得非常卡顿,响应速度大幅下降。

内存占用溢:崩溃

如果应用的内存占用超过系统分配的上限,浏览器可能会直接崩溃,导致用户无法正常使用应用。

场景

A.配置低,经常出现页面崩溃

在配置较低的设备上,如果页面内存占用太大,打开几个页面后,内存可能会直接拉满,导致页面崩溃。

B.大数据渲染

左侧是一个 Tree 树形控件,该控件一次性加载了三千条数据。难以置信,该页面的内存竟然到了 113M,而改为懒加载子节点后,该页面的内存直接降到了 15M,内存的前后差异是惊人的。

Memory :内存快照

  1. 打开 chrome 浏览器控制台,选择 Memory 工具
  2. 点击左侧 start按钮,刷新页面,开始录制的 JS堆动态分配时间线,会生成页面加载过程内存变化的柱状统计图(蓝色表示未回收,灰色表示已回收)

关键项

  • Constructor:对象的类名;
  • Distance:对象到根的引用层级;
  • Objects Count:对象的数量;
  • Shallow Size: 对象本身占用的内存,不包括引用的对象所占内存;
  • Retained Size: 对象所占总内存,包含引用的其他对象所占内存;
  • Retainers:对象的引用层级关系

// 测试代码
class Jane {}
class Tom {
  constructor() {
    this.jane = new Jane();
  }
}
let list = Array(1000000)
  .fill('')
  .map(() => new Tom());

shallow size 和 retained size 的区别,以用红框里的 Tom 和 Jane 更直观的展示

Tom 的 shallow 占了 32M,retained 占用了 56M,这是因为 retained 包括了引用的指针对应的内存大小,即 tom.jane 所占用的内存

所以 Tom 的 retained 总和比 shallow 多出来的 24M,正好跟 Jane 占用的 24M 相同

内存分析:内存最高的点

  1. 从柱状图中找到最高的点,重点分析该时间内造成内存变大的原因
  2. 按照 Retainers size(总内存大小)排序,点击展开内存最高的哪一项,点击展开构造函数,可以看到所有构造函数相关的对象实例
  3. 选中构造函数,底部会显示对应源码文件,点击源码文件,可以跳转到具体的代码,这样我们就知道是哪里的代码造成内存过大
  4. 结合具体的代码逻辑,来判断这块内存变大的原因,重点是如何去优化它们,降低内存的使用大小

点击 keyghost.js 可以跳转到具体的源码

内存泄露

意外的全局变量, 挂载到 window 上全局变量

遗忘的定时器,定时器没有清除

闭包

内存优化

减少组件DOM渲染(首要原因)

数据懒加载

组件懒加载

虚拟滚动

数据分页

window上的监听事件没有移除或移除错误

节流与防抖

// 版本一
mounted() {
    window.addEventListener('resize', debounce(this.fn, 100))
},
beforeDestroy() {
    window.removeEventListener('resize', debounce(this.fn, 100)) 
}

每次调用 debounce(this.fn, 100) 时, 其实都会返回一个新的函数,导致 addEventListener 和 removeEventListener 方法传入的回调函数已经不是同一个函数

// 版本二
data() {
    return {
        debounceFn: null
    }
},
mounted() {
    this.debounceFn = debounce(this.fn, 100)
    window.addEventListener('resize', this.debounceFn)
},
beforeDestroy() {
    window.removeEventListener('resize', this.debounceFn)  
}

console 导致的内存泄漏:引用

因为 list 数组被 console 所引用,导致 list 内存不能被释放

function fn () {
   let list = new Array(10 * 1024 * 1024).fill(1);  // 约占42M内存
   return function () {
      console.log(list)
   }
}
fn()()

闭包的错误使用:所引用的变量在函数外部

绝大多数情况,只要引用闭包的函数被正常销毁,闭包所占的内存都会被 gc 自动回收

特别是现在 SPA 项目的盛行,用户在切换页面时,老页面的组件会被框架自动清理,所以我们可以放心大胆的使用闭包,无需多虑

// 错误的写法: 闭包所引用的info变量在函数外部
let info = {
  arr: new Array(10 * 1024 * 1024).fill(1),
  timer: null
};
export const debounce = (fn, time) => {
  // 正确的写法: 闭包所引用的info变量在函数内部
  let info = {
    arr: new Array(10 * 1024 * 1024).fill(1),
    timer: null
  };
  return function (...args) {
    info.timer && clearTimeout(info.timer);
    info.timer = setTimeout(() => {
      fn.apply(this, args);
    }, time);
  };
};

绑在 EventBus 的事件没有解绑

mounted () {
 this.$EventBus.$on('homeTask', res => this.fn(res))
},
destroyed () {
 this.$EventBus.$off()
}

弱引用:weakset、weakmap

它们对值的引用都是不计入垃圾回收机制的,如果其他对象都不再引用该对象,那么gc 会自动回收该对象所占用的内存

注册监听事件的 listener 对象: WeakMap

由于监听函数是放在 WeakMap 里面,一旦 element 对象的其他引用消失,与它绑定的监听函数 handler 所占的内存也会被自动释放

// 代码1
element.addEventListener('click', handler, false)
// 代码2
weak.set(element, handler)
element.addEventListener('click', weak.get(element), false)

注意:本文内容基于2024年4月的技术环境,部分工具和方法可能已经更新,建议结合最新版本的开发工具和环境进行实践。

© 2023 北京元石科技有限公司 ◎ 京公网安备 11010802042949号