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 代碼。

相關文章
相關標籤/搜索