WebAssembly 實戰 —— 前端提速的黑科技

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 的場景

 

 

參考資料:

《MDN: WebAssembly 概念》

《WebAssembly 中文文檔》 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章