前端内存分析、优化、检测泄露
前端内存分析、优化、检测泄露
在前端开发中,内存管理是一个非常重要但又容易被忽视的领域。随着应用功能的不断增加和数据量的持续增长,内存问题可能会导致应用性能下降甚至崩溃。本文将从内存检测、分析、泄露原因以及优化方法等多个维度,帮助开发者更好地理解和解决前端内存问题。
用户端检测
日志记录
在应用程序中添加详细的日志记录,特别是关于内存使用和释放的信息。通过日志可以观察内存占用是否持续增加而不减少,这可能是内存泄漏的迹象。
异常/性能监控
通过浏览器的性能监控工具,可以实时监测应用的内存使用情况,及时发现异常。
用户反馈现象
内存占用濒临极限:卡顿
当应用的内存占用接近系统分配的上限时,浏览器会变得非常卡顿,响应速度大幅下降。
内存占用溢:崩溃
如果应用的内存占用超过系统分配的上限,浏览器可能会直接崩溃,导致用户无法正常使用应用。
场景
A.配置低,经常出现页面崩溃
在配置较低的设备上,如果页面内存占用太大,打开几个页面后,内存可能会直接拉满,导致页面崩溃。
B.大数据渲染
左侧是一个 Tree 树形控件,该控件一次性加载了三千条数据。难以置信,该页面的内存竟然到了 113M,而改为懒加载子节点后,该页面的内存直接降到了 15M,内存的前后差异是惊人的。
Memory :内存快照
- 打开 chrome 浏览器控制台,选择 Memory 工具
- 点击左侧 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 相同
内存分析:内存最高的点
- 从柱状图中找到最高的点,重点分析该时间内造成内存变大的原因
- 按照 Retainers size(总内存大小)排序,点击展开内存最高的哪一项,点击展开构造函数,可以看到所有构造函数相关的对象实例
- 选中构造函数,底部会显示对应源码文件,点击源码文件,可以跳转到具体的代码,这样我们就知道是哪里的代码造成内存过大
- 结合具体的代码逻辑,来判断这块内存变大的原因,重点是如何去优化它们,降低内存的使用大小
点击 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月的技术环境,部分工具和方法可能已经更新,建议结合最新版本的开发工具和环境进行实践。