这是一个非常宽泛而又有深度的问题,他涉及到很多的页面性能优化问题,我依稀还记得当初面试被问到这个问题时我是这么回答的:
- 先会检查是否是网络请求太多,导致数据返回较慢,可以适当做一些缓存
- 也有可能是某块资源的bundle太大,可以考虑拆分一下
- 然后排查一下js代码,是不是某处有过多循环导致占用主线程时间过长
- 浏览器某帧渲染的东西太多,导致的卡顿
- 在页面渲染过程中,可能有很多重复的重排重绘
- emmmmmm....不知道了
后来了解到了,感官上的长时间运行页面卡顿也有可能是因为内存泄漏引起的
1、内存泄漏的定义
那什么是内存泄漏呢?借助别的大佬给出的定义,内存泄漏就是指由于疏忽或者程序的某些错误造成未能释放已经不再使用的内存的情况。简单来讲就是假设某个变量占用100M的内存,而你又用不到这个变量,但是这个变量没有被手动的回收或自动回收,即仍然占用100M的内存空间,这就是一种内存的浪费,即内存泄漏
2、JS的数据存储
JavaScript
的内存空间分为栈内存和堆内存,前者用来存放一些简单变量,后者用来存放复杂对象
- 简单变量指的是JS的基本数据类型,例如:
String
、Number
、Boolean
、null
、undefined
、Symbol
、BigInt
- 复杂对象指的是JS的引用数据类型,例如:
Object
、Array
、Function
...
3、JS垃圾回收机制
根据内存泄漏的定义,有些变量或数据不再被使用或不需要了,那么它就是垃圾变量或垃圾数据,如果其一直保存在内存中,最终可能会导致内存占用过多的情况。那么此时就需要对这些垃圾数据进行回收,这里引入了垃圾回收机制的概念
垃圾回收的机制分为手动和自动两种
例如C/C++
采用的就是手动回收的机制,即先用代码为某个变量分配一定的内存,然后在不需要了后,再用代码手动释放掉内存
而JavaScript
采用的则是自动回收的机制,即我们不需要关心何时为变量分配多大的内存,也不需要关心何时去释放内存,因为这一切都是自动的。但这不表示我们不需要关心内存的管理!!!!否则也不会有本文讨论的内存泄露了
接下来就讲一下JavaScript
的垃圾回收机制
通常全局状态(window)下的变量是不会被自动回收的,所以我们来讨论一下局部作用域下的内存回收情况
function fn1 () { let a = { name: '零一' } let b = 3 function fn2() { let c = [1, 2, 3] } fn2() return a } let res = fn1()
以上代码的调用栈如下图所示:
图中左侧为栈空间,用于存放一些执行上下文和基本类型数据;右侧为堆空间,用于存放一些复杂对象数据
当代码执行到fn2()
时,栈空间内的执行上下文从上往下依次是 fn2函数执行上下文 => fn1函数执行上下文 => 全局执行上下文
待fn2
函数内部执行完毕以后,就该退出fn2函数执行上下文
了,即箭头向下移动,此时fn2函数执行上下文
会被清除并释放栈内存空间,如图所示:、
待fn1
函数内部执行完毕以后,就该退出fn1函数执行上下文
了,即箭头再向下移动,此时fn1函数执行上下文
会被清除并释放相应的栈内存空间,如图所示:
此时处于全局的执行上下文中。JavaScript
的垃圾回收器会每隔一段时间遍历调用栈,假设此时触发了垃圾回收机制,当遍历调用栈时发现变量b
和变量c
没有被任何变量所引用,所以认定它们是垃圾数据并给它们打上标记。因为fn1
函数执行完后将变量a
返回了出去,并存储在全局变量res
中,所以认定其为活动数据并打上相应标记。待空闲时刻就会将标记上垃圾数据的变量给全部清除掉,释放相应的内存,如图所示:
从这我们得出几点结论:
JavaScript
的垃圾回收机制是自动执行的,并且会通过标记来识别并清除垃圾数据
- 在离开局部作用域后,若该作用域内的变量没有被外部作用域所引用,则在后续会被清除
补充:JavaScript
的垃圾回收机制有着很多的步骤,上述只讲到了标记-清除
,其实还有其它的过程,这里简单介绍一下就不展开讨论了。例如:标记-整理
,在清空部分垃圾数据后释放了一定的内存空间后会可能会留下大面积的不连续内存片段,导致后续可能无法为某些对象分配连续内存,此时需要整理一下内存空间;交替执行
,因为JavaScript
是运行在主线程上的,所以执行垃圾回收机制时会暂停js
的运行,若垃圾回收执行时间过长,则会给用户带来明显的卡顿现象,所以垃圾回收机制会被分成一个个的小任务,穿插在js
任务之中,即交替执行,尽可能得保证不会带来明显的卡顿感
4、Chrome devTools查看内存情况
在了解一些常见的内存泄漏的场景之前,先简单介绍一下如何使用Chrome
的开发者工具来查看js
内存情况
首先打开Chrome
的无痕模式,这样做的目的是为了屏蔽掉Chrome
插件对我们之后测试内存占用情况的影响
然后打开开发者工具
,找到Performance
这一栏,可以看到其内部带着一些功能按钮,例如:开始录制按钮;刷新页面按钮;清空记录按钮;记录并可视化js内存、节点、事件监听器按钮;触发垃圾回收机制按钮等等
简单录制一下百度页面,看看我们能获得什么,如下动图所示:
从上图中我们可以看到,在页面从零到加载完成这个过程中JS Heap(js堆内存)
、documents(文档)
、Nodes(DOM节点)
、Listeners(监听器)
、GPU memory(GPU内存)
的最低值、最高值以及随时间的走势曲线,这也是我们主要关注的点
再来看看开发者工具中的Memory
一栏,其主要是用于记录页面堆内存的具体情况以及js
堆内存随加载时间线动态的分配情况
堆快照就像照相机一样,能记录你当前页面的堆内存情况,每快照一次就会产生一条快照记录,如图所示:
如上图所示,刚开始执行了一次快照,记录了当时堆内存空间占用为13.9MB
,然后我们点击了页面中某些按钮,又执行一次快照,记录了当时堆内存空间占用为13.4MB
。并且点击对应的快照记录,能看到当时所有内存中的变量情况(结构、占总占用内存的百分比...)
然后我们还可以看一下页面动态的内存变化情况,如图所示:
在开始记录后,我们可以看到图中右上角有起伏的蓝色与灰色的柱形图,其中蓝色表示当前时间线下占用着的内存;灰色表示之前占用的内存空间已被清除释放。
从上图过程来看,我们可以看到刚开始处于的tab
所对应显示的页面中占用了一定的堆内存空间,成蓝色柱形,在点击别的tab
后,原tab
对应的内容消失,并且原来蓝色的柱形变成灰色(表示原占用的内存空间得到了释放),同时新tab
所对应显示的页面也占用了一定的堆内存空间。因此后续我们就可以针对这个图来查看内存的占用与清除情况