JavaScript性能优化
花了一个人生命中,最宝贵的的时光来赚钱,为了在最不宝贵的时间里享受一点点容易被质疑的自由。
一、内存管理
⑴. 内存管理
- 内存: 由可读单元组成,表示一片可操作空间
- 管理: 人为地操作一片空间的申请、使用、释放
- 内存管理: 开发者主动的申请空间、使用空间、释放空间
- 流程: 申请 - 使用 - 释放
示例:
// 申请let obj = {}// 使用obj.name = 'zoe'// 释放obj = null
⑵. 垃圾回收
垃圾:
- JavaScript 中内存管理是自动的
- 对象不再被
引用
时是垃圾 - 对象不能从
根上访问
到是垃圾
可达对象:
- 可以访问到的对象就是可达对象(引用,作用域链)
- 可达的标准就是从根上出发是否能被找到
- JavaScript 中的根可以理解为全局变量对象
代码示例:
function objGroup(obj1, obj2) { obj1.next = obj2 obj2.prev = obj1 return { o1: obj1, o2: obj2 }}let obj = objGroup({name: 'obj1'}, {name: 'obj2'})console.log(obj)// => 如下:// {// o1: { name: 'obj1', next: { name: 'obj2', prev: [Circular *1] } },// o2: {name: 'obj2', prev: { name: 'obj1', next: [Circular *2] } }// }
对象引用关系:
二、GC 算法
1. 概述
定义与作用:
- GC 就是垃圾回收机制的简写
- GC 可以找到内存中的垃圾、并释放和回收空间
GC 中的垃圾:
- 程序中不再需要的对象
- 程序中不能再访问到的对象
GC 算法:
- GC 是一种机制,垃圾回收器完成具体的工作
- 具体工作:查找垃圾、释放空间、回收空间
- 算法就工作时查找和回收垃圾所遵循的规则
2. 引用计数
⑴. 概念
- 核心思想: 设置引用数,判断当前引用数是否为零
- 引用计数器
- 引用关系改变时修改引用数字
- 引用数字为 0 时,立即回收
示例:
// 能够被全局引用 - 引用计数为 1function fn() { const num1 = 1 const num2 = 2}fn()// 被调用后 引用数字 -1, 立即回收
⑵. 优缺点
- 优点:
- 发现垃圾时立即回收
- 最大限度减少程序暂停
- 缺点:
- 无法回收循环引用的对象
- 时间开销大(对所有对象进行数值的监控和修改,本身就会占用时间和资源)
对象循环引用示例:
function fn() { // 引用数字 +1 const obj1 = {} const obj2 = {} // 引用数字 +1 obj1.name = obj2 obj2.name = obj1 return}// 引用数字 -1, 因为存在循环引用对象的关系, 所以并不能垃圾回收fn()
3. 标记清除
⑴. 原理
- 核心思想: 分标记和清除两个阶段
- 遍历所有对象找标记活动对象
- 遍历所有对象,清除没有标记对象,并抹掉第一个阶段标的标记
- 回收相应空间,将回收的空间加到空闲链表中,方便后面的程序申请空间使用
- Global 中的引用: 会采用递归的方式遍历所有对象,并进行标记
- a1 => b1 只是内部引用,并不会被标记(解决了引用计数的缺陷)
⑵. 优缺点
优点: 很好地解决了标记清除法中,存在的 对象循环引用 问题(局部作用域里面的内容无法被标记);
缺点:
- 空间链表地址不连续(空间碎片化),不能进行空间最大化使用
- 不会立即回收垃圾对象,清除的时候程序是停止工作的
空间碎片化: 由于当前回收的垃圾对象,在地址上是不连续的,这些对象被回收后,他们释放的空间分散在各个角落,并且因为空间大小原因,不便于后续使用
4. 标记整理
⑴. 原理
- 标记整理可以看做标记清除的增强
- 标记阶段的操作和标记清除一致
- 清除阶段会先执行整理,移动对象位置
⑵. 优缺点
- 优点: 相较标记清除算法减少了碎片化空间
- 缺点: 不会立即回收垃圾对象,清除的时候程序是停止工作的
三、V8 引擎的垃圾回收
⑴. 概念
- V8 是一款主流的 JavaScript 执行引擎
- V8 采用即时编译
- V8 内存设限(64 位 不超过 1.5G)
⑵. 垃圾回收策略
- 采用分代回收思想: 内存分为新生代、老生代针对不同对象,采用不同的操作
- V8 中常见的 GC 算法: 分代回收、空间复制、标记清除、标记整理、标记增量
⑶. 回收新生代对象
内存分配:
- V8 内存空间一分为二
- 小空间用于储存新生代对象(32M | 16M)
- 新生代对象指的是存活时间较短的对象
回收实现:
- 复制算法 + 标记算法
- 新生代内存区分为两个等大小的空间
- 使用状态: From; 空间状态: To
- 活动对象储存于 Form 空间
- 标记整理后将活动对象拷贝至 To
- Form 与 To 交完完成空间释放
回收细节说明:
- 拷贝过程中可能出现晋升
- 晋升就是将新生代对象移动至老生代
- 一轮 GC 还存活的新生代需要晋升
- To 空间使用率超过 25%
⑷. 回收老生代对象
说明:
- 老生代对象存放于右侧老生代区域
- 老生代区域空间大小:64 位系统 1.4G,32 位 700M
- 老生代对象指的就是存活时间较久的对象
回收实现:
- 标记清除 + 标记整理 + 标记增量
- 首先使用标记清除,完成垃圾空间回收
- 采用标记整理进行空间优化
- 采用标记增量进行效率提升
同新生代对比:
- 新生代区域垃圾回收使用空间换时间: 复制算法,每时每刻都有一个空置的空间,带来时间生的提升
- 老生代区域垃圾回收使用时间换空间
⑸. 标记增量
将垃圾回收操作拆分,组合实现垃圾回收;实现垃圾回收与程序执行交替完成
四、performance 工具
1. 工具介绍
- GC 的目的是为了实现内存空间的良性循环
- 良心循环的基石是合理使用
- 时刻关注才能确定是否合理
- performance 提供了多种监控方式
2. 内存问题的体现
- 页面出现延迟加载、经常暂停的情况
- 页面持续性出现糟糕的性能
- 页面的性能随时间延迟,越来越差
3. 监控内存的几种方式
界定内存问题的标准:
- 内存泄漏: 内存使用持续升高
- 内存膨胀: 在多数设备上都存在性能问题
- 频繁垃圾回收: 通过内存变化图分析
监控内存的几种方式:
- 浏览器任务管理器
- Timeline 时序图记录
- 堆快照查找分离 DOM
- 判断是否存在频繁的垃圾回收
4. 浏览器任务管理器
能够直观感受到内存占用的大小,如果内存一直增加没能得到释放的话就是存在内存泄漏的
5. Timeline 记录
测试代码:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>时间线内存变化</title></head><body> <button id="oBtn">add</button> <script> const arrList = [] function test() { for(let i =0; i< 100000; i++) { // 添加 p 标签 document.body.appendChild(document.createElement('p')) } // 创建字符串 arrList.push(new Array(100000).join('x')) } // 创建绑定事件 document.getElementById('oBtn').addEventListener('click', test) </script></body></html>
performance 工具:
可以清晰看到堆的走势,对应时间和操作,可以查看JS 的内存消耗情况
6. 堆快照查找分离 DOM
什么是分离 DOM:
- 界面元素存活在 DOM 树上
- 垃圾对象时的 DOM 节点
- 分离状态的 DOM 节点
调试的代码:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>时间线内存变化</title></head><body> <button id="oBtn">add</button> <script> var tmpEle function fn() { var ul = document.createElement('ul') for(let i =0; i< 1000; i++) { var li = document.createElement('li') ul.appendChild(li) } // 设定一个变量 引用了 ul, 页面中没有渲染, 这里就是一个 分离 DOM tmpEle = ul // 如果确定之后这个 DOM 不需要,直接可以清空 tmpEle = null } document.getElementById('oBtn').addEventListener('click', fn) </script></body></html>
内存快照:
活动对象的展示:
分离 DOM 在界面上不体现,但是会造成内存的浪费,通过堆快照可以定位分离 DOM
7. 判断是否存在频繁GC
为什么确定垃圾回收:
- GC 工作时应用程序是停止的
- 频繁且过长的 GC 会导致应用卡死
- 用户使用中感知应用卡顿
判断频繁GC 的方法:
- Timeline 中频繁的上升和下降
- 任务管理器中数据频繁的增加减小
五、V8 工作流程
词法分析->语法分析->预解析->全量解析->预编译->编译->机器码
六、堆栈处理
七、闭包与垃圾回收
八、代码优化
1. JSBench
JSBench 官网: JSBench是一个在线测试JS效率的一个网站
2. 变量局部化
- 尽量使用局部变量,提升代码执行效率(减少数据访问时需要查找的路径 - 数据的存储和读取)
代码示例:
// 全局变量var i, arr = ''function packageDom1() { for (i = 0; i < 1000; i++) { arr += i }}packageDom1()// 局部变量function packageDom2() { let arr = '' for (let i = 0; i < 1000; i++) { arr += i }}packageDom2()
JSBench测试:
由此可见,局部变量对于 JS 性能更有利;(数值越大,性能越好;防止偶然性可以多测试几次)
3. 缓存数据
对于需要多次使用的数据进行提前保存,后续使用(作用域链查找变快)
减少声明和语句数(词法 语法)
代码示例:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>缓存数据</title></head><body> <button id="oBtn" class="skip">Add</button> <script> const oBtn = document.getElementById('oBtn') // 使用时调用 function hasClassName (ele, cls) { return ele.className == cls } // 将函数体中的 className 值缓存起来 function hasClassName(ele, cls) { var clsName = ele.className return clsName == cls } console.log(hasClassName(oBtn, 'skip')) </script></body></html>
JSBench测试:
由此可见,提前将数据缓存,供多次调用有助于提升性能
4. 减少访问层级
代码示例:
// 少层级调用function Person() { this.name = 'zoe', this.age = 18}let p = new Person()console.log(p.name)// 多层级调用function Person() { this.name = 'zoe', this.age = 18, this.getName = function() { return this.name }}let p = new Person()console.log(p.getName())
JSBench测试:
由此可见,减少访问层级,有助于提升性能
5. 节流与防抖
- 目的: 在一些高频率事件的触发场景下,并不希望对应的函数多次执行
- 场景: 鼠标滚动、输入的模糊匹配、轮播、点击
- 浏览器默认情况下,
4~6ms
监听时间间隔 - 防抖: 在一定时间内,只触发一次
- 节流: 在触发后的一定时间后执行,如果事件重新触发,则重新计时
⑴. 实现防抖
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>防抖函数实现</title></head><body> <button id="btn">点击</button> <script> var oBtn = document.getElementById('btn') // oBtn.onclick = function () { // console.log('点击了') // } / * handle 最终需要执行的事件监听 * wait 事件触发之后多久开始执行 * immediate 控制执行第一次还是最后一次,false 执行最后一次 */ function myDebounce(handle, wait, immediate) { // 参数类型判断及默认值处理 if (typeof handle !== 'function') throw new Error('handle must be an function') if (typeof wait === 'undefined') wait = 300 if (typeof wait === 'boolean') { immediate = wait wait = 300 } if (typeof immediate !== 'boolean') immediate = false // 所谓的防抖效果我们想要实现的就是有一个 ”人“ 可以管理 handle 的执行次数 // 如果我们想要执行最后一次,那就意味着无论我们当前点击了多少次,前面的N-1次都无用 let timer = null return function proxy(...args) { let self = this, init = immediate && !timer clearTimeout(timer) timer = setTimeout(() => { timer = null !immediate ? handle.call(self, ...args) : null }, wait) // 如果当前传递进来的是 true 就表示我们需要立即执行 // 如果想要实现只在第一次执行,那么可以添加上 timer 为 null 做为判断 // 因为只要 timer 为 Null 就意味着没有第二次....点击 init ? handle.call(self, ...args) : null } } // 定义事件执行函数 function btnClick(ev) { console.log('点击了1111', this, ev) } // 当我们执行了按钮点击之后就会执行...返回的 proxy oBtn.onclick = myDebounce(btnClick, 200, false) // oBtn.onclick = btnClick() // this ev </script></body></html>
⑵. 实现节流
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>节流函数实现</title> <style> body { height: 5000px; } </style></head><body> <script> // 节流:我们这里的节流指的就是在自定义的一段时间内让事件进行触发 function myThrottle(handle, wait) { if (typeof handle !== 'function') throw new Error('handle must be an function') if (typeof wait === 'undefined') wait = 400 let previous = 0 // 定义变量记录上一次执行时的时间 let timer = null // 用它来管理定时器 return function proxy(...args) { let now = new Date() // 定义变量记录当前次执行的时刻时间点 let self = this let interval = wait - (now - previous) if (interval <= 0) { // 此时就说明是一个非高频次操作,可以执行 handle clearTimeout(timer) timer = null handle.call(self, ...args) previous = new Date() } else if (!timer) { // 当我们发现当前系统中有一个定时器了,就意味着我们不需要再开启定时器 // 此时就说明这次的操作发生在了我们定义的频次时间范围内,那就不应该执行 handle // 这个时候我们就可以自定义一个定时器,让 handle 在 interval 之后去执行 timer = setTimeout(() => { clearTimeout(timer) // 这个操作只是将系统中的定时器清除了,但是 timer 中的值还在 timer = null handle.call(self, ...args) previous = new Date() }, interval) } } } // 定义滚动事件监听 function scrollFn() { console.log('滚动了') } // window.onscroll = scrollFn window.onscroll = myThrottle(scrollFn, 600) </script></body></html>
6. 减少判断层级
代码示例:
function doSomeThing(part, chapter) { const parts = ['Vue', '工程化', 'Node', '性能优化'] if (part) { if (parts.includes(part)) { console.log('属于当前课程') if (chapter > 5) { console.log('请充值') } else { console.log('请登录') } } else { console.log('查无此课程') } } else { console.log('请确认模块信息') }}doSomeThing('Vue', 6)// 减少判断层级function doSomeThing(part, chapter) { const parts = ['Vue', '工程化', 'Node', '性能优化'] if (!part) { console.log('请确认模块信息') return } if (!parts.includes(part)) { console.log('查无此课程') return } console.log('属于当前课程') if (chapter > 5) { console.log('请充值') return } console.log('请登录')}doSomeThing('Vue', 6)
JSBench测试:
由此可见,减少判断层级,有助于提升性能
7. 减少循环体活动
代码示例:
const test = () => { const arr = [1, 2, 3, 4, 5] for(let i =0; i < arr.length; i++) { console.log(arr[i]) }}test()// 减少循环体活动const test = () => { const arr = [1, 2, 3, 4, 5] let len = arr.length for(let i =0; i < len; i++) { console.log(arr[i]) }}test()// 减少循环体活动 2const test = () => { const arr = [1, 2, 3, 4, 5] let len= arr.length while(len--) { console.log(arr[len]) }}test()// => 5, 4, 3, 2, 1 从后往前找 len == 0, 不再执行
8. 字面量与构造式
代码示例:
// 构造式 - 不断地调用函数const test = () => { const obj = new Object() obj.name = 'zoe' obj.age = 18 obj.slogan = 'fall in love' return obj}console.log(test())// 字面量 - 直接开辟空间存储const test = () => { const obj = { name: 'zoe', age: 18, slogan: 'fall in love' } return obj}console.log(test())