JavaScript性能优化

142 阅读7分钟

概述

  • JavaScript语言的优化

内存管理

  • 内存:由可读写单元组成,表示一片可操作空间
  • 管理:人为操作一片空间的申请、使用和释放
  • 内存管理:开发者主动申请空间、使用空间、释放空间
  • 管理流程:申请-使用-释放

JavaScript中的内存管理

  • 申请内存空间

    let obj = []
    
  • 使用内存空间

    obj.name = 'lg'
    
  • 释放内存空间

    obj = null
    

垃圾回收与常见GC算法

  • 垃圾
    • JavaScript中内存管理是自动的
    • 对象不再被引用时是垃圾
    • 对象不能从根上访问到时是垃圾
  • 可达对象
    • 可以访问到的对象就是可达对象(引用、作用域链)

    • 可达的标准就是从根出发是否能够被找到

    • JavaScript中的根就可以理解为是全局变量对象

      let a = {name:'1'}
      let ali = a
      a = null
      //ali还在引用这个对象,1仍是可达的
      

      可达对象:

      graph TB
      A[全局变量]--> |obj| B[Object : o1,o2]
      B[Object : o1,o2] --> |o1| C[Object name:obj1]
      B[Object : o1,o2] --> |o2| D[Object name:obj2]
      C[Object name:obj1] --> |Next| D[Object name:obj2]
      D[Object name:obj2] --> |Prev| C[Object name:obj1]
      

GC算法

  • GC垃圾回收机制的简写
  • GC可以找到内存中的垃圾、并释放和回收空间
  • GC里的垃圾
    • 程序不再需要使用的对象
    • 程序中不能再访问到的对象
    • 例如:函数中定义的对象
  • 算法就是工作时查找和回收所遵循的规则

常见GC算法

1.引用计数

  • 核心思想:设置引用数,判断当前引用数是否为0

  • 引用计数器

    1. 引用关系改变时修改引用字

    2. 引用数字为0时立即回收

const user1 = {age:11}
const user2 = {age:22}
const user3 = {age:33}

const nameList = [user1.age,user2.age,user3.age]

function fn() {
    const num1 = 1
    const num2 = 2
}

fn()
//执行完num1\2就引用不到了
//user1\2\3还在被引用着
  • 优点:
  1. 发现垃圾时立即回收
  2. 最大限度减少程序暂停:当内存即将爆满时,引用计数立即找到那些引用计数为0的对象空间,对其进行释放
  • 缺点:
  1. 无法回收循环引用的对象

    function fn(){
        const o1 = {}
        const o2 = {}
    
        o1.name = o2
        o2.name = o1
        //在作用域内还有一个互相的指引关系,引用计数器的数值不为0
        //造成内存的浪费
        return 0
    }
    
    fn()
    
  2. 时间开销大:维护一个数值的变化

2.标记清除

  • 核心思想:分标记清除二个阶段完成

  • (递归)遍历所有对象找标记活动对象

  • (递归)遍历所有对象清除没有标记对象

  • 回收相应的空间:放在空闲列表上,方便之后的程序使用

  • 优点:

    解决循环中不可达对象无法收回的问题:回收的时候会直接找到没有引用标记的对象回收

  • 缺点:

    空间碎片化:地址不连续,申请地址空间大于、小于所拥有空间

3.标记整理

  • 标记整理时标记清除的增强

  • 标记阶段的操作和标记清除一致

  • 清除阶段会先执行整理,移动对象位置:能让地址产生连续

  • 优点:解决空间碎片化问题

  • 缺点:不会立即回收垃圾对象

  • 分代回收

V8引擎的垃圾回收

V8

  • 一款主流的JavaScript执行引擎
  • 采用即时编译:源码=>机器码
  • 内存有限制:64位不超过1.4g,有垃圾回收的机制
  • 基于分代回收思想实现垃圾回收
  • 内存分为新生代和老生代

垃圾回收策略

V8常用的GC算法

  • 分代回收思想:内存分为新生代、老生代,针对不同对象采用不同算法

  • 空间复制

  • 标记清除

  • 标记整理

  • 标记增量

新生代回收

新生代:存活时间较短的对象,例如,函数内变量对象

老生代:反之

V8内存分配

  • V8内存空间一分为二
  • 小空间用于存储新生代对象(32M | 16M)

算法实现

  • 回收过程采用复制算法 + 标记整理
  1. 新生代内存区分为二个等大小空间
  2. 使用空间为From,空闲空间为To
  3. 活动对象存储于From空间
  4. 标记整理后将活动对象拷贝至To
  5. From与To交换空间完成释放

注意:

  • 拷贝过程可能出现晋升:将新生代对象移动至老对象

    什么时候晋升:一轮GC还存活的新生代需要晋升

  • To空间的使用率超过25%

老生代回收

  • 存放在右侧老生代区域
  • 64位操作系统1.4G,32操作系统700M

算法实现

  • 采用标记清除 + 标记整理 + 增量标记算法
    1. 使用标记清除完成垃圾空间的回收
    2. 采用标记整理进行空间优化(晋升触发)
    3. 采用增量标记进行效率优化

对比:

  • 新生代区域垃圾回收使用空间换时间
  • 老生代区域垃圾回收不适合复制算法

采用增量标记进行效率优化:

※ 标记可以不一口气做完:直接可达和间接可达。找到第一层可达就可以停下去让程序执行一会儿,再让GV做二步执行操作……反复,最后清除

Performance工具

为什么使用Performance

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

使用步骤

内存问题的外在表现

  • 页面出现延迟加载或者经常性暂停
  • 页面持续性出现糟糕的性能
  • 页面的性能随着时间延长越来越差

监控内存的几种方式

界定内存问题标准

  1. 内存泄漏:内存使用持续升高

  2. 内存膨胀:在多数设备上都存在性能问题

  3. 频繁垃圾回收:通过内存变化图进行分析

监控方式

1.浏览器任务管理器

shift+esc

内存:DOM节点占用的内存,不变为好

JavaScript内存:小括号内不变为好

2.Timeline时序图记录

​ 下降:垃圾回收

3.对快照查找分离DOM

  • 界面元素存活在DOM树上

  • 垃圾对象时的DOM节点:节点从DOM树上脱离,也没有引用的DOM节点

  • 分离状态的DOM节点:从DOM树上分离,但是还在引用

清除分离DOM:当不使用节点时置null

  1. 判断是否存在频繁的垃圾回收

代码优化实例

代码优化介绍

精准测试JavaScript性能

  • 本质:采集大量的执行样本进行数学统计和分析

  • 基于Benchmark.js的jsperf.com

    Jsperf使用:

    • 填写详细的测试用例信息(title、slug)

    • 填写准备代码(DOM操作时经常使用)

    • 填写必要setup与teardown代码

    • 填写测试代码片段

慎用全局变量

  • 全局变量定义在全局执行上下文,是所有作用域链的顶端
  • 全局执行上下文一直存在于上下文执行栈,直到程序退出
  • 某个局部作用域出现了同名变量则会遮蔽或污染全局

缓存全局变量

  • 将使用中无法避免的全局变量缓存到局部

    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')
        }
    

通过原型新增方法

  • 在原型对象上新增实例对象需要的方法

    //通过构造函数添加 this
    var fn1 = function() {
      this.foo = function() {
        console.log(11111)
      }
    }
    
    let f1 = new fn1()
    
    //使用原型对象 prototype
    var fn2 = function() {}
    fn2.prototype.foo = function() {
      console.log(11111)
    }
    
    let f2 = new fn2()
    

避开闭包陷阱

  • 闭包特点:
  1. 外部具有指向内部的引用
  2. 在”外“部作用域访问”内“部作用域的数据
  • 闭包使用不当很容易出现内存泄漏

    function foo(){
        var el = document.getElementById('btn')
        el.onclick = function() {
            console.log(el.id)
        }
        el = null //解决,代码+DOM引用都消失了
    }
    foo()
    //内存会泄漏
    

避免属性访问方法使用

  • JS不需要属性的访问方法,所有属性都是外部可见的

  • 使用属性访问方法只会增加一层重定义,没有访问的控制力

    //访问方法
    function Person() {
      this.name = 'icoder'
      this.age = 18
      this.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循环优化

  • 提前获取遍历长度

    for (var i = arrList.length; i; i--) {
      console.log(arrList[i])
    }
    

采用最优循环方式

//forEach < for < forin
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])
}

节点添加优化

  • 节点的添加操作必然会有回流和重绘

  • 文档碎片添加节点

    const fragEle = document.createDocumentFragment()
        for (var i = 0; i < 10; i++) {
          var oP = document.createElement('p')
          oP.innerHTML = i 
          fragEle.appendChild(oP)
        }
    
    document.body.appendChild(fragEle)
    

克隆优化节点操作

var oldP = document.getElementById('box1')
    for (var i = 0; i < 3; i++) {
      var newP = oldP.cloneNode(false)
      newP.innerHTML = i 
      document.body.appendChild(newP)
    }

直接量替换Object操作

//直接量快
var a = [1, 2, 3]

//Object操作
var a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3

本文首发于我的GitHub博客,其它博客同步更新。