WebAssembly 早在 2015 年就開始萌芽,直到 2018 年纔得到各大瀏覽器的廣泛支持
從最初的狂熱到現在的冷靜,WebAssembly(簡稱 wasm )到底給前端開發帶來了什麼?
一、什麼是 WebAssembly
首先必須明確一點,wasm 不是一個新的框架或者庫,而是一種全新的語法,和 HTML、CSS、JavaScript 平級
但 wasm 的出現絕不是要讓它成爲一門新的編程語言(不需要手寫 .wasm 文件,所以不用想着取代 JavaScript)
而是被設計爲一個編譯目標(就像 windows 系統中的 .exe 文件),爲諸如 C、C++ 和 Rust 等語言提供一個高效的編譯目標
所以 WebAssembly 只是提供了在瀏覽器上 (or node.js) 運行非 JavaScript 編程語言的能力
事實上,大多數編寫 wasm 的開發人員並不是純前端工程師
二、實戰
WebAssembly 有兩種文件格式:.wasm 和 .wat
其中 .wasm 文件以二進制的可執行文件,是編譯後的結果
而 .wat 是一種文本格式(就像 .js 文件),可用於 debug,最終需要編譯爲 .wasm 文件
上文已經說過, WebAssembly 被設計爲一個編譯目標,所以不需要手寫 .wat 文件
而是通過編譯工具將 C / C++ / Rust 編譯爲 .wasm
// .wat 文件長這個樣子:
(module
(func $fac (param f64) (result f64)
local.get 0
f64.const 1
f64.lt
if (result f64)
f64.const 1
else
local.get 0
local.get 0
f64.const 1
f64.sub
call $fac
f64.mul
end)
(export "fac" (func $fac)))
1. Rust → WebAssembly
接下來就感受一下將 Rust 編譯爲 WebAssembly 的過程
首先需要安裝 Rust 環境,建議查看官方文檔的介紹,如果是 macOS 或者 Linux 可以直接運行這一行代碼:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
安裝結束後可以通過 cargo --version 檢查是否安裝成功
然後安裝構建工具 wasm-pack
cargo install wasm-pack
準備就緒,接下來在合適的目錄下創建項目:
cargo new --lib hello-wasm
這裏的 Cargo.toml 就相當於前端應用的 package.json,接下來將 Cargo.toml 改爲這樣:
[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2"
然後將 src/lib.rs 的內容改爲:
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fib_recursive(n: usize) -> usize {
if n <= 2 {
return 1;
} else {
return fib_recursive(n - 2) + fib_recursive(n - 1);
}
}
現在我們已經使用 Rust 實現了一個斐波拉契數列的累加函數 fib_recursive(雖然是通過未優化的遞歸實現的)
接下來在項目的根目錄下執行 wasm-pack build,生成 .wasm 文件
2. 在 node 環境中使用 wasm
上面已經編譯出了 .wasm 文件,可以直接讀取
// index.js
const fs = require('fs');
const src = new Uint8Array(fs.readFileSync('./pkg/hello_wasm_bg.wasm'));
WebAssembly.instantiate(src)
.then((result) => {
const { fib_recursive } = result.instance.exports;
console.log('fib_recursive(10) ====> ', fib_recursive(10));
})
.catch((e) => console.error(e));
接着在 node 環境中執行 index.js
3. 在 web 項目中使用 wasm
目前 <script type='module'> 或 ES6 import 語句還不支持 wasm
// 或者使用 vite-plugin-wasm 等插件
在 web 端可以通過 fetch 或 XMLHttpRequest 來加載 wasm
這裏以 React 項目爲例:
import React, { useEffect } from 'react';
// pkg 位於 public 目錄下
const WASM_PKG_PATH = '/pkg/hello_wasm_bg.wasm';
// 加載並實例化 wasm
function fetchAndInstantiate(url, importObject) {
return fetch(url)
.then((response) => response.arrayBuffer())
.then((bytes) => WebAssembly.instantiate(bytes, importObject))
.then((results) => results.instance);
}
const Demo = () => {
useEffect(() => {
fetchAndInstantiate(WASM_PKG_PATH).then((result) => {
const { fib_recursive } = result.exports;
console.log('fib_recursive(10) ====> ', fib_recursive(10));
});
}, []);
return <h1>Hello WebAssembly</h1>;
};
export default Demo;
4. 性能對比
接下來在 node 環境對比一下 js 與 wasm 的性能差異
// 測試函數, 對比 n = 40 的情況下函數執行的時間
export default function testing(fn, n = 40) {
const timer = 'testing';
console.time(timer);
const result = typeof fn === 'function' && fn(n);
console.log('計算結果:', result);
console.timeEnd(timer);
}
先用 js 實現上面的斐波那契數列累加函數(依然是未優化的遞歸版本)
// js-fib.js
function jsFibRecursive(n) {
return n <= 2 ? 1 : jsFibRecursive(n - 2) + jsFibRecursive(n - 1);
}
testing(jsFibRecursive);
然後改爲 rust 編譯的 wasm 版本
// rust-fib.js
const fs = require('fs');
const src = new Uint8Array(fs.readFileSync('./pkg/hello_wasm_bg.wasm'));
WebAssembly.instantiate(src)
.then((result) => {
const { fib_recursive } = result.instance.exports;
testing(fib_recursive);
})
.catch((e) => console.error(e));
可以看到,同樣的邏輯,wasm 的耗時只有 js 版本的 25% 左右
這個數據在不同的環境下會有所差異,但 wasm 更快已是不爭的事實
以上的實戰代碼用到了 JavaScript 中內置的全局對象 WebAssembly
這是 js 與 wasm 交互的膠水層,詳細介紹可以查看 MDN 文檔
三、應用前景
通過以上內容可以看出,wasm 的優點在於一個字——快!
但這僅限於 wasm 的沙箱之內,而 wasm 與 js 的交互相當耗時,所以在使用的時候應當注意:
儘可能將純計算邏輯限定在 wasm 內部,應該儘量減少 js 與 wasm 的來回調用(所以業務代碼不適合也不應該編譯爲 wasm)
概括的說,wasm 的應用場景有以下兩個方面:
1. 複雜的計算可以使用 wasm 來提高性能
比如: 視頻/音樂編輯、遊戲引擎、AutoCAD、Figma
2. 把一些 C++ / Rust 寫的 native 庫移植到瀏覽器裏來增強瀏覽器的能力
這是目前最適合使用 wasm 的場景
參考資料: