這是專門探索 JavaScript 及其構建組件系列的第 6 期。在識別和描述核心元素的過程當中,咱們還分享了構建 SessionStack 時使用的一些經驗法則 —— 這是一個輕量級的 JavaScript 應用程序,但必須強大且性能卓越,才能幫助用戶實時查看和重現其 Web 應用的缺陷。javascript
此次咱們將剖析 WebAssembly 的工做原理,更重要的是在性能方面分析它與 JavaScript 的差別:加載時間、執行速度、垃圾回收、內存使用狀況、平臺 API 調用、調試、多線程和可移植性。html
咱們構建 Web 應用程序的方式正處於革命的邊緣 —— 仍然是初級階段,但咱們對 Web 應用程序的見解正在發生變化。前端
WebAssembly(也叫做 wasm)是一種高效且底層的給 web 使用的字節碼。html5
WASM 讓你可以用 JavaScript 以外的語言(例如 C、C++、Rust 或其餘)編寫程序,而後將其(提早)編譯到 WebAssembly。java
其結果是 Web 應用程序加載和執行速度都很是快。android
爲了加載 JavaScript,瀏覽器必須加載全部文本形式的 .js
文件。ios
WebAssembly 在瀏覽器中加載速度更快,由於只需經過互聯網傳輸已編譯的 wasm 文件。而 wasm 是一種很是簡潔的二進制格式的底層類彙編語言。git
今天 Wasm 的運行速度只比本地代碼(native code)執行慢 20%。不管如何,這是一個驚人的結果。這是一種編譯到沙盒環境中的格式,而且在不少約束條件下運行,以確保它沒有或者很難有安全漏洞。與真正的本地代碼相比,速度損失很小。更重要的是,它將在將來更快。github
更好的是,它與瀏覽器無關 —— 目前全部主要引擎都增長了對 WebAssembly 的支持,而且執行時間相近。web
爲了理解 WebAssembly 與 JavaScript 相比執行得有多快,你應該首先閱讀咱們關於 JavaScript 引擎的文章。
咱們來看看大概看看 V8 中會發生什麼:
V8 的方法:延遲編譯
在左邊,咱們有一些 JavaScript 源代碼,包含 JavaScript 函數。首先須要解析它,以便將全部字符串轉換爲詞法標記(token)並生成抽象語法樹(AST)。AST 是 JavaScript 程序邏輯的內存表示。一旦生成了這種表示,V8 會直接跳到機器碼。過程基本上是遍歷語法樹,生成機器代碼,最後獲得編譯好的函數。沒有真正的嘗試來加速它。
如今,咱們來看看 V8 流水線在下一階段的功能:
V8 流水線設計。
此次咱們有了 TurboFan —— V8 的優化編譯器之一。隨着你的 JavaScript 應用的運行,大量代碼運行在 V8 中。TurboFan 能夠監控某些代碼是否運行緩慢,是否存在瓶頸和熱點來優化它們。它把這些代碼推到編譯器後端 —— 一個優化的 JIT,這個後端可爲那些消耗大部分 CPU 的函數建立更快的代碼。
它解決了上面的問題,但這裏的問題在於,分析並決定優化哪些代碼的過程也會消耗 CPU。這反過來又意味着更高的電池消耗,特別是在移動設備上。
好了,wasm 並不須要全部的這些 —— 它會被插入工做流中,以下所示:
V8 流水線設計 + WASM。
Wasm 在編譯階段就已經優化好。最重要的是,也再也不須要解析過程。你有了一個已優化的二進制文件,它能夠直接掛接到生成機器碼的編譯器後端。全部優化都在編譯器前端完成。
這讓執行 wasm 更有效率,由於流程中的不少步驟均可以簡單地跳過。
WebAssembly 可信和不可信狀態。
舉個例子,C++ 程序中的內存是一個連續的區塊,其中並無「空隙」。有助於提升安全性的 wasm 的特性之一是,執行棧與線性內存分離的概念。在 C++ 程序中,你有一個堆,你從底部分配堆內存,並從堆頂部獲取棧空間。這就有可能造出一個指向棧空間的指針來玩弄那些本不該該接觸到的變量。
這是不少惡意軟件所利用的缺陷。
WebAssembly 採用徹底不一樣的模型。執行棧與 WebAssembly 程序自己是分開的,所以你沒法修改棧變量等內容。並且,函數中使用整數偏移而不是指針。函數指向一個間接函數表。而後經過這些計算出的直接數字跳轉到模塊內部的函數中。這種設計方式使得你能夠加載多個 wasm 模塊,並排排列,平移全部的索引,互不影響。
有關 JavaScript 中內存模型和管理的更多信息,能夠查看咱們很是詳細的關於此主題的文章。
你已經知道 JavaScript 的內存管理是使用垃圾收集器處理的。
WebAssembly 的狀況有點不一樣。它支持手動管理內存的語言。你的 wasm 模塊能夠自帶 GC,但這是一項複雜的任務。
目前,WebAssembly 是圍繞 C++ 和 RUST 用例設計的。因爲 wasm 是很是底層的,所以只有彙編語言上一層的編程語言才易於編譯。C 可使用普通的 malloc,C++ 可使用智能指針,Rust 使用徹底不一樣的形式(徹底不一樣的主題)。這些語言不使用 GC,所以它們不須要那些複雜的運行時事務來跟蹤內存。WebAssembly 對他們來講是天做之合。
另外,這些語言並非 100% 被設計用於調用複雜的 JavaScript 事物,如操做 DOM。徹底在 C++ 中編寫 HTML 應用是沒有意義的,由於 C++ 不是爲它設計的。在大多數狀況下,當工程師編寫 C++ 或 Rust 時,他們的目標是 WebGL 或高度優化的庫(例如繁重的數學計算)。
可是,未來 WebAssembly 也將支持不附帶 GC(但須要垃圾回收)的語言。
取決於執行 JavaScript 的運行時,不一樣特定於平臺的 API 能夠經過 JavaScript 應用程序直接訪問。例如,若是在瀏覽器中運行 JavaScript,你能夠經過一系列 Web APIs 來控制 web 瀏覽器 / 設備的功能,而且可使用例如 DOM、CSSOM、WebGL、IndexedDB、Web Audio API 等等
好吧,WebAssembly 模塊沒法直接調用任何平臺 API。一切都是由 JavaScript 代理的。若是你想在 WebAssembly 模塊中調用的某些平臺特定的 API,則必須經過 JavaScript 調用它。
例如,若是你想用 console.log
,必須經過 JavaScript 調用它,而不是你的 C++ 代碼。這些 JavaScript 調用的成本會比較高。
也並不老是如此。規範將在將來爲平臺 API 提供 wasm 接口,而且你將可以在沒有 JavaScript 的狀況下發布應用程序。
當你壓縮 JavaScript 代碼時,須要一種正確調試它的方法。這就是源碼映射大顯身手地方。
基本上,源碼映射是一種將整合/壓縮文件映射回構建前狀態的方法。當你構建線上版時,壓縮和組合 JavaScript 文件時將生成一個包含原始文件信息的源碼映射。當你在生成的 JavaScript 中查詢某一行號和列號時,能夠在源碼映射中查找代碼的原始位置。
WebAssembly 目前不支持源碼映射,由於暫時沒有規範,但最終會有的(可能很快)。
當在 C++ 代碼中設置斷點時,你將看到 C++ 代碼而不是 WebAssembly。至少這是目標。
JavaScript 在單線程上運行。有不少方法能夠發揮事件循環和異步編程優點,詳見咱們關於該主題的文章。
JavaScript 也使用 Web Workers,但他們有一個很是具體的用例 —— 基本上,阻止主 UI 線程的任何重 CPU 計算均可以從 Web Worker 中受益。可是 Web Workers 沒法訪問 DOM。
WebAssembly 目前不支持多線程。可是將來可能會。Wasm 將會和本地線程更近(例如 C++ 型線程)。擁有「真實」的線程將在瀏覽器中創造出許多新的機會。固然,這也將打開更多濫用可能性的大門。
現在,JavaScript 幾乎能夠在任何地方運行,從瀏覽器到服務器端甚至嵌入式系統。
WebAssembly 設計目標是安全且可移植。就像 JavaScript 同樣。它將運行在支持 wasm 的每一個環境中(例如每一個瀏覽器)。
WebAssembly 具備與 Java Applets 初期嘗試實現的移植性相同的可移植性目標。
在 WebAssembly 的第一個版本中,主要關注 CPU 佔用大的計算(例如處理數學)。想到的最主流的用途是遊戲 —— 那裏有大量的像素操做。你可使用你習慣的 OpenGL 綁定在 C++ / Rust 中編寫應用,並將其編譯爲 wasm。它會在瀏覽器中運行。
看看這個(在 Firefox 中運行)—— s3.amazonaws.com/mozilla-gam…。它使用虛幻引擎。
另外一種使用 WebAssembly 可能有意義(性能方面)的場景是實現一些這是一個 CPU 密集型的庫。例如,一些圖像處理庫。
如前所述,因爲大多數處理步驟都是在編譯期間提早完成的,所以 wasm 能夠減小移動設備上的電池消耗(取決於引擎)。
未來,即便你實際上沒有編寫代碼,你也可使用 WASM 二進制文件。能夠在 NPM 中找到開始使用此方法的項目。
對於 DOM 操做和大量的平臺 API 操做,固然用 JavaScript 更好,由於它不會增長額外的開銷,而且具備原生的 API。
在 SessionStack,爲了編寫高度優化且高效的代碼,咱們不斷突破 JavaScript 性能的機極限。咱們的解決方案須要提供超快的性能,由於咱們不能阻礙客戶應用自己。將 SessionStack 集成到線上 Web 應用或網站後,它會開始記錄全部內容:全部 DOM 更改、用戶交互、JavaScript 異常、堆棧跟蹤、失敗的網絡請求和調試數據。全部這些都在你的線上環境中進行,但不會影響產品的任何體驗和性能。咱們須要大量優化咱們的代碼並儘量使其異步。
並且不僅是庫!當你在 SessionStack 中重放用戶會話時,咱們必須渲染在發生問題時用戶瀏覽器中發生的全部事件,而且必須重構整個狀態,容許你在會話時間線中來回跳轉。爲了作到這一點,咱們正在大量使用 JavaScript 提供的異步能力,由於缺乏更好的選擇。
藉助 WebAssembly,咱們可以將一些最繁重的處理和渲染交給更適合作這個工做的語言,同時將數據收集和 DOM 操做留給 JavaScript。
若是你想試試 SessionStack,能夠從這裏免費開始。免費版能夠提供 1000 會話 / 月。
資源:
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。