> 文档中心 > JavaScript性能优化

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. 标记清除

⑴. 原理

  • 核心思想: 分标记和清除两个阶段
  • 遍历所有对象找标记活动对象
  • 遍历所有对象,清除没有标记对象,并抹掉第一个阶段标的标记
  • 回收相应空间,将回收的空间加到空闲链表中,方便后面的程序申请空间使用

JavaScript性能优化

  • Global 中的引用: 会采用递归的方式遍历所有对象,并进行标记
  • a1 => b1 只是内部引用,并不会被标记(解决了引用计数的缺陷)

⑵. 优缺点

优点: 很好地解决了标记清除法中,存在的 对象循环引用 问题(局部作用域里面的内容无法被标记);

缺点:

  • 空间链表地址不连续(空间碎片化),不能进行空间最大化使用
  • 不会立即回收垃圾对象,清除的时候程序是停止工作的

空间碎片化: 由于当前回收的垃圾对象,在地址上是不连续的,这些对象被回收后,他们释放的空间分散在各个角落,并且因为空间大小原因,不便于后续使用

JavaScript性能优化

4. 标记整理

⑴. 原理

  • 标记整理可以看做标记清除的增强
  • 标记阶段的操作和标记清除一致
  • 清除阶段会先执行整理,移动对象位置

JavaScript性能优化JavaScript性能优化

⑵. 优缺点

  • 优点: 相较标记清除算法减少了碎片化空间
  • 缺点: 不会立即回收垃圾对象,清除的时候程序是停止工作的


三、V8 引擎的垃圾回收

⑴. 概念

  • V8 是一款主流的 JavaScript 执行引擎
  • V8 采用即时编译
  • V8 内存设限(64 位 不超过 1.5G)

⑵. 垃圾回收策略

  • 采用分代回收思想: 内存分为新生代、老生代针对不同对象,采用不同的操作
  • V8 中常见的 GC 算法: 分代回收、空间复制、标记清除、标记整理、标记增量

JavaScript性能优化

⑶. 回收新生代对象

内存分配:
JavaScript性能优化

  • V8 内存空间一分为二
  • 小空间用于储存新生代对象(32M | 16M)
  • 新生代对象指的是存活时间较短的对象

回收实现:

  • 复制算法 + 标记算法
  1. 新生代内存区分为两个等大小的空间
  2. 使用状态: From; 空间状态: To
  3. 活动对象储存于 Form 空间
  4. 标记整理后将活动对象拷贝至 To
  5. Form 与 To 交完完成空间释放

回收细节说明:

  • 拷贝过程中可能出现晋升
  • 晋升就是将新生代对象移动至老生代
  • 一轮 GC 还存活的新生代需要晋升
  • To 空间使用率超过 25%

⑷. 回收老生代对象

说明:

  • 老生代对象存放于右侧老生代区域
  • 老生代区域空间大小:64 位系统 1.4G,32 位 700M
  • 老生代对象指的就是存活时间较久的对象

回收实现:

  • 标记清除 + 标记整理 + 标记增量
  1. 首先使用标记清除,完成垃圾空间回收
  2. 采用标记整理进行空间优化
  3. 采用标记增量进行效率提升

同新生代对比:

  • 新生代区域垃圾回收使用空间换时间: 复制算法,每时每刻都有一个空置的空间,带来时间生的提升
  • 老生代区域垃圾回收使用时间换空间

⑸. 标记增量

将垃圾回收操作拆分,组合实现垃圾回收;实现垃圾回收与程序执行交替完成

JavaScript性能优化


四、performance 工具

1. 工具介绍

  • GC 的目的是为了实现内存空间的良性循环
  • 良心循环的基石是合理使用
  • 时刻关注才能确定是否合理
  • performance 提供了多种监控方式

JavaScript性能优化

2. 内存问题的体现

  • 页面出现延迟加载、经常暂停的情况
  • 页面持续性出现糟糕的性能
  • 页面的性能随时间延迟,越来越差

3. 监控内存的几种方式

界定内存问题的标准:

  • 内存泄漏: 内存使用持续升高
  • 内存膨胀: 在多数设备上都存在性能问题
  • 频繁垃圾回收: 通过内存变化图分析

监控内存的几种方式:

  • 浏览器任务管理器
  • Timeline 时序图记录
  • 堆快照查找分离 DOM
  • 判断是否存在频繁的垃圾回收

4. 浏览器任务管理器

JavaScript性能优化能够直观感受到内存占用的大小,如果内存一直增加没能得到释放的话就是存在内存泄漏的

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 的内存消耗情况
JavaScript性能优化

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>

内存快照:
JavaScript性能优化

活动对象的展示:
分离 DOM 在界面上不体现,但是会造成内存的浪费,通过堆快照可以定位分离 DOM
JavaScript性能优化

7. 判断是否存在频繁GC

为什么确定垃圾回收:

  • GC 工作时应用程序是停止的
  • 频繁且过长的 GC 会导致应用卡死
  • 用户使用中感知应用卡顿

判断频繁GC 的方法:

  • Timeline 中频繁的上升和下降
  • 任务管理器中数据频繁的增加减小

五、V8 工作流程

词法分析->语法分析->预解析->全量解析->预编译->编译->机器码
JavaScript性能优化


六、堆栈处理


七、闭包与垃圾回收


八、代码优化

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 性能更有利;(数值越大,性能越好;防止偶然性可以多测试几次)

JavaScript性能优化

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测试:

由此可见,提前将数据缓存,供多次调用有助于提升性能
JavaScript性能优化

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测试:

由此可见,减少访问层级,有助于提升性能
JavaScript性能优化

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测试:

由此可见,减少判断层级,有助于提升性能
JavaScript性能优化

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())


下一篇:前端脚手架工具