JavaScript 性能优化
内存管理- 内存:由可读写单元组成,表示一片可操作空间
- 管理:人为的去操作一片空间的申请、使用和释放
- 内存管理: 开发者主动申请空间、使用空间、释放空间
- 管理流程:
- 申请 let obj = {}
- 使用 obj.name = ‘lg’
- 释放 obj = null
垃圾回收- JavaScript中内存管理是自动
- 对象不再被引用时是垃圾
- 对象不能从根上访问到时是垃圾
- 可达对象: 可以访问到的对象(引用、作用域链)
- 可达的标准就是从根出发是否能够被找到
- JavaScript中的根就可以理解为是全局变量对象
let obj = {name: 'xm'}let ali = obj; //xm对象被两个引用obj = null //obj没有引用xm了,只有ali了,xm对象仍是可达对象
GC 算法- GC 就是垃圾回收机制的简写,垃圾回收器完成具体的工作
- GC可以找到内存中的垃圾、并释放和回收空间
- 算法就是工作时查找和回收所遵循的规则
常用GC算法- 引用计数
- 发现垃圾时立即回收
- 最大限度减少程序暂停
- 缺点: 无法回收循环引用的对象
- 时间开销大
- 引用计数
- 标记清除- 核心思想:分标记和清除二个阶段完成- 遍历所有对象找标记活动对象- 遍历所有对象清除没有标记对象- 回收相应的空间- 优点:能解决循环引用不能回收的问题- 缺点:会产生空间碎片化问题- 标记整理- 标记整理可以看做是标记清除的增强- 标记阶段的操作和标记清除一致- 清除阶段会先执行整理,移动对象位置- 分代回收
V8- V8是一款主流的JavaScript执行引擎
- V8采用即时编译
- V8内存设限
V8垃圾回收策略- 采用分代回收的思想
- 内存分为新生代、老生代
- 针对不同对象采用不同算法
V8中常用GC算法- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
Performance工具介绍- GC的目的是为了实现内存空间的良性循环
- 良性循环的基石是合理使用
- 时间关注才能确定是否合理
- Performance提供多种监控方式
- 通过Performance时刻监控内存
Performance使用步骤- 打开浏览器输入目标网址
- 进入开发人员工具面板,选择性能
- 开启录制功能,访问具体界面
- 执行用户行为,一段时间后停止录制
- 分析界面中记录的内存信息
内存问题的外在表现- 页面出现延迟加载或经常必暂停
- 页面持续性出现糟糕的性能
- 页面的性能随时间延长越来越差
界定内存问题的标准- 内存泄露:内存使用持续升高
- 内存膨胀:在多数设备上都存在性能问题
- 频繁垃圾回收:通过内存变化图进行分析
监控内存的几种方式- 浏览器任务管理器
- Timeline时序图记录
- 堆快照查找分离DOM
- 判断是否存在频繁的垃圾回收
任务管理器监控内存- Shift + Esc打开浏览器任务管理器
- 右键选中JavaScript使用的内存
- 内存列:当前DOM节点所占内存,如果频繁在变,说明当前界面存在频繁的DOM操作
- JavaScript内存列: 小括号里的内存如果一直在增加,说明内存一直往上走,有问题
Timeline记录内存- 打开F12,然后Performance
- 点击录屏,操作页面之后,点结束
- 然后查看数据Screenshots,Memory
- 查看蓝色波浪线,如果一直上升没下降的,鼠标移上去找到对应画面即代码块
堆快照查找分离DOM- 打开F12,然后Memory
- 在操作前,点击Take snapsshot,保存操作前快照
- 找到要查找的对象值后,回到Profiles
- 操作之后,重新拍照
- 之前在代码中创建的DOM节点,这时候就能查得到了
- 在确定一个DOM元素在后续不会使用之后,可以在代码中置为null
为什么要确定频繁垃圾回收- GC工作时应用程序是停止的
- 频繁且过长的GC会导致应用假死
- 用户使用中感知应用卡顿
- 判断方法一:Timeline中频繁的上升下降
- 二:任务管理器中数据频繁的增加减小
优化代码,如何精准测试JavaScript性能- 本质上就是采集大量的执行样本进行数学统计和分析
- 使用基于Benchmark.js的https://jsperf.com/完成
为什么要慎用全局变量- 全局变量定义在全局执行上下文,是所有作用域链的顶端
- 全局执行上下文一直存在于上下文执行栈,直到程序退出
- 如果某个局部作用域出现了同名变量则会遮蔽或污染全局
var i, str = ''for (i = 0; i < 1000; i++) {str += i}for (let i = 0; i < 1000; i++) {let str = ''str += i}
示例图:
缓存全局变量- 将使用中无法避免的全局变量缓存到局部
function getBtn2() {let obj = document //缓存documentlet oBtn1 = obj.getElementById('btn1')let oBtn3 = obj.getElementById('btn3')let oBtn5 = obj.getElementById('btn5')let oBtn7 = obj.getElementById('btn7')let oBtn9 = obj.getElementById('btn9')}
通过原型对象添加附加方法
```javascript var fn1 = function() {this.foo = function() {console.log(11111)}
}
let f1 = new fn1()
var fn2 = function() {}fn2.prototype.foo = function() { //通过构造函数原型对象通过prototype直接指向foo,设置函数console.log(11111)}let f2 = new fn2() //接收拿到的实例对象
- <br />避开闭包陷阱- 特点:外部具有指向内部的引用- 在“外”部作用域访问“内”部作用域的数据- 闭包是一种强大的语法- 闭包使用不当很容易出现内存泄露- 不要为了闭包而闭包```javascriptfunction foo() {var el = document.getElementById('btn');el.onclick = function() {console.log(el.id)}el = null; //优化,当btnDOM元素被删除后,有了这个null,el及btn这一块空间将得到释放}foo();
避免属性访问方法使用- Javascript中的面向对象
- JS不需属性的访问方法,所有属性都是外部可见的
- 使用属性访问方法只会增加一层重定义,没有访问的控制力
- Javascript中的面向对象
function Person() {this.name = 'icoder'this.age = 18this.getAge = function() {return this.age}}const p1 = new Person()const a = p1.getAge()function Person() {this.name = 'icoder'this.age = 18}const p2 = new Person()const b = p2.age
For循环优化var arrList = []arrList[10000] = 'icoder'for (var i = 0; i < arrList.length; i++) {console.log(arrList[i])}for (var i = arrList.length; i; i--) {console.log(arrList[i])}for (var i = 0,len = arrList.length; i < len; i++) {console.log(arrList[i])}//三种比较var arrList = new Array(1, 2, 3, 4, 5)arrList.forEach(function(item) { //最优console.log(item)})for (var i = arrList.length; i; i--) { //次之console.log(arrList[i])}for (var i in arrList) { //最差console.log(arrList[i])}
文档碎片优化节点添加- 节点的添加操作必然会有回流和重绘
for (var i = 0; i < 10; i++) {var oP = document.createElement('p')oP.innerHTML = idocument.body.appendChild(oP)}const fragEle = document.createDocumentFragment()for (var i = 0; i < 10; i++) {var oP = document.createElement('p')oP.innerHTML = ifragEle.appendChild(oP)}document.body.appendChild(fragEle)
克隆来优化节点操作- cloneNode() 方法可创建指定的节点的精确拷贝。
- cloneNode() 方法 拷贝所有属性和值。
- 该方法将复制并返回调用它的节点的副本。如果传递给它的参数是 true,它还将递归复制当前节点的所有子孙节点。否则,它只复制当前节点。
<body><p id="box1">old</p><script>for (var i = 0; i < 3; i++) {var oP = document.createElement('p')oP.innerHTML = idocument.body.appendChild(oP)}var oldP = document.getElementById('box1')for (var i = 0; i < 3; i++) {var newP = oldP.cloneNode(false)newP.innerHTML = idocument.body.appendChild(newP)}</script></body>
直接量替换new Objectvar a = [1, 2, 3] //直接量,优var a1 = new Array(3) //new Object,差a1[0] = 1a1[1] = 2a1[2] = 3
JS性能优化二
jsperf.com不能用了,用jsbench.me
堆栈中的JS执行过程- JS代码在开始执行后,首先会在堆内存里创建一个执行环境栈ECStack
- 执行环境栈ECStack存储不同的执行上下文
- 从上往下创建执行上下文
- 基本类型数值都是存放在栈里面,一般主线程管理
- 引用类型存放在堆里面,一般用GC处理
- 在栈内存里,每遇到一个新函数,都会重新生成一个执行上下文进栈
示例图:

减少判断层级- 每当碰到多层if else 判断时,考虑下提前return的操作,以减少层级
- 还可以考虑switch case
function doSomething (part, chapter) {const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']if (part) {if (parts.includes(part)) {console.log('属于当前课程')if (chapter > 5) {console.log('您需要提供 VIP 身份')}}} else {console.log('请确认模块信息')}}doSomething('ES2016', 6)//优function doSomething (part, chapter) {const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']if (!part) {console.log('请确认模块信息')return}if (!parts.includes(part)) returnconsole.log('属于当前课程')if (chapter > 5) {console.log('您需要提供 VIP 身份')}}doSomething('ES2016', 6)
减少作用域链查找层级
```javascript var name = ‘zce’function foo () { name = ‘zce666’ // 这里的Name 是属于全局的 function baz () { var age = 38 console.log(age) console.log(name) } baz() }
foo()
//优 var name = ‘zce’
function foo () { var name = ‘zce666’ // 这里的Name 是属于全局的,但这里是牺牲空间获取速度,如果name数据量大,那上面那种方式可能更合适 function baz () { var age = 38 console.log(age) console.log(name) } baz() }
foo()
- <br />减少数据读取次数<br />```javascript<body><div id="skip" class="skip"></div><script>var oBox = document.getElementById('skip')function hasEle (ele, cls) {return ele.className == cls}function hasEle (ele, cls) {var clsname = ele.className //频繁要读取的数据进行了缓存 优return clsname == cls}console.log(hasEle(oBox, 'skip'))</script></body>
- 字面量与构造式
//构造式 慢let test = () => {let obj = new Object()obj.name = 'zce'obj.age = 38obj.slogan = '我为前端而活'return obj}//字面量 快let test = () => {let obj = {name: 'zce',age: 38,slogan : '我为前端而活'}return obj}console.log(test())var str1 = 'zce说我为前端而活' //快 zce说我为前端而活var str2 = new String('zce说我为前端而活') //慢 [String: 'zce说我为前端而活']console.log(str1)console.log(str2)
- 减少循环体中的活动
//最次// var test = () => {// var i// var arr = ['zce', 38, '我为前端而活']// for(i=0; i<arr.length; i++) {// console.log(arr[i])// }// }//次优var test = () => {var ivar arr = ['zce', 38, '我为前端而活']var len = arr.lengthfor(i=0; i<len; i++) {console.log(arr[i])}}//最优var test = () => {var arr = ['zce', 38, '我为前端而活']var len = arr.lengthwhile(len--) {console.log(arr[len])}}test()
减少声明及语句数<body><div id="box" style="width: 100px; height: 100px;"></div><script>// var oBox = document.getElementById('box')//对于不会每次都会使用的变量,就不用做缓存,反而会消耗时间// var test = (ele) => {// let w = ele.offsetWidth// let h = ele.offsetHeight// return w * h// }// var test = (ele) => {// return ele.offsetWidth * ele.offsetHeight// }// console.log(test(oBox))var test = () => {var name = 'zce'var age = 38var slogan = '我为前端而活'return name + age + slogan}var test = () => {//减少语句数var name = 'zce',age = 38,slogan = '我为前端而活'return name + age + slogan}console.log(test())</script></body>
示例图:
- 采用事件绑定(委托)
<body><ul id="ul"><li>ZCE</li><li>28</li><li>我为前端而活</li></ul><script>var list = document.querySelectorAll('li')// function showTxt (ev) {// console.log(ev.target.innerHTML)// }// for(let item of list) {// item.onclick = showTxt// }//事件委托,优var oUl = document.getElementById('ul')oUl.addEventListener('click', showTxt, true)function showTxt (ev) {var obj = ev.targetif (obj.nodeName.toLowerCase() === 'li') {console.log(obj.innerHTML)}}</script></body>
