JavaScript 二進位制的 AST

sunshine小小倩發表於2017-08-24

JavaScript 二進位制的 AST

在這個部落格文章中,我想介紹一下 JavaScript 二進位制 AST,我們希望在我們的專案中這將有助於使網頁載入更快,以及其他一些好處。

背景介紹

多年來,JavaScript 已經從最慢的指令碼語言之一,從老爺車發展為蘭博基尼,不管是通過 Web 瀏覽器還是其他環境。它都能夠快到可以執行桌面、伺服器、移動甚至嵌入式應用程式。

隨著 JavaScript 的增長,應用程式的複雜程度和規模都越來越複雜。然而,二十年前,少數使用過 JavaScript 的網站也就載入幾千位元組的 JavaScript,許多網站和非 Web 應用程式現在需要在使用者開始實際使用之前載入幾兆的 JavaScript 程式碼。

“幾兆的 JavaScript 程式碼”聽起來會很陌生,但是像 Steam 這樣的本地應用程式只有 3.1 兆(純二進位制,沒有資源,沒有除錯符號,沒有動態依賴,在我的 Mac 上測量的結果)。Telegram 是 11 兆,Opera 更新程式 是 5.8 兆。因為瀏覽器實際上是動態依賴構建的,所以我並沒算上 Web 瀏覽器的體積,但我估計Firefox 和 Chrome 有 100 餘兆的大小。

當然,大型 JavaScript 原始碼有幾個成本,包括:

  • 重型網路傳輸;
  • 慢速啟動。

我們現在已經能在很短的時間內解析 JavaScript 程式碼,在以前一個大型的 web 應用例如 Facebook 在一臺較好的電腦上通過 500ms-800ms 的時間編譯完成。幾乎沒有理由相信隨著時間的推移,JavaScript 應用程式會變得越來越小。

因此,Mozilla 和 Facebook 的一個聯合小組決定開始研究一種新的機制,我們相信通過二進位制 AST 執行 JavaScript 可以極大地提高應用程式的速度。

二進位制 AST 簡介

JavaScript 二進位制 AST 的思想很簡單:我們可以通過傳送二進位制而不是傳送文字源。

讓我來澄清一下:二進位制 AST 原始碼相當於文字的原始碼。並不是一個新的語言,也不是 JavaScript 的子集或超集,它 JavaScript。它不是一個位元組碼,而是原始碼的二進位制表示形式。如果您願意,這個二進位制 AST 就是一種專為JavaScript而設計的,併為瞭解析速度而優化過的原始碼。我們還在構建一個可以提供可讀的格式良好的原始碼解碼器。目前,這種形式並沒有保留註釋,但是有一個保留註釋的提議。

生成一個二進位制 AST 檔案需要一個構建過程,我們希望這個過程越快越好。像 WebPack 或者 Babel 這樣的構建工具會產生一個二進位制的 AST 檔案,因此,切換到二進位制 AST 就像向構建傳遞一個標誌一樣簡單,許多開發者已經開始使用。

我想在我未來部落格的文章中詳細介紹一些二進位制 AST 的標準和我們的現狀,現在,我來簡述一下,早期的實驗暗示我們可以能得到很好的源壓縮和可觀的解析速度。

我們已經研究二進位制 AST 幾個月了,現在專案已經作為 Stage 1 Proposal 被 ECMA TC-39 所接受。這是鼓舞人心的,但是還是需要一定的時間,你才能看到所有的 JavaScript 虛擬機器和工具鏈的實現。

對比一下

和壓縮格式對比

大部分的 web 伺服器在傳送 JavaScript 的時候已經使用了例如 gzip 或者 brotli 這樣的壓縮工具將 JavaScript 壓縮了。這大大減少了等待資料的時間。我們在這裡做的是一種專為 JavaScript 設計的格式。的確,在早期的原型內部使用 gzip,相比許多其他的技巧,我們早期的原型有兩個主要優勢:

  • 它使得解析速度更快;
  • 根據早期的實驗,我們大幅度擊敗了 gzip 或 brotli。請注意,我們的主要目的是使分析速度更快,因此在未來,如果我們需要在檔案大小和解析速度中做選擇,我們最有可能選擇更快的解析。另外,使用的壓縮格式的內部可能會改變。

和壓縮工具相比

web 開發者早期使用的用來減少 JS 檔案大小的傳統工具,例如 UglifyJS 和 Google’s Closure Compiler,這些工具稱為壓縮工具。

壓縮工具通常移除未使用的空格和註釋、修改變數然後縮短名稱,並使用一些其他轉換來使程式更短。

雖然這些工具確實有用,但它們有兩個主要缺點:

  • 它們並不試圖更快地進行解析 —— 事實上,我們已經目睹了在很多情況下,縮小意外使得解析更慢;
  • 它們有使 JavaScript 程式碼更難閱讀的副作用,包括重新命名不便於閱讀的變數和函式,使用奇怪的特徵將宣告的變數打包,等等。

相反,使用二進位制 AST 轉換:

  • 用於使解析更快;
  • 以易於解碼的方式保留了原始碼並容易閱讀所有變數名等。

當然,如果不希望保持原始碼可讀性的應用程式,混淆和二進位制 AST 轉換可以結合在一起。

和 WebAssembly 相對比

另一個令人興奮的旨在提升確定的效能的 web 技術是 WebAssembly(wasm)。wasm 是為了使本地的應用被編譯為一種格式,這種格式既可以有效地傳輸,也可以快速的解析,並通過 JavaScript 虛擬機器以本地速度執行。

然而,設計者的意圖是將 wasm 受限於原生程式碼,所以如果不是原生程式碼,JavaScript 將不起作用。

我不認為所有的 JavaScript 專案都可以通過 wasm 的編譯。雖然這是可行的,但這將會是一項相當冒險的專案,因為這至少和開發一個新的 JavaScript 虛擬機器的複雜度是相同的。同時還要確保仍然可以和 JavaScript 相容(這是一個非常棘手的語言,並且每年至少生成一次說明文件或擴充套件),當然,如果生成的程式碼比今天的 JavaScript 虛擬機器慢的話,這個任務就沒用了,JavaScript 虛擬機器現在越來越快了。並且如果編譯之後的程式碼執行速度過慢或者檔案太大,會使得啟動非常慢(這也是我們在這裡要解決的問題)或者使用編譯的 JavaScript 庫和(適用於瀏覽器應用程式的)DOM 導致無法工作。

現在,對這方面的探索絕對是一個有趣的工作,所以如果有人想證明我們錯了,無論如何,請這樣做:)

提高快取

當 JavaScript 程式碼被瀏覽器下載時,它被儲存在瀏覽器的快取中,以避免以後再下載它。Chromium 和 Firefox 近期更新了他們的瀏覽器使得不僅 JavaScript 原始檔可以快取,位元組碼也可以加入快取。因此,可以很好地解決頁面再次載入的解析時間問題。我不知道 Safari 和 Edge 在這方面的進展,所以他們可能也會有類似的技術。

恭喜 Chromium 和 Firefox,這些技術都很棒!事實上,他們很好地提高過載頁面的效能。這對於那些自從上次訪問 JavaScript 程式碼但是沒有更新的頁面非常有效。

我們試圖用二進位制 AST 解決的問題是不同的,雖然一些頁面是我們已經訪問過並且經常訪問的,但是還有更多的頁面是我們只訪問一次,哪怕是這個頁面近期已經更新過了但我們並沒有再訪問。特別是,越來越多的應用程式得到非常頻繁的更新 —— 例如,Facebook 每天傳送幾次新的 JavaScript 程式碼,並且 Twitter、LinkedIn、Google Docs 等情況也會類似。另外,如果你是一個 JS 的開發人員然後釋出一個 JavaScript 應用程式 —— 無論是 Web 應用程式還是其他程式,你總是希望你和使用者之間的第一次接觸儘可能平滑,這意味著你希望第一個載入(或更新後的第一次載入)也非常快。

這些問題我們都可以使用 二進位制 AST 解決。

假設

如果我們提高了快取會怎樣?

額外的技術是要使得瀏覽器提前抓取和預編譯 JS 程式碼和位元組碼。

這些技術確實值得研究,也將有助於我們開發二進位制 AST 指令碼 —— 每一種技術都改進了另一種技術。特別是,當使用這種技術時,二進位制AST的更好的資源效率將有助於限制資源浪費,同時也改善了這些技術根本不能使用的情況。

如果我們使用一個現有的 JS 位元組碼會怎樣?

大多數(要不就是所有的)JavaScript 虛擬機器已經使用一個內部的 JS 位元組碼。我似乎記得至少微軟的虛擬機器支援特殊的應用使用 JavaScript 位元組。

所以,你可以想象一下瀏覽器廠商將他們的位元組碼開源並且使所有的 JavaScript 應用使用位元組碼。這樣的話,聽起來不是一個好主意,有以下幾個原因:

第一:影響虛擬機器的開發者。一旦你暴露自己的內部表示的 JavaScript,你註定要維護它。事實證明,JavaScript 位元組碼經常變化,以適應新版本的語言或新的優化。強迫虛擬機器保持與舊版本的位元組碼的相容性將是一個維護和/或效能災難,所以我懷疑任何瀏覽器或 VM 供應商都願意提交這個,除非在非常有限的設定中。

第二:影響 JS 開發者。有幾個位元組碼就意味著維護和運送幾個二進位制,可能有幾十個,如果你想要優化後續版本的瀏覽器的位元組碼。更糟糕的是,這些位元組碼會有不同的語義,導致不同語義的 JS 程式碼編譯。雖然這是可能的,畢竟,移動和本地的開發者都是這樣做的,這就是在回退 JavaScript。

我們有一個標準的 JS 位元組碼會怎樣?

所以,如果 JavaScript 虛擬機器開發者決定想出一個新的位元組碼格式,可能作為一個擴充套件 WebAssembly,但專為 JavaScript 設計呢?

要明確一點:我聽到有人後悔沒有開發一個這樣的格式,但我不知道有人積極致力於此。

沒有人這樣做的原因是設計和維護一種隨時變化的語言的位元組碼是相當複雜的,對於一種像 JavaScript 這種已經很複雜的語言來說,將會更加複雜。最重要的是,持續編譯 JavaScript 和對 JavaScript 進行位元組很有可能會失敗。這將會產生兩個不相容的 JavaScript 語言,對於 web 有時非常不利。

此外,這樣的位元組碼實際上對程式碼的大小和效能是否有幫助,還有待論證。

我們只是讓解析器更快會怎樣?

那豈不是很好,如果我們可以讓解析器更快?不幸的是,雖然 JS 解析器有了很大改進,但改進的速度是在逐步放緩。

讓我引用幾個不能跳過或一直有效的步驟:

  • 處理外來編碼,標記 Unicode 位元組順序和其他細節;
  • 找出 / 字元,是一個除法操作或者一個註釋或正規表示式的開始;
  • 找出 ( 字元,是表示式的開始,一個函式呼叫的引數列表,箭頭函式的引數列表等;
  • 找出這個字串(分別是字串模板、陣列、函式等)在哪停止,這取決於所有模稜兩可的問題;
  • 找出 let a 宣告是否和其他的 let avar aconst a 宣告衝突,實際上可能在稍後的程式碼出現;
  • 當遇到使用 eval 時,決定使用 4 個語義中的哪一個;
  • 確定哪些是真正的本地變數;
  • 等等

理想情況下,虛擬機器開發者希望能夠並行解析,或延遲直到我們知道我們實際上使用的語法在進行解析。事實上,最近的虛擬機器實施這些戰略。遺憾的是,JavaScript 語法中大量的標記含糊性大大增加了併發性的機會,同時必須丟擲對語法錯誤的限制,從而限制了懶惰解析的機會。遺憾的是,JavaScript 語法中大量模稜兩可的標記大大增加了併發性的機會,同時必須丟擲對語法錯誤的限制,從而限制了懶惰解析的機會。

在任何情況下,虛擬機器都需要進行昂貴的預分析步驟,可卻往往適得其反,產生比常規的解析速度較慢,尤其是當應用在壓縮編碼時。

實際上,二進位制 AST 建議旨在克服文字源 JavaScript 的語法和語義所帶來的效能限制。

現在是什麼情況?

我們釋出這篇部落格因為我們想讓你 —— Web 開發人員或者工具開發商必須在儘可能早的瞭解二進位制的 AST。到目前為止,我們從兩組收集的反饋是非常好的,我們期待著與社群密切合作。

我們已經完成了一個早期的基準測試原型(因此不太實用),正在開發一個先進的原型,無論是對於工具還是 Firefox,但是我們還有幾個月的時間去做一些有用的事情。

我會在幾周內釋出更多的細節。

閱讀更多:


掘金翻譯計劃 是一個翻譯優質網際網路技術文章的社群,文章來源為 掘金 上的英文分享文章。內容覆蓋 AndroidiOSReact前端後端產品設計 等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章