一文带你了解如何排查内存泄漏导致的页面卡顿现象


不知道在座的各位有没有被问到过这样一个问题:如果页面卡顿,你觉得可能是什么原因造成的?有什么办法锁定原因并解决吗?
这是一个非常宽泛而又有深度的问题,他涉及到很多的页面性能优化问题,我依稀还记得当初面试被问到这个问题时我是这么回答的:
先会检查是否是网络请求太多,导致数据返回较慢,可以适当做一些缓存 也有可能是某块资源的 bundle 太大,可以考虑拆分一下 然后排查一下 js 代码,是不是某处有过多循环导致占用主线程时间过长 浏览器某帧渲染的东西太多,导致的卡顿 在页面渲染过程中,可能有很多重复的重排重绘 emmmmmm....不知道了
后来了解到了,感官上的长时间运行页面卡顿也有可能是因为内存泄漏引起的
- 1 -
内存泄漏的定义
那什么是内存泄漏呢?借助别的大佬给出的定义,内存泄漏就是指由于疏忽或者程序的某些错误造成未能释放已经不再使用的内存的情况。简单来讲就是假设某个变量占用 100M 的内存,而你又用不到这个变量,但是这个变量没有被手动的回收或自动回收,即仍然占用 100M 的内存空间,这就是一种内存的浪费,即内存泄漏
- 2 -
JS 的数据存储
JavaScript 的内存空间分为栈内存和堆内存,前者用来存放一些简单变量,后者用来存放复杂对象
简单变量指的是 JS 的基本数据类型,例如:String、Number、Boolean、null、undefined、Symbol、BigInt 复杂对象指的是 JS 的引用数据类型,例如:Object、Array、Function...
- 3 -
JS 垃圾回收机制
function fn1 () {
let a = {
name: '零一'
}
let b = 3
function fn2() {
let c = [1, 2, 3]
}
fn2()
return a
}
let res = fn1()

图中左侧为栈空间,用于存放一些执行上下文和基本类型数据;右侧为堆空间,用于存放一些复杂对象数据

待 fn1函数内部执行完毕以后,就该退出 fn1函数执行上下文了,即箭头再向下移动,此时 fn1函数执行上下文会被清除并释放相应的栈内存空间,如图所示:

此时处于全局的执行上下文中。JavaScript 的垃圾回收器会每隔一段时间遍历调用栈,假设此时触发了垃圾回收机制,当遍历调用栈时发现变量 b 和变量 c 没有被任何变量所引用,所以认定它们是垃圾数据并给它们打上标记。因为fn1函数执行完后将变量 a 返回了出去,并存储在全局变量 res 中,所以认定其为活动数据并打上相应标记。待空闲时刻就会将标记上垃圾数据的变量给全部清除掉,释放相应的内存,如图所示:

JavaScript 的垃圾回收机制是自动执行的,并且会通过标记来识别并清除垃圾数据 在离开局部作用域后,若该作用域内的变量没有被外部作用域所引用,则在后续会被清除
补充:JavaScript 的垃圾回收机制有着很多的步骤,上述只讲到了标记-清除,其实还有其它的过程,这里简单介绍一下就不展开讨论了。例如:标记-整理,在清空部分垃圾数据后释放了一定的内存空间后会可能会留下大面积的不连续内存片段,导致后续可能无法为某些对象分配连续内存,此时需要整理一下内存空间;交替执行,因为 JavaScript 是运行在主线程上的,所以执行垃圾回收机制时会暂停 js 的运行,若垃圾回收执行时间过长,则会给用户带来明显的卡顿现象,所以垃圾回收机制会被分成一个个的小任务,穿插在js任务之中,即交替执行,尽可能得保证不会带来明显的卡顿感
- 4 -
Chrome devTools 查看内存情况
在了解一些常见的内存泄漏的场景之前,先简单介绍一下如何使用 Chrome 的开发者工具来查看js内存情况
首先打开 Chrome 的无痕模式,这样做的目的是为了屏蔽掉 Chrome 插件对我们之后测试内存占用情况的影响


简单录制一下百度页面,看看我们能获得什么,如下动图所示:




- 5 -
内存泄漏的场景
闭包使用不当引起内存泄漏 全局变量 分离的 DOM 节点 控制台的打印 遗忘的定时器
5.1 闭包使用不当
<button onclick='myClick()'>执行fn1函数</button><script> function fn1 () { let a = new Array(10000) // 这里设置了一个很大的数组对象
let b = 3
function fn2() { let c = [1, 2, 3] }
fn2()
return a }
let res = []
function myClick() { res.push(fn1()) }</script>



5.2 全局变量
function fn1() {
// 此处变量name未被声明
name = new Array(99999999)
}
fn1()
function fn1() { 'use strict'; name = new Array(99999999)}
fn1()
5.3 分离的 DOM 节点
<div id='root'>
<div class='child'>我是子元素</div>
<button>移除</button>
</div>
<script>
let btn = document.querySelector('button')
let child = document.querySelector('.child')
let root = document.querySelector('#root')
btn.addEventListener('click', function() {
root.removeChild(child)
})
</script>

<div id='root'> <div class='child'>我是子元素</div> <button>移除</button></div><script> let btn = document.querySelector('button')
btn.addEventListener('click', function() { let child = document.querySelector('.child') let root = document.querySelector('#root')
root.removeChild(child) })
</script>

5.4 控制台的打印
<button>按钮</button>
<script>
document.querySelector('button').addEventListener('click', function() {
let obj = new Array(1000000)
console.log(obj);
})
</script>

<button>按钮</button><script> document.querySelector('button').addEventListener('click', function() { let obj = new Array(1000000)
// console.log(obj); })</script>

未注释 console.log

注释掉了 console.log

// 如果在开发环境下,打印变量obj
if(isDev) {
console.log(obj)
}
5.5 遗忘的定时器
<button>开启定时器</button><script>
function fn1() { let largeObj = new Array(100000)
setInterval(() => { let myObj = largeObj }, 1000) }
document.querySelector('button').addEventListener('click', function() { fn1() })</script>


<button>开启定时器</button>
<script>
function fn1() {
let largeObj = new Array(100000)
let index = 0
let timer = setInterval(() => {
if(index === 3) clearInterval(timer);
let myObj = largeObj
index ++
}, 1000)
}
document.querySelector('button').addEventListener('click', function() {
fn1()
})
</script>
performance

memory

- 6 -
总结

这个挑战,是年轻人不可逃避的!观看视频内容
