大前端进阶-js性能优化

内存管理

内存由可读写单元组成,表示一片连续可操做的空间。在编程时,能够经过主动操做来申请,使用和释放可操做空间。内存管理指的就是主动操做过程,也就是申请内存,使用内存和释放内存。算法

// 申请内存
let str
// 使用内存
str = 'foo'
// 释放内存
str = null // 再也不引用,垃圾回收会自动回收内存

垃圾回收

当内存再也不被使用时,其会被视为垃圾,而后被释放(回收)。编程

在JavaScript中,垃圾回收是自动进行的。

如何判断垃圾内存?数组

  1. 对象再也不被引用。
  2. 对象不能从根上访问到。
“根”在js中,能够将根看做全局对象。不能从根上访问到指的就是不能从全局对象上经过某条路径找到,能够是直接挂载在全局对象上,也能够是间接挂载在全局对象上。
function fn(obj1, obj2) {
    obj1['next'] = obj2
    obj2['pre'] = obj1
    return {
        o1: obj1,
        o2: obj2
    }
}
const obj = fn()

上述代码的关系以下图所示,此时obj,obj1,obj2均可以从全局对象上找到,所以不能看成垃圾被回收。
微信图片_20200725155844.png
以下图所示,若是经过delete将obj的o1属性和obj2的prev属性删除,那么obj1就没法从全局对象上找到,此时obj1将会被看成垃圾回收。
微信图片_20200725155951.png浏览器

可达对象

可到对象指的是能访问到的对象,访问的方式能够是引用,也能够是经过做用域链查找到。
判断一个对象是不是可达对象的标准就是从根出发是否能够被找到。缓存

GC算法

GC能够理解为是垃圾回收机制的简写。算法也就指的是查找垃圾,回收垃圾的规则。
经常使用的GC算法包含如下几个:微信

  1. 引用计数
  2. 标记清除
  3. 标记整理
  4. 分代回收

引用计数算法

经过引用计数器设置内存的引用数,当内存的引用关系发生改变的时候修改引用数,当引用数为0的时候内存当即被回收。闭包

// {name: 'zs'}所在的空间是一块内存
// 此时obj1引用这块内存,因此引用计数器上记为1
let obj1= {name: 'zs'}
// obj2 一样引用了这块内存,因此引用计数器为2
let obj2 = obj1
// obj1 再也不引用这块内存,因此计数器变为1
obj1 = null
// obj2也再也不引用这块内存,此时计数器为0.这块内存会被看成垃圾回收
obj2 = null

算法优势:app

  • 发现垃圾时当即回收。
  • 最大程度减小程序暂停(垃圾回收时程序会被暂停,若是回收的速度快,那么暂停的时间也就越少)。

算法缺点:dom

  • 没法回收循环引用的对象。
function fn() {
    const obj1 = { name: 'zs' }
    const obj2 = { name: 'ls' }
    // 在方法执行完毕之后,obj1和obj2应该被看成垃圾被回收,可是因为其相互引用,此时引用计数器上不为0, 因此没法回收
    obj1['friend'] = obj2
    obj2['friend'] = obj1
}
fn()
  • 时间开销大(因为须要引用计数器,当引用计数器对象越大,每次修改引用数的时间越长)。

标记清除算法

标记清除算法将垃圾回收分为标记和删除阶段,其算法步骤以下:jsp

  1. 遍历全部对象,找到活动对象进行标记。
  2. 遍历全部对象,找到全部没有标记的对象并清除。

以下图所示,第一不找到全部活动对象,因为ABCDE能够经过全局对象找到,因此被标记,a1和b1不能经过全局对象找到,因此不会被标记。第二步,找到没有被标记的a1和b1,将其看成垃圾回收。
微信图片_20200725155851.png
与引用计数算法相比。
优势:

  • 能够回收循环引用的对象

缺点:

  • 回收后内存地址可能再也不连续,形成碎片化。

假设内存中有一段连续的内存空间ABCDEF,若是BCDE被标记为活动对象,AB和F没有被标记,那么AB,F会被看成垃圾回收。回收完成后,形成存在AB和F两个碎片内存能够被使用,其只能放入对应长度的数据。

标记整理算法

标记整理算法和标记清除算法相似,只是多了整理内存步骤。

  1. 遍历全部对象,找到活动对象进行标记。
  2. 遍历全部对象,整理标记的内存,而后找到全部没有标记的对象并清除。

经过整理,能够解决标记清除算法形成内存碎片化的问题。

V8引擎

V8是一款主流的JavaScript执行引擎,采用即时编译,内存有限制(64位1.5G,32位800M)。

垃圾回收策略

js中的数据分为原始数据和对象引用数据两种,其中原始数据是由语言自己去处理,因此此处的垃圾回收策略主要针对栈上的对象引用数据。
V8采起分代回收的策略,因为v8对内存大小有限制,因此其将内存分红新生代和老生代两种,不一样的生代采起不一样的垃圾回收策略。
V8主要采起的GC算法有以下:

  1. 分代回收
  2. 空间复制
  3. 标记清除
  4. 标记整理
  5. 标记增量

新生代

V8将内存分为两块,其中小的空间称为新生代(64位32M/32位16M),其主要存储存活时间较短的对象。新生代内部一样分为两个等大小的空间From和To,经过空间复制和标记整理两个算法完成垃圾回收。

  1. From为使用空间,To为空闲空间,活动对象存储在From。
  2. 标记整理后从From拷贝到To。
  3. 清理From,将From和To交换空间。

From到To的拷贝过程可能出发晋升,也就是重新生代拷贝到老生代,下面两种状况将出发晋升。

  1. 一轮GC以后还存活的新生代。
  2. To空间的使用率超过25%。

老生代

老生代指的是空间较大的内存块(64位1.4G,32位700M),其内部存储存活时间长的对象,采用标记清除,标记整理和增量标记三种算法实现垃圾回收。

  1. 首先采用标记清除进行垃圾回收(会遗留空间碎片)。
  2. 新生代向老生代拷贝而且老生代存储区不足的时候进行空间优化(标记整理)。
  3. 采用增量标记进行效率优化(js代码执行和垃圾回收互斥,执行垃圾回收时没法执行js代码,增量标记指的时将遍历对象进行标记的过程拆分红多个小的执行段,这样js代码执行和标记过程可交叉进行)。

内存问题

js代码在浏览器中执行的时候,可能出现的和内存相关的问题以下:

  • 内存泄露: 内存使用持续增长。
  • 内存膨胀: 内存使用短期内暴涨,超过内存限制。
  • 分离Dom: Dom节点没有在Dom树上,被变量引用致使没法回收。
  • 频繁GC: GC操做会暂停代码执行,频繁GC会使得页面卡顿。

代码优化

慎用全局变量

全局变量会致使的问题以下:

  • 全局变量存在于全局上下文,全局上下文是做用域链的顶端,当经过做用域链进行变量查找的时候,会延长查找时间。
  • 全局执行上下文会一直存在于上下文执行栈,直到程序推出,这样会影响GC垃圾回收。
  • 若是局部做用域中定义了同名变量,会遮蔽或者污染全局。

缓存全局变量

将不可避免的全局变量缓存到局部做用域中,减小查找时间,优化性能。适用于在局部做用域中频繁使用某个全局变量。

function query() {
    // 在局部做用域中直接使用全局的document变量,在执行时,局部做用域找不到该变量,会沿着做用域链向上查找直到在全局中找到
    return document.getElementsByTagName('input')
}

function query1() {
    // 经过将全局变量赋值给局部变量,那么查找时直接在局部做用域找到,不用再向上查找
    let dom = document
    return dom.getElementsByTagName('input')
}

经过原型新增方法

在为全部的实例对象添加共享方法的时候,经过原型定义比在构造函数中经过this定义性能更好。这是因为构造函数中this定义的方法在每一个实例中都会保存一份单独的引用,而经过原型定义,全部的实例会指向同一个引用。

function Person() {
    // 每一个实例对象都会保存一份say的引用,10个就会有10个内存引用
    this.say = function () {
        console.log(1)
    }
}
const zs = new Person()

function Person1() { }
// 全部实例的原型都指向一个内存引用,减小内存开销
Person1.prototype.say = function () {
    console.log(1)
}
const ls = new Person1()

避开闭包陷井

闭包是指在外部做用域中可使用内部做用域中的变量。

function foo() {
    let str = 'foo'
    return function () {
        console.log(str)
    }
}

let f = foo()
// f在外部执行的时候依然可以访问foo做用域中的str变量
f()

闭包是一种常见写法,能够解决js编程中的不少问题,可是因为内部做用域中的变量被外部引用,因此此变量不能被垃圾回收,若是使用不当很容易形成内存泄露,所以在编程中不能为了闭包而闭包

避免属性访问方法使用

js在编写类的时候,很容易的出如今类上提供一个方法,该方法用于访问类内部的一个属性。

function Person() {
    this.name = 'foo'
    // 为了便于控制,在属性的访问上添加了一层
    this.getName = function () {
        return this.name
    }
}
const zs = new Person()
console.log(zs.getName)

function Person1() {
    this.name = 'foo'
}
const ls = new Person1()
// 直接访问属性
console.log(ls.name)

经过jsperf测试,发现直接访问会比包装访问要快的多。所以抛开代码编写规范,单从执行速度上来说,直接访问更快。

for循环优化

let arr = Array(100).fill('foo')
// 每次循环都要获取数组长度
for (let i = 0; i < arr.length; i++) {
    console.log(i)
}
// 缓存数组长度,
for (let i = 0, len = arr.length; i < len; i++) {
    console.log(i)
}

缓存数组长度for循环执行速度要更快,特别适合很是大或者很是复杂的数组遍历。

选择最优的循环方式

let arr = Array(100).fill('foo')
arr.forEach(function (item) {
    console.log(item)
})

for (let i = 0, len = arr.length; i < len; i++) {
    console.log(i)
}

for (let i in arr) {
    console.log(arr[i])
}

经过jsperf工具发现,forEach的执行速度最快,所以在不影响功能的前提下,尽可能使用forEach可加快代码的执行速度。

节点添加优化

在日常的js代码编写过程当中,经常伴有Dom节点的添加,因为Dom节点添加操做经常伴有回流和重绘,这两个操做比较耗时,可使用文档碎片优化这种耗时操做。

for (let i = 0; i < 10; i++) {
    let p = document.createElement('p')
    document.body.append(p)
}

let fraEls = document.createDocumentFragment()
for (let i = 0; i < 10; i++) {
    let p = document.createElement('p')
    fraEls.append(p)
}
document.body.append(fraEls)
相关文章
相关标签/搜索