文章说明:本文章为拉钩大前端训练营所做笔记和心得,若有不当之处,还望各位指出与教导,谢谢 !
一、Performance
为什么使用Performance
- GC的目的是为了实现内存空间的良性循环
- 良性循环的基石是合理使用
- 时刻关注才能确定是否合理
- Performance提供多种监控方式
Performance 使用步骤
- 打开浏览器输入目标网址
- 打开开发人员工具面板,选择性能
- 开启录制功能,访问具体页面
- 执行用户行为,一段时间后录制,然后结束录制,点击performance按钮查看内存使用情况
二、内存问题的体现
内存问题的外在表现:
- 页面出现延迟加载或经常性暂停(网络环境正常)
- 页面持续性出现糟糕的性能
- 页面的性能随时间延长越来越差
三、监控内存的集中方式
界定内存问题的标准
- 内存泄漏:内存使用持续升高
- 内存膨胀:在多数设备上都存在性能问题(当前应用程序的本身为了达到最优的效果,它去需要很大的内存空间,在这个过程当中,也许是当前设备本身的硬件不支持,那么才造成了使用过程中性能上的差异)
- 频繁垃圾回收:通过内存变化图进行分析
监控内存的几种方式
- 浏览器任务管理器
- Timeline时序图记录
- 堆块照查找分离DOM
- 判断是否存在烦繁的垃圾回收
1.任务管理器监控内存
通过快捷键shift+esc调出浏览器任务管理器,右击选择当前页面的JavaScript内存就会显示了
第一列的内存占用空间是原生内存,简单理解就是当前界面的很多Dom节点,这个内存就是Dom占用的内存,如果该内存持续增大那就说明页面不断的再创建新Dom。
最后一列JavaScript使用的内存,里面表示的是js的堆,我们需要关注的是小括号里面的值,它表示的是所有可达对象正在使用的大小,如果说这个数字在增大,那么当前的界面中要么在创建新对象,要么就是当前现有对象在不断增长。
只能发现有没有问题,定位问题就不好用了。
2.Timeline记录内存
我们使用Timeline时间线记录内存变化,能够更加精准的观察内存的变化,例子:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" charset="UTF-8" />
<title>任务管理器监控内存变化</title>
</head>
<body>
<button id="btn">
Add
</button>
<script type="text/javascript">
const arrList = []
function test(){
for(let i = 0;i < 100000;i++){
document.body.appendChild(document.createElement('p'))
}
arrList.push(new Array(100000).join('x'))
}
document.getElementById('btn').addEventListener('click',test)
</script>
</body>
</html>
用chrome浏览器将这个html文件打开,并打开控制台,点击performance选项,开始录制,然后点击页面上的add按钮,间断的点击几次,然后结束录屏。
3.堆快照查找分离DOM
什么是分离DOM
- 界面元素存活在DOM树上
- 垃圾对象时的DOM节点:如果这个节点从当前的DOM树上进行了脱离,而且在js代码当中也没有人再引用的DOM节点,它其实就成为了一个垃圾
- 分离状态的DOM节点:如果这个节点从当前的DOM树上进行了脱离,而且在js代码当中有人在引用的DOM节点,它称为分离状态的DOM节点
例子:点击按钮后,DOM树当中中生成了分离的DOM,该分离的DOM页面上没有使用,但是代码中在使用,因此造成空间的浪费,我们把TmpEle置空,让GC对垃圾进行回收。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" charset="UTF-8" />
<title>任务管理器监控内存变化</title>
</head>
<body>
<button id="btn">
Add
</button>
<script type="text/javascript">
var tmpEle
function fn() {
var ul = document.createElement('ul')
for(var i = 0; i<10 ;i++ ){
var li = document.createElement('li')
ul.appendChild(li)
}
tmpEle = ul
}
document.getElementById('btn').addEventListener('click',fn)//js中创建的节点并没有往我们页面里添加,是分离的DOM
</script>
</body>
</html>
以上动图出处:https://coder5.blog.csdn.net/article/details/109723747
4.判断是否存在频繁GC
为什么确定频繁垃圾回收
- GC工作时应用程序是停止的
- 频繁且过长的GC会导致应用假死
- 用户使用中感知应用卡顿
确定频繁的垃圾回收
- Timeline中频繁的上升下降
- 任务管理器中数据频繁的增加减小
四、代码优化介绍
如何精准测试JavaScript性能
- 本质上就是采集大量的执行样本进行数学统计和分析
- 使用基于Benchmark.js的https://jsperf.com(服务器停止维护,不再使用,可以使用JSBench:https://jsbench.me/)完成
Jsperf使用流程
- 使用GitHub账号登录
- 填写个人信息(非必填)
- 填写详细的测试用例信息(title、slug):slug必须是唯一的
- 填写准备代码(DOM操作时经常使用)
- 填写必要有setup与teardown代码:setup是一个前置准备,teardown是一个所有代码执行完成之后要做一个销毁的工作
- 填写测试代码片段
五、 慎用全局变量
为什么要慎用
- 全局变量定义在全局执行上下文,是所有作用域链的顶端
- 全局执行上下文一直存在于上下文执行栈,直到程序退出才会消失,对于GC的工作状态是不利的,因为GC只要发现这个全局变量还处于存活的状态就不回收
- 如果某个局部作用域出现了同名变量则会遮蔽或污染全局
代码演示:
var i,str = ''
for(i = 0;i<1000;i++){
str += i
}
for(i = 0;i<1000;i++){
let str = '' //函数内部定义的局部变量
str += i
}
可以看出全局变量定义的i的这段代码性能没有局部变量里定义的好。
缓存全局变量:
将使用中无法避免的全局变量缓存到局部,当我们想要查找DOM元素的时候,这种情况下,我们就必须使用document,这个document不是我们后边去定义的,而是存在于我们当前全局的对象下边直接内置好了可以去使用的,所以在这种情况下可以选择把这种大量需要重复使用的全局变量放置于某个局部作用于当中,从而达到缓存的效果。
代码演示:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width,initial-scale=1.0" charset="UTF-8" />
<title>缓存全局变量</title>
</head>
<body>
<input type="button" value="btn" id="btn1">
<input type="button" value="btn" id="btn2">
<input type="button" value="btn" id="btn3">
<input type="button" value="btn" id="btn4">
<p>1111</p>
<input type="button" value="btn" id="btn5">
<input type="button" value="btn" id="btn6">
<p>222</p>
<input type="button" value="btn" id="btn7">
<input type="button" value="btn" id="btn8">
<p>333</p>
<input type="button" value="btn" id="btn9">
<input type="button" value="btn" id="btn10">
<script type="text/javascript">
function getBtn(){
let oBtn1 = document.getElementById('btn1')
let oBtn3 = document.getElementById('btn3')
let oBtn5 = document.getElementById('btn5')
let oBtn7 = document.getElementById('btn7')
let oBtn9 = document.getElementById('btn9')
}
function getBtn2(){
let obj = document
let oBtn1 = obj.getElementById('btn1')
let oBtn3 = obj.getElementById('btn3')
let oBtn5 = obj.getElementById('btn5')
let oBtn7 = obj.getElementById('btn7')
let oBtn9 = obj.getElementById('btn9')
}
</script>
</body>
</html>
把body里面的html代码放到准备阶段里,不包含body以外的内容
缓存全局变量的代码 性能更高
通过原型对象添加附加方法:在原型对象上新增实例对象需要的方法,代码演示如下:
var fn1 = function(){
this.foo = function(){//相当于在某个构造函数内部直接给所有的对象实例添加上了成员方法,后续再使用的过程中都可以拿到
console.log(11111)
}
}
let f1 = new fn1()
var fn2 = function(){}
fn2.prototype.foo = function(){//通过原型对象添加方法
console.log(11111)
}
let f2 = new fn2()
通过原型对象添加方法更好一些。