Javascript 代码性能优化探索

前言

  • 本文从代码角度探索一些会影响到 js 代码性能的地方,并用实例进行说明
  • 如果需要运行本文的示例代码,请每次运行前务必刷新一下浏览器,否则会因为缓存影响准确性
  • 不同运行环境得到的测试结果可能会有出入,但不影响结论

作用域链对代码性能的影响

例一:

let val = 100
let arr = []
for (let i = 0; i < 10000; i++) {
  arr.push(parseInt(Math.random() * 100))
}

function fun () {
  for(var i = 0; i < arr.length; i++) {
    arr[i] += val
  }
}

console.time("eg1")
fun()
console.timeEnd("eg1")

例二:

let val = 100
let arr = []
for (let i = 0; i < 10000; i++) {
  arr.push(parseInt(Math.random() * 100))
}

function fun () {
  let a = arr
  let v = val
  for(var i = 0; i < a.length; i++) {
    a[i] += v
  }
}

console.time("eg2")
fun()
console.timeEnd("eg2")

经过八次性能测试,得到耗时数据如下(单位 ms,保留 4 位小数):

示例 测试 1 测试 2 测试 3 测试 4 测试 5 测试 6 测试 7 测试 8 平均
示例一 3.2978 1.9038 1.9460 2.7460 1.7109 3.7761 2.1069 2.7219 2.5262
示例二 0.8098 0.9179 1.2670 1.0791 0.7309 0.6979 0.8940 1.1311 0.9410

分析: 随着代码中作用域数量的增加,访问当前作用域以外的变量的时间也会增加,访问全局变量总是比访问局部变量要慢,因为需要遍历作用域链

建议: 尽量减少使用外层作用域中的变量,避免使用 with 语句

对象属性对性能的影响

例一:

let arr = []
let person = { name: 'john' }

function fun () {
  for (let i = 0; i < 10000; i++) {
    arr.push(person.name)
  }
}

console.time('eg1')
fun()
console.timeEnd('eg1')

例二:

let arr = []
let name = 'john'

function fun () {
  for (let i = 0; i < 10000; i++) {
    arr.push(name)
  }
}

console.time('eg2')
fun()
console.timeEnd('eg2')

经过八次性能测试,得到耗时数据如下(单位 ms,保留 4 位小数):

示例 测试 1 测试 2 测试 3 测试 4 测试 5 测试 6 测试 7 测试 8 平均
示例一 3.5778 3.0419 3.3750 4.1450 3.4670 3.5048 3.1711 3.7150 3.4997
示例二 1.7890 1.8078 1.7873 0.8750 0.9091 0.7719 1.7167 1.3950 1.3814

分析: 对象上的任何属性查找都要比访问变量或数组花费更长时间,因为要获取到对象的属性可能需要多次搜索,示例中第一次搜索要在 window 中找到 person,第二次搜索要从 person 中找到 name

建议: 需要多次用到的对象属性,应将其存储在局部变量

Array、Object 等构造函数对性能的影响

例一:

function fun () {
  for(var i = 0; i < 10000; i++) {
    let obj = new Object()
    obj.name = 'john'
  }
}

console.time("eg1")
fun()
console.timeEnd("eg1")

例二:

function fun () {
  for(var i = 0; i < 10000; i++) {
    let obj = { name: 'john' }
  }
}

console.time("eg2")
fun()
console.timeEnd("eg2")

经过八次性能测试,得到耗时数据如下(单位 ms,保留 4 位小数):

示例 测试 1 测试 2 测试 3 测试 4 测试 5 测试 6 测试 7 测试 8 平均
示例一 1.2849 1.1770 1.2751 1.3068 1.1960 1.1081 1.2700 1.4938 1.2640
示例二 0.4360 0.6108 0.5610 0.5949 0.6237 0.5590 0.6010 0.5458 0.5665

分析: 使用构造函数的方式创建新数组和对象在构造函数中的操作要比直接用字面量方式创建数组和对象复杂得多

建议: 使用字面量方式创建数组和对象

双重解释对性能的影响

双重解释是指一段 JS 代码需要被解析两次,比如下面三个例子就存在双重解释的情况

eval("alert('hello world')")
var sayHi = new Function("alert('hello world')")
setTimeout("alert('hello world')", 100)

下面使用 eval 来进行实验

例一:

let arr = []
function fun () {
  for(var i = 0; i < 10000; i++) {
    eval("arr[i] = i")
  }
}

console.time("eg1")
fun()
console.timeEnd("eg1")

例二:

let arr = []
function fun () {
  for(var i = 0; i < 10000; i++) {
    arr[i] = i
  }
}

console.time("eg2")
fun()
console.timeEnd("eg2")

经过八次性能测试,得到耗时数据如下(单位 ms,保留 5 位有效数字):

示例 测试 1 测试 2 测试 3 测试 4 测试 5 测试 6 测试 7 测试 8 平均
示例一 276.60 272.67 272.38 264.97 266.83 278.57 264.99 268.27 270.66
示例二 0.8239 0.8059 0.9020 0.9560 0.9899 0.8581 0.9211 0.9079 0.8956

分析: 例一中的代码在运行的同时必须新启动一个解析器来解析新的代码,实例化一个新的解析器有不容忽视的开销,故这种代码要比直接解析要慢

建议: 尽量避免使用 eval,尽量不用 Function 构造函数去创建函数,而对于 setTimeout 可以传入函数作为第一个参数

调用帧对性能的影响

调用帧一个最典型的应用就是递归,下面是使用递归的方式计算 Fibonacci 数列的两个例子,例一是一个普通递归,例二是一个尾递归

例一:

function Fibonacci (n) {
  if ( n <= 1 ) { return 1 }
  return Fibonacci(n - 1) + Fibonacci(n - 2)
}

Fibonacci(10) // 89
Fibonacci(100) // 堆栈溢出
Fibonacci(500) // 堆栈溢出

例二:

function Fibonacci2 (n , ac1 = 1 , ac2 = 1) {
  if( n <= 1 ) { return ac2 }
  return Fibonacci2 (n - 1, ac2, ac1 + ac2)
}

Fibonacci2(100) // 573147844013817200000
Fibonacci2(1000) // 7.0330367711422765e+208
Fibonacci2(10000) // Infinity

分析: 递归非常耗费内存,很容易发生“栈溢出”错误,例一中最多的一次帧调用记录达到了 2^(n-1) 次,而例二中使用了尾递归优化,每次递归都只有一次帧调用

建议: 函数可以使用尾调用优化,关于函数的尾调用优化可以参考 http://es6.ruanyifeng.com/#docs/function#尾调用优化

除特殊说明外本人博客均属原创,转载请注明出处:http://blog.johnhan.cn/blog_1035.html
鄂ICP备17018604号-1  鄂公网安备42060702000030号