V8 引擎和 JavaScript 优化建议

V8 是谷歌用于编译 JavaScript 的引擎,Firefox 一样也有一个,叫 SpiderMonkey,它和 V8 有一些不一样,但整体颇为类似。咱们将在本篇文章中讨论 V8。git

V8 引擎的一些基础点:github

  • 用 C++ 语言实现,使用在 Chrome 浏览器和 Node.js 中(以及最新版的 Microsoft Edge)
  • 遵循 ECMA-262 标准

JavaScript 旅程

当咱们把压缩、混淆以及作了各类处理的 JavaScript 放到 V8 引擎中解析时,到底发生了些什么?浏览器

下图阐述了整个流程,接下来咱们会对其中的每一个步骤进行详细说明: bash

在本篇文章中,咱们将探讨 JavaScript 代码是如何被解析的,以及如何最大程度的优化 JavaScript 的编译效率。V8 里的优化编译器(又名 Turbofan)拿到 JavaScript 代码以后,会将其转化成高效率的机器码,所以,咱们能向其输入越多的代码,咱们的应用就会越快。附注一点,Chrome 里的解释器称做 Ignition。markdown

JavaScript 解析

整个过程当中的第一步是解析 JavaScript。首先探讨什么是解析。ide

解析有两个阶段:函数

  • Eager(全解析)- 当即解析全部的代码
  • Lazy(预解析)- 按需作最少的解析,剩下的留到后面

哪种方式更好则须要根据实际状况来决定。工具

下面来看一段代码。oop

// 变量声明会被当即解析
const a = 1;
const b = 2;

// 目前不须要的暂时不解析
function add(a, b) {
  return a + b;
}

// add 方法被执行到了,因此须要返回解析该方法
add(a, b);
复制代码

变量声明会被当即解析,函数则会被懒解析,但上述代码里紧接着就执行了 add(a, b),说明 add 方法是立刻就须要用到的,因此这种状况下,把 add 函数进行即时解析会更高效。性能

为了让 add 方法被当即解析,咱们能够这样作:

// 变量声明会被当即解析
const a = 1;
const b = 2;

// eager parse this too
var add = (function(a, b) {
  return a + b;
})();

// add 方法已经被解析过了,因此这段代码能够当即执行
add(a, b);
复制代码

这就是大多数模块被建立的过程。那么,当即解析会是高效 JavaScript 应用的最好方式吗?

咱们能够用 optimize-js 这个工具对公共库代码进行彻底的当即解析处理,好比对比较有名的 lodash 进行处理后,优化效果是很显著的:

  • 没有使用 optimize-js:11.86ms
  • 使用了 optimize-js:11.24ms

必须声明的是,该结果是在 Chrome 浏览器中获得的,其它环境的结果则没法保证:

若是您须要优化应用,必须在全部的环境中进行测试。

另外一个解析相关的建议是不要让函数嵌套:

// 糟糕的方式
function sumOfSquares(a, b) {
  // 这里将被反复懒解析
  function square(num) {
    return num * num;
  }

  return square(a) + square(b);
}
复制代码

改进后的方式以下:

function square(num) {
  return num * num;
}

// 好的方式
function sumOfSquares(a, b) {
  return square(a) + square(b);
}

sumOfSquares(a, b);
复制代码

上述示例中,square 方法只被懒解析了一次。

内联函数

Chrome 有时候会重写 JavaScript 代码,内联函数便是这样一种状况。

下面是一个代码示例:

const square = (x) => { return x * x }

const callFunction100Times = (func) => {
  for(let i = 100; i < 100; i++) {
    // func 参数会被调用100次
    func(2)
  }
}

callFunction100Times(square)
复制代码

上述代码会被 V8 引擎进行以下优化:

const square = (x) => { return x * x }

const callFunction100Times = (func) => {
  for(let i = 100; i < 100; i++) {
    // 函数被内联后就不会被持续调用了
    return x * x
  }
}

callFunction100Times(square)
复制代码

从上面能够看出,V8 实际上会把 square 函数体内联,以消除调用函数的步骤。这对提升代码的性能是颇有用处的。

内联函数问题

上述方法存在一点问题,让咱们看看下面这段代码:

const square = (x) => { return x * x }
const cube = (x) => { return x * x * x }

const callFunction100Times = (func) => {
  for(let i = 100; i < 100; i++) {
    // 函数被内联后就不会被持续调用了
    func(2)
  }
}

callFunction100Times(square)
callFunction100Times(cube)
复制代码

上面的代码中先会调用 square 函数100次,紧接着又会调用 cube 函数100次。在调用 cube 以前,咱们必须先对 callFunction100Times 进行反优化,由于咱们已经内联了 square 函数。在这个例子中,square 函数彷佛会比 cube 函数快,但实际上,由于反优化的这个步骤,使得整个执行过程变得更长了。

对象

谈到对象,V8 引擎底层有个类型系统能够区分它们:

单态

对象具备相同的键,这些键没有区别。

// 单态示例
const person = { name: 'John' }
const person2 = { name: 'Paul' }
复制代码

多态

对象有类似的结构,并存在一些细微的差异。

// 多态示例
const person = { name: 'John' }
const person2 = { name: 'Paul', age: 27 }
复制代码

复杂态

这两个对象彻底不一样,不能比较。

// 复杂态示例
const person = { name: 'John' }
const building = { rooms: ['cafe', 'meeting room A', 'meeting room B'], doors: 27 }
复制代码

如今咱们了解了 V8 里的不一样对象,接下来看看 V8 引擎是如何优化对象的。

隐藏类

隐藏类是 V8 区分对象的方式。

让咱们将这个过程分解一下。

首先声明一个对象:

const obj = { name: 'John'}
复制代码

V8 会为这个对象声明一个 classId。

const objClassId = ['name', 1]
复制代码

而后对象会按以下方式被建立:

const obj = {...objClassId, 'John'}
复制代码

而后当咱们获取对象里的 name 属性时:

obj.name
复制代码

V8 会作以下查找:

obj[getProp(obj[0], name)]
复制代码

这就是 V8 建立对象的过程,接下来看看如何优化对象以及重用 classId。

建立对象的建议

应该尽可能将属性放在构造器中声明,以保证对象的结构不变,从而让 V8 能够优化对象。

class Point {
  constructor(x,y) {
    this.x = x
    this.y = y
  }
}

const p1 = new Point(11, 22) // 隐藏的 classId 被建立
const p2 = new Point(33, 44)
复制代码

应该保证属性的顺序不变,以下面这个示例:

const obj = { a: 1 } // 隐藏的 classId 被建立
obj.b = 3

const obj2 = { b: 3 } // 另外一个隐藏的 classId 被建立
obj2.a = 1

// 这样会更好
const obj = { a: 1 } // 隐藏的 classId 被建立
obj.b = 3

const obj2 = { a: 1 } // 隐藏类被复用
obj2.b = 3
复制代码

其它的优化建议

接下来咱们看一下其它的 JavaScript 代码优化建议。

修正函数参数类型

当参数被传进函数中时,保证参数的类型一致是很重要的。若是参数的类型不一样,Turbofan 在尝试优化4次以后就会放弃。

下面是一个例子:

function add(x,y) {
  return x + y
}

add(1,2) // 单态
add('a', 'b') // 多态
add(true, false)
add({},{})
add([],[]) // 复杂态 - 在这个阶段, 已经尝试了4+次, 不会再作优化了

复制代码

另外一个建议是保证在全局做用域下声明类:

// 不要这样作
function createPoint(x, y) {
  class Point {
    constructor(x,y) {
      this.x = x
      this.y = y
    }
  }

  // 每次都会从新建立一个 point 对象
  return new Point(x,y)
}

function length(point) {
  //...
}
复制代码

结论

但愿你们学到了一些 V8 底层的知识,知道如何去编写更优的 JavaScript 代码。

相关文章
相关标签/搜索