Javascript性能优化

Javascript性能优化

GC常见算法

常用的内存回收(Garbage Collection)算法整理:

GC回收算法

代码优化

可以使用Jsperf来进行Javascript代码的性能测试和对比

https://jsperf.com/yif-global-variable

1. 慎用全局变量

  • 全局变量定义在全局执行上下文,是所有作用域链的顶端(根作用域)
  • 由于一直存在于根作用域,难以被GC(内存回收)清理,会一直存在知道程序退出
  • 容易被局部作用域的同名变量遮蔽或者污染

2. 将使用中无法避免的全局变量放入缓存

例如为了获取获取dom元素节点,在需要大量获取document节点的函数中缓存全局变量document可以增加性能

// 不使用对象缓存
function getBtnWithoutCache() {
    let btn1 = document.getElementById('btn1')
    let btn2 = document.getElementById('btn2')
    let btn3 = document.getElementById('btn3')
}

// 使用对象缓存
function getBtnWitCache() {
    let obj = document
    let btn1 = obj.getElementById('btn1')
    let btn2 = obj.getElementById('btn2')
    let btn3 = obj.getElementById('btn3')
}

其原理是通过缓存加快对document全局变量的访问速度,不需要每次都从根作用域重新查找

3. 通过原型链添加方法

原型链是所有对象共享的,比起在每个对象的this上增加方法,使用原型链的性能会更好

image-20200630155359315

4. 避免闭包陷阱

闭包会产生额外的对象引用,会使得本身应该被释放回收的对象由于闭包内的引用而无法被回收

function foo() {
  let el = document.getElementById('btn')
  el.onclick = function() {
    console.log(this.a)
  }
}

foo()

上面的el由于onclick函数的闭包会一直存在,使得即便btn节点已经从dom中被移除了也不会被回收,因为el一直对其进行引用

5. 避免属性访问方法

Javascript中没有传统面向对象语言的属性访问限制,一个对象里所有属性都是对外暴露的(这里没有讨论ES2015之后的class),在这种情况下,使用对象访问方法反而会降低性能

function Person() {
    this.name = 'Me'
    this.age = 18
    this.getAge = function() {
        return this.age
    }
}

上面的getAge就是一种属性访问方法,我们可以和直接的属性访问对比:

image-20200630162233394

可以发现直接访问性能明显更好,这也是符合直觉的

6. For循环优化

如果for循环的终止条件是一个定值,最好直接把它取出来缓存成一个变量

// 使用len变量来直接获取数组长度,避免重复访问
for (let i = 0, len = arr.length; i < len; i++) {
    console.log(i)
}

image-20200630164130926

再对比一下常用的四种for方法

https://jsperf.com/yif-for-loop-performance

  • forEach (ES5引入)
  • for
  • for in
  • for of (ES2015引入,只能遍历可迭代对象,如数组,不能直接遍历对象,可以遍历Object.values()或者Object.keys())

测试的数组

const arrList = []
for (let i = 0; i < 10000; i++) {
    arrList.push(i)
}

image-20200630180407937

可以发现forEach的速度是最快的(但这个结果或许并不准确,关于forEach和for的对比争论在stackoverflow上很多,两者其实在不同场景各有胜负,不过从语义角度而言,forEach作为比较新的语法表达更简洁易读,但要注意的是forEach不能使用break/continue来中断,而ES2015/ES6新加入的for of则可以)

7. 使用文档碎片优化节点添加

// 直接往body上添加节点元素
for (let i = 0; i < 10; i++) {
    let oP = document.createElement('p')
    oP.innerHTML = i
    document.body.appendChild(oP)
}

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

早期批量操作dom的开销比较大,使用Fragment可以很好的降低开销,因为是先把文档操作放到虚拟的Fragment上再一次性插入到document,Fragment就相当于占位符,在插入之后本身就会销毁

现代浏览器已经对这类批量文档操作做了优化,实际的运行速度上使用文档碎片并不会有很大的优势了,但是从语义上讲文档碎片依然是很好的选择,因为它表明了不需要马上对页面dom进行更新操作

7. 使用克隆节点代替创造节点

当需要插入新节点时,另一个优化的方法是使用cloneNode函数,复制一个已有的同类节点再进行更改

for (let i = 0; i < 3; i++) {
    let newP = oldP.cloneNode(false) // 参数表示是否进行深拷贝
    newP.innerHTML = i
    document.body.appendChild(newP)
}

cloneNode比使用createElement性能更好,尤其批量创造同类节点

8. 直接量替换new Object

// 使用new
let a1 = new Array(3)
a1[0] = 1
a1[1] = 2
a1[2] = 3

// 直接量
let a2 = [1, 2, 3]

这个非常好理解,使用直接量的性能更好


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!