#### JavaScript性能优化 **如何编写高性能的JavaScript**
- 性能优化是不可避免的
- 哪些内容可以看作是性能优化
- 无所不在的前端性能优化
#### 本阶段的核心是JavaScript语言的优化
- 内存管理
- 垃圾回收与常见的GC算法
- V8引擎的垃圾回收
- Performance工具(垃圾回收的监控)
- 代码优化实例
#### JavaScript内存管理(Memory Management)
- 内存: 由可读写的单元组成, 表示一片可操作空间
- 管理: 认为的去操作一片空间的申请、使用和释放
- 内存管理: 开发者主动申请空间、使用空间、释放空间
- 管理流程: 申请-使用-释放
```javascript
// 申请空间
let obj = {}
// 使用空间
obj.name = 'liuchao'
// 释放空间
obj = null
```
#### JavaScript中的垃圾回收
- JavaScript中内存管理是自动的
- 对象不再被引用时是垃圾
- 对象不能从根上访问到时是垃圾
#### 可达对象
- 可达对象: 可以访问到的对象(引用、作用域链)
- 可达的标准就是从根出发时候能够被找到
- JavaScript中的跟可以理解为是全局变量对象
```javascript
let obj = {name: 'liuchao'}
let ali = obj
obj = null//ali变量还是有值的
```
#### 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)
const user1 = {age: 1}
const user2 = {age: 2}
const user3 = {age: 3}
const nameList = [user1.name, user2.name, user3.name, ]
// function fn(){
// num1 = 1
// num2 = 2
// }
function fn(){
const num1 = 1
const num2 = 2
}
fn()
```
#### GC算法介绍
- GC就是垃圾回收机制的简写
- GC可以找到内存中的垃圾、并释放和回收空间
- 程序中不再需要使用的对象
- 程序中不能在访问到的对象
- 算法就是工作时查找和回收所遵循的规则
- 常见GC算法
- 引用计数
- 标记清除
- 标记整理
- 分代回收
#### 引用计数算法实现原理
- 核心思想: 设置引用数, 判断当前引用数是否为0
- 引用计数器(被弃用的原因, 性能不好)
- 引用关系改变时修改引用数字
- 引用数字为0时立即回收
- 引用计数的优点
- 发现垃圾时立即回收
- 减少程序卡顿时间
- 最大限度减少程序暂停(内存即将占满,立即清除)
- 引用计数的缺点
- 无法回收循环引用的对象
- 时间(资源)开销大
```javascript
// circular reference 对象循环引用
function fn(){
const obj1 = {}
const obj2 = {}
obj1.name = obj2
obj2.name = obj1
return 'A circular reference.'
}
fn()
```
#### 标记清除算法的实现原理
- 核心思想: 分为标记和清除两个阶段
- 遍历所有对象找标记活动对象(对象、子对象递归标记)
- 遍历所有对象清除没有标记对象(也会清除未清除对象的标记)
- 回收相应的空间(回收的空间放到空闲链表上方便后续使用)
- 标记清除算法优点
- 解决对象循环引用
- 标记清除算法缺点
- 不会立即回收垃圾对象
- 空间碎片化: 回收空间地址不连续(分散在各个角落)
#### 标记整理算的实现原理
- 标记整理可以看作是标记清除的增强
- 标记阶段的操作和标记清除一致
- 清除阶段会执行整理, 移动对象位置(为了地址上连续)
- 不会立即回收垃圾对象
#### 常见GC算法总结
- 引用计数的优点
- 发现垃圾时立即回收
- 减少程序卡顿时间
- 最大限度减少程序暂停(内存即将占满,立即清除)
- 引用计数的缺点
- 无法回收循环引用的对象
- 时间(资源)开销大
- 标记清除算法优点
- 解决对象循环引用
- 标记清除算法缺点
- 不会立即回收垃圾对象
- 空间碎片化: 回收空间地址不连续(分散在各个角落)
- 标记整理的优点
- 减少碎片化空间
- 标记整理的缺点点
- 不会立即回收垃圾对象
#### 认识V8
- V8是一款主流的JavaScript执行引擎
- V8采用即时编译
- V8快: 1. 优秀内存管理机制 2. V8采用即时编译(原先源码转换成字节码, 然后字节码转换成机器码执行; 即时编译: 源码转换成机器码直接执行)
- V8内存设限(64X: 1.5G; 32X: 800M)
- V8本身为了浏览器而制造, 现有大小对网页应用来说足够使用了
- V8内部的垃圾回收机制决定了再用此设置是合理的
- 垃圾内存达到1.5G的时候使用增量标记进行垃圾回收是50ms,如果使用非增量标记进行垃圾回收需要1s
#### V8垃圾回收策略
- 采用分代回收的思想
- 内存分为新生代、老生代
- 针对不同对象采用不同GC算法
- 分代回收
- 空间复制
- 标记清除
- 标记整理
- 标记增量
#### V8如何回收新生代对象
- V8内存分配
- V8内存空间一分为二
- 小空间用于存储新生代对象(32M(64X)|16M(32X))
- 新生代对象指存活时间比较短的对象(eq: 局部变量)
- 新生代对象回收实现
- 回收过程采用复制算法 + 标记整理
- 新生代内存去分为二个等大小空间
- 使用空间为From, 空闲空间为To
- 活动对象存储于From空间
- 标记整理后将活动对象拷贝至To
- From与To交换空间, 完成释放
- 新生代对象回收实现---说明
- 拷贝过程中可能出现晋升
- 晋升就是将新生代对象移动至老生代
- 一轮GC还存活的新生代需要晋升
- To空间的使用率超过25%(整体移动至老生代: From和To会交换)
#### V8如何回收老生代对象
- 老生代对象存放在右侧老生代区域
- 内存限制(64位操作系统1.4G, 32位操作系统700M)
- 老生代对象就是指存活时间较长的对象(闭包等)
- 老生代对象回收实现
- 主要采用标记清除、标记整理、增量标记算法
- 首先使用标记清除完成垃圾空间的回收
- 采用标记整理进行空间优化(晋升的时候并且老生代存储空间不足以存放新生代移动过来的对象)
- 采用增量标记进行效率优化
#### 新生代与老生代回收的细节对比
- 新生代区域垃圾回收使用空间换时间(复制算法)
- 老生代区域篮机回收不适合复制算法(空间大浪费奢侈、对象数据比较多消耗时间多)
- 标记增量如何优化垃圾回收
- 垃圾回收会终止程序执行
- 标记增量: 整个垃圾回收过程拆分成多个小步,替代整个垃圾回收, 实现程序执行和垃圾回收交替执行
#### V8垃圾回收总结
- V8是一款主流的JavaScript执行引擎
- V8内存设限(64X: 1.5G; 32X: 800M)
- 采用分代回收的思想
- 内存分为新生代、老生代
- 针对不同对象采用不同GC算法
#### Performance工具介绍
### 为什么使用Performance
- GC的目的是为了实现内存空间的良性循环
- 良性循环的基石是合理使用
- 时刻关注才能确定是否合理
- Performance提供多种监控方式
- **通过Performance时刻监控内存**
#### Performance使用步骤
- 打开浏览器输入牧鞭网址
- 进入开发人员工具面板, 选择性能
- 开启录制功能, 访问具体页面
- 执行用户行为, 一段时间后停止录制
- 分析界面中记录的内存信息
#### 内存问题的外在体现
- 频繁垃圾回收: 页面出现延迟加载或经常性暂停(网络正常下)
- 内存膨胀: 页面持续性出现糟糕的性能(网络正常下)
- 内存泄漏: 页面性能随时间延长越来越差(网络正常下)
#### 监控内存的几种方式
- 内存泄漏: 内存使用持续升高
- 内存膨胀: 在多数设备上都存在性能问题(主流设备上测试)
- 频繁垃圾回收: 通过内存变化图进行分析
- 方式
- 浏览器任务管理器
- Timeline时序图记录
- 堆快照查找分离DOM
- 判断是否存在频繁的垃圾回收
#### 浏览器任务管理器监控内存
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>浏览器任务管理器监控内存变化</title>
</head>
<body>
<button id='btn'>Add</button>
<script>
const oBtn = document.getElementById('btn')
oBtn.onclick = function (){
let arrList = new Array(1000000)
}
</script>
</body>
</html>
```
#### Timeline记录内存
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Timeline记录内存</title>
</head>
<body>
<button id='btn'>Add</button>
<script>
// 1. 创建大量DOM节点模拟内存消耗
// 2. DOM节点不够,数组配合其他方法, 形成非常长的字符串,模拟内存消耗
const arrList = []
function test(){
for(let i = 0; i < 100000;i++){
document.body.appendChild(document.createElement('p'))
}
arrList.push(new Array(1000000).join('x'))
}
document.getElementById('btn').addEventListener('click', test)
</script>
</body>
</html>
```
#### 堆快照查找分离DOM
- 界面元素存活在DOM树上
- 垃圾对象时的DOM节点(从DOM树上脱离, 并且程序中无引用)
- 分离状态的DOM节点(分离节点: 从DOM树上脱离, 但是程序中有引用; 页面上看不见, 但是占据内存, 导致内存泄漏)
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>堆快照查找分离DOM</title>
</head>
<body>
<button id='btn'>Add</button>
<script>
// 1. 创建大量DOM节点模拟内存消耗
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
tmpELe = null
}
document.getElementById('btn').addEventListener('click', fn)
</script>
</body>
</html>
```
#### 判断是否存在频繁的垃圾回收
- 原因
- GC工作时应用程序时停止的
- 频繁且过长的GC会导致应用假死
- 用户使用中感知应用卡顿
- 方法
- Timeline中频繁的上升和下降
- 任务管理器中数据频繁的增大减小
#### Performance使用总结
- Performance使用步骤
- 内存问题的相关分析
- Performance时序图监控内存变化
- 浏览器任务管理器监控内存
- 堆快照查找分离DOM
#### 代码优化介绍
- 如何精准测试JavaScript性能
- 本质上就是采集大量的执行样本进行数学统计和分析
- 使用基于Benchmark.js的https://jsperf.com/ 完成
#### Jsperf使用流程
- 使用GitHub账号登陆
- 填写个人信息(非必须)
- 填写详细的测试用例信息(title、slug)
- 填写准备代码(DOM操作室经常使用)
- 填写必要的setup和teardown代码
- 填写测试代码片段
#### 慎用全局变量
- 全局变量定义在全局执行上下文, 是所有作用域链的顶端(时间长)
- 全局执行上下文一直存在于上下文执行栈, 知道程序退出(GC工作不力, 降低内存使用)
- 如果某个局部作用域出现了同名变量则护额遮蔽或者污染全局
```javacsript
// 全局变量
var i, str = ''
for(i = 0; i < 100; i++ ){
str += i
}
for(let i = 0; i < 100; i++ ){
let str = ''
str += i
}
// 局部变量有很大性能提升
// Jsperf中查看结果
```
#### 缓存全局变量
- 将使用中无法避免的全局变量缓存到局部
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<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'>
<p>111</p>
<input type="button" value='btn' id='btn4'>
<input type="button" value='btn' id='btn5'>
<p>222</p>
<input type="button" value='btn' id='btn6'>
<input type="button" value='btn' id='btn7'>
<input type="button" value='btn' id='btn8'>
<p>33333</p>
<input type="button" value='btn' id='btn9'>
<input type="button" value='btn' id='btn10'>
<script>
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')
}
// Jsperf中查看结果
</script>
</body>
</html>
```
#### 通过原型新增方法
- 在原型对象上新增实例对象需要的方法
```javascript
var fn1= function(){
this.foo = function(){
console.log(11111)
}
}
let f1 = new fn1()
var fn2 = function(){}
fn2.prototype.foo = function(){
console.log(1111)
}
let f2 = new fn2()
// Jsperf中查看运算速度
```
#### 避开闭包陷阱
- 闭包特点
- 外部具有指向内部的引用
- 在‘外’部作用域访问‘内’部作用域的数据
- 闭包
- 闭包是一种强大的语法
- 闭包使用不当很容易出现内存泄漏
- 不要为了闭包而闭包
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>闭包陷阱</title>
</head>
<body>
<button id="btn">btn</button>
<script>
// function foo() {
// var el = document.getElementById('btn')
// el.onclick = function () {
// console.log(el.id)
// }
// }
// foo()
// 闭包优化
function foo() {
var el = document.getElementById('btn')
el.onclick = function () {
console.log(el)
console.log(el, el.id)
}
el = null
}
foo()
</script>
</body>
</html>
```
#### 避免属性访问方法使用
- JavaScript中的面向对象
- JS不需要属性的访问方法, 所有属性都是外部可见的
- 书用属性访问方法只会增加一层重定义, 没有访问的控制力
```javascript
function Person (){
this.name = 'Person'
this.age = 19
this.getAge = function(){
return this.age
}
}
const p1 = new Person()
const a = p2.getAge()
function Person1 (){
this.name = 'Person'
this.age = 19
}
const p2 = new Person1()
const b = p2.age
// Jsperf上查看运行速度
```
#### For循环优化 length提取
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>For循环优化</title>
</head>
<body>
<p class="btn"></p>
<p class="btn"></p>
<p class="btn"></p>
<p class="btn"></p>
<p class="btn"></p>
<p class="btn"></p>
<p class="btn"></p>
<p class="btn"></p>
<p class="btn"></p>
<p class="btn"></p>
<script>
var aBtns = document.getElementsByClassName('btn')
for(var i = 0; i < aBtns.length; i++){
console.log(i)
}
for(var i = 0, len = aBtns.length; i < len; i++){
console.log(i)
}
// Jsperf上查看运行速度
</script>
</body>
</html>
```
#### 采用最优循环方式
```javascript
// 采用最优循环方式
const arr = new Array(1, 2, 3, 4, 5)
arr.forEach(function(item){
console.log(item)
})
for(var i = arr.length; i; i--){
console.log(arr[i])
}
for(var i in arr){
console.log(arr[i])
}
// Jsperf中查看运行速度
```
#### 节点添加优化
- 节点添加操作必然会有回流和重绘
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>优化节点添加</title>
</head>
<body>
<script>
for(var i = 0; i < 100; i++){
var oP = document.createElement('p')
oP.innerHTML = i
document.body.appendChild(oP)
}
const fragEle = document.createDocumentFragment()
for(var i = 0; i < 100; i++){
var oP = document.createElement('p')
oP.innerHTML = i
fragEle.appendChild(oP)
}
document.body.appendChild(fragEle)
// Jsperf 查看运行速度
</script>
</body>
</html>
```
#### 克隆优化节点操作
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>克隆优化节点操作</title>
</head>
<body>
<p id="box1">old</p>
<script>
// 创建新节点时 : 先clone一个已有的节点, 再在clone节点上作修改
for(var i = 0; i< 5; i++){
var oP = document.createElement("p")
oP.innerHTML = i
document.body.appendChild(oP)
}
var oldP = document.getElementById('box1')
for(var i = 0; i< 5; i++){
var newP = oldP.cloneNode(false)
newP.innerHTML = i
document.body.appendChild(newP)
}
// Jsperf 查看运行速度
</script>
</body>
</html>
```
#### 直接量替换Object操作
```javascript
var a = [1, 2, 3]
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3
// Jsperf中查看运算速度
```
#### **JavaScript性能提升2** 空间换时间 或者 时间换空间
#### JSBench的使用(JSBench.me)
#### 堆栈中代码执行流程
```javascript
let a = 10
function foo(b){
let a = 2
function baz(c){
console.log(a+b+c)
}
return baz
}
let fn = foo(2)
fn(3)//=> 7 存在闭包 导致函数foo作用域没有被释放
// 减少判断层级
```
#### 减少判断层级
```javascript
// function doSome(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('请确认模块信息')
// }
// }
// doSome('ES2016', 6)
function doSome(part, chapter){
const parts = ['ES2016', '工程化', 'Vue', 'React', 'Node']
if(!part){
console.log('请确认模块信息')
return
}
if(!parts.includes(part)) return
console.log('属于前端课程')
if(chapter > 5){
console.log('需要提供Vip身份')
}
}
doSome('ES2016', 6)
```
#### 减少作用域链查找层级
```javascript
// var name = 'foo'
// function foo(){
// name = 'foo666'//全局的name变量
// function baz(){
// var age = 39
// console.log(age)
// console.log(name)
// }
// baz()
// }
// foo()
var name = 'foo'
function foo(){
var name = 'foo666'//全局的name变量
function baz(){
var age = 39
console.log(age)
console.log(name)
}
baz()
}
foo()
```
#### 减少数据读取次数
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>减少数据读取次数</title>
</head>
<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>
</html>
```
#### 字面量与构造式
```javascript
// let test = () => {
// let obj = new Object()
// obj.name = 'liuchao'
// obj.age = 39
// obj.slogan = '我为前端而活'
// return obj
// }
// let test = () => {
// let obj = {
// name : 'liuchao',
// age : 39,
// slogan : '我为前端而活'
// }
// return obj
// }
// console.log(test())
// ----------------------
var str1 = '我为前端而活'
var str2 = new String('我为前端而活')
console.log(str1)
console.log(str2)
```
#### 减少循环体活动
```javascript
// var test = () => {
// var i
// var arr = ['liuchao', 39, '我为前端而活']
// for( i =0;i<arr.length;i++){
// console.log(arr[i])
// }
// }
// var test = () => {
// var i
// var arr = ['liuchao', 39, '我为前端而活']
// var len = arr.length
// for( i =0;i<len;i++){
// console.log(arr[i])
// }
// }
var test = () => {
var arr = ['liuchao', 39, '我为前端而活']
var len = arr.length
while(len--){
console.log(arr[len])
}
}
test()
```
#### 减少声明及语句数--词法分析消耗时间
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>减少声明及语句数-词法分析消耗时间</title>
</head>
<body>
<div id="box" style="width: 100px;height: 200px;"></div>
<script>
var oBox = document.getElementById('box')
// var test = function (ele){
// let w = ele.offsetWidth
// let h = ele.offsetHeight
// return w * h
// }
// var test = function (ele){
// return ele.offsetWidth * ele.offsetHeight
// }
// console.log(test(oBox))
// var test = () => {
// var name = 'liucaho'
// var age = 39
// var slogan = '我为前端而活'
// return name + age + slogan
// }
var test = () => {
var name = 'liucaho',
age = 39,
slogan = '我为前端而活'
return name + age + slogan
}
console.log(test())
</script>
</body>
</html>
```
#### 惰性函数与性能
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>惰性函数与性能</title>
</head>
<body>
<button id="btn">点击</button>
<script>
var oBtn = document.getElementById('btn')
function foo(){
console.log(this)
}
// function addEvent (obj, type, fn){
// if(obj.addEventListerer){
// obj.addEventListerer(type, fn, false)
// }else if(obj.attachEvent){
// obj.attachEvent('on' + type, fn)
// }else{
// obj['on' + type ] = fn
// }
// }
function addEvent (obj, type, fn){
if(obj.addEventListerer){
addEvent = obj.addEventListerer(type, fn, false)
}else if(obj.attachEvent){
addEvent = obj.attachEvent('on' + type, fn)
}else{
addEvent = obj['on' + type ] = fn
}
return addEvent
}
addEvent(oBtn, 'click', foo)
</script>
</body>
</html>
```
#### 采用事件委托
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>使用事件委托</title>
</head>
<body>
<ul id="ul">
<li>liuchao</li>
<li>28</li>
<li>male</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, false)
function showTxt(ev){
var obj = ev.target
if(obj.nodeName.toLowerCase() === 'li'){
console.log(ev.target.innerHTML)
}
}
</script>
</body>
</html>
```