【DailyENJS第12期】V8引擎和JavaScript優化技巧

DailyENJS 致力於翻譯優秀的前端英文技術文章,爲技術同窗帶來更好的技術視野。前端

V8是Google用來編譯JavaScript的引擎。Firefox擁有本身的名爲SpiderMonkey的引擎,與V8十分類似,但有所不一樣。咱們將在本文中討論V8引擎。git

有關V8引擎的一些事實:github

  • 用C ++編寫,並在Chrome和Node.js(以及Microsoft Edge的最新版本)中使用
  • 實現 ECMA-262 標準

那麼,當咱們發送要由V8引擎解析的JavaScript時(在將其縮小,醜化以後以及您對JavaScript代碼進行的其餘瘋狂處理以後),到底發生了什麼?web

我畫了下面這個圖,圖裏顯示了全部步驟,而後咱們將詳細討論每一個步驟:瀏覽器

在本文中,咱們將討論JavaScript代碼是如何解析的,以及如何儘量地讓你的 JavaScript 走到 Optimising Compiler。Optimizing Compiler(又名Turbofan)將咱們的JavaScript代碼轉換爲高性能機器代碼,所以,咱們能夠給它提供的代碼越多,咱們的應用程序就會越快。附帶說明一下,Chrome中的解釋器稱爲 Ignition。app

解析JavaScript

所以,咱們的JavaScript代碼第一步是對其進行解析。讓咱們討論一下究竟是什麼解析。ide

解析分爲兩種方式:函數

  • 非惰性(徹底解析)-這會當即解析每行
  • 懶惰(預先解析)-進行最少的工做,解析咱們須要的內容,其他部分留到之後

哪一個更好?性能

讓咱們看一些代碼。測試

// 當即解析
const a = 1;
const b = 2;

// 惰性解析,由於暫時用不到
function add(a, b) {
  return a + b;
}

// 由於使用到了,因此返回去 解析
add(a, b);
複製代碼

在這裏變量聲明將被當即解析了,可是咱們的函數將被延遲解析。在咱們添加 add(a,b) 以前,這是很棒的,可是由於咱們須要使用這個函數,所以當即解析 add 會更快。

爲了當即解析 add 函數,咱們能夠執行如下操做:

// 當即解析
const a = 1;
const b = 2;

// 當即解析
var add = (function(a, b) {
  return a + b;
})();

// 當咱們使用到這個函數的時候已經被解析了
add(a, b);
複製代碼

這就是你使用的大多數 module 的建立方式。那麼,有能夠解析出性能最佳的JavaScript應用程序的最佳方法嗎?

讓咱們看一下庫 optimize-js,它遍歷庫並當即解析全部代碼。若是咱們看看像lodash這樣的流行庫,這些優化是很棒的:

  • Without optimize-js: 11.86ms
  • With optimize-js: 11.24ms

可是必須考慮到,這是在Chrome瀏覽器環境中,在其餘環境中,可能會有所不一樣:

所以,對您的web應用進行這些優化時,重要的是要在將要運行應用的全部環境中進行測試。

另外一個解析技巧是不要將函數嵌套在其餘函數中:

// bad way
function sumOfSquares(a, b) {
  // this is lazily parsed over and over
  function square(num) {
    return num * num;
  }

  return square(a) + square(b);
}
複製代碼

更好的方法是:

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

// good way
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++) {
    // the func param will be called 100 times
    func(2)
  }
}

callFunction100Times(square)
複製代碼

上面的代碼將經過V8引擎進行優化,以下所示:

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

const callFunction100Times = (func) => {
  for(let i = 100; i < 100; i++) {
    // the function is inlined so we don't have
    // to keep calling func
    return x * x
  }
}

callFunction100Times(square)
複製代碼

從上面能夠看到,V8本質上刪除了咱們調用func的步驟,而是內聯了 square 的內通。這很是有用,由於它將提升咱們的代碼的性能。

函數內聯陷阱

這種方法有些陷阱,讓咱們看下面的代碼示例:

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

const callFunction100Times = (func) => {
  for(let i = 100; i < 100; i++) {
    // the function is inlined so we don't have
    // to keep calling func
    func(2)
  }
}

callFunction100Times(square)
callFunction100Times(cube)

複製代碼

所以,此次咱們調用 square 函數100次以後,而後調用 cube 函數100次。在調用 cube 以前,咱們必須首先對callFunction100Times 進行優化,由於咱們已內嵌了 square 函數主題。在這種狀況下,square 函數彷佛比 cube 函數快,可是事實是優化步驟使執行時間更長。

Objects

當涉及對象時,V8底層有用於區分你的的對象的類型系統:

Monomorphism

對象具備相同的鍵:

// mono example
const person = { name: 'John' }
const person2 = { name: 'Paul' }
複製代碼

Polymorphism

這些對象共享類似的結構,但有一些細微差異。

// poly example
const person = { name: 'John' }
const person2 = { name: 'Paul', age: 27 }
複製代碼

Megamorphism

對象徹底不一樣,沒法比較。

// mega example
const person = { name: 'John' }
const building = { rooms: ['cafe', 'meeting room A', 'meeting room B'], doors: 27 }
複製代碼

如今,咱們瞭解了V8中的不一樣對象,讓咱們看看V8如何優化咱們的對象。

隱藏類(Hidden classes)

隱藏類是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) // hidden classId created
const p2 = new Point(33, 44)
複製代碼

你應該使屬性順序保持不變,請使用如下示例:

const obj = { a: 1 } // hidden class created
obj.b = 3

const obj2 = { b: 3 } // another hidden class created
obj2.a = 1

// this would be better
const obj = { a: 1 } // hidden class created
obj.b = 3

const obj2 = { a: 1 } // hidden class is reused
obj2.b = 3
複製代碼

常規優化技巧

如今,讓咱們進入一些通用技巧,這些技巧將幫助您更好地優化JavaScript代碼。

修復函數參數類型

將參數傳遞給函數時,重要的是它們必須是同一類型。若是參數類型不一樣,Turbofan將在嘗試4次後放棄嘗試優化JavaScript。

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

add(1,2) // monomorphic
add('a', 'b') // polymorphic
add(true, false)
add({},{})
add([],[]) // megamorphic - at this stage, 4+ tries, no optimization will happen
複製代碼

另外一個技巧是確保在全局做用域內聲明類:

// don't do this
function createPoint(x, y) {
  class Point {
    constructor(x,y) {
      this.x = x
      this.y = y
    }
  }

  // new point object created every time
  return new Point(x,y)
}

function length(point) {
  //...
}
複製代碼

結論

我但願你瞭解了V8的工做原理以及如何編寫更好的優化JavaScript代碼的一些知識。

原文: alligator.io/js/v8-engin…

最後照舊是一個廣告貼,最近新開了一個分享技術的公衆號,歡迎你們關注👇(目前關注人數可憐🤕)

相關文章
相關標籤/搜索