WebAssembly (Wasm) 作為一種新興的網頁技術,能將高效能語言如 Rust 帶到前端,解決 JavaScript 在處理密集運算時的效能瓶頸。本文以影像處理為例,示範如何使用 Rust 和 Wasm 建構高效率的前端應用。首先,透過 wasm-pack 工具,將 Rust 程式碼編譯成 Wasm 模組,並產生 JavaScript 繫結,方便前端整合。接著,利用 Webpack 封裝 Wasm 模組和前端程式碼,簡化開發流程。文章也涵蓋影像處理流程的說明,包含影像表示法、壓縮格式,以及如何在 Rust 和 JavaScript 間傳遞影像資料。最後,透過一個實際的影像縮放範例,展示如何使用 Rust 的 image 套件處理影像,並在前端使用 Canvas API 顯示結果,完整呈現 Wasm 在前端效能最佳化的應用。
使用 wasm-pack 建立高效能 Web 前端專案
建立專案前的準備
在開始建立專案之前,需要先安裝必要的工具。首先,需要安裝 npm(Node Package Manager),以便將專案封裝成 npm 套件,讓熟悉現代 JavaScript 開發的開發者能夠輕鬆使用。若您使用的是 Ubuntu,可以透過以下指令安裝 npm:
sudo apt update
sudo apt install npm
對於其他平台,請參考 https://nodejs.org/en/download 上的安裝。本章節使用的是 npm 版本 8.5.1,但較新的版本也應該可以正常運作。
建立專案
安裝完必要的工具後,可以開始建立專案。執行以下指令:
wasm-pack new hello-wasm
此指令會使用 cargo-generate 從 GitHub 下載 wasm-pack-template 並在本地建立專案。過程中會要求輸入專案名稱,可以輸入「hello-wasm」。完成後,會在目前的目錄下看到一個名為 hello-wasm 的資料夾。
專案結構與設定
在 hello-wasm 資料夾中,您會看到一個典型的 Cargo library 專案,包含 Cargo.toml 和 src/lib.rs。檢視 Cargo.toml 的內容,會發現它有一些特別的設定:
[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
[features]
default = ["console_error_panic_hook"]
[dependencies]
wasm-bindgen = "0.2.63"
console_error_panic_hook = { version = "0.1.6", optional = true }
wee_alloc = { version = "0.4.5", optional = true }
[dev-dependencies]
wasm-bindgen-test = "0.3.13"
[profile.release]
opt-level = "s"
這裡有幾個值得注意的設定:
crate-type設定為["cdylib", "rlib"],其中cdylib表示輸出為遵循 C FFI 約定的動態連結函式庫,而rlib則是為了執行單元測試而新增的。opt-level在[profile.release]中被設定為"s",表示對產生的程式碼進行大小最佳化。- 使用了
wee_alloc這個小型記憶體分配器,以減少產生的 Wasm 二進位檔大小。
程式碼解析
檢視 src/lib.rs 的內容,可以看到以下程式碼:
mod utils;
use wasm_bindgen::prelude::*;
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, hello-wasm!");
}
這段程式碼的主要功能是:
- 將 JavaScript 的
window.alert()函式暴露給 Rust/Wasm。 - 將一個名為
greet()的 Wasm 函式暴露給 JavaScript。 - 當 JavaScript 呼叫
greet()函式時,會呼叫alert()函式,在瀏覽器中顯示一個彈出訊息。
#### 內容解密:
- 使用
wasm_bindgen將 JavaScript 的alert函式繫結到 Rust/Wasm 中,讓 Rust/Wasm 可以呼叫這個函式。 - 使用
#[wasm_bindgen]屬性將greet()函式暴露給 JavaScript,讓 JavaScript 可以呼叫這個 Wasm 函式。 - 在
greet()函式中呼叫alert()函式,顯示一個彈出訊息。
資料表示轉換的複雜性
在 Wasm 的規範中,只能在 JavaScript 和 Wasm 之間傳遞整數和浮點數。因此,當需要傳遞字串時,需要使用一些技巧。wasm_bindgen 可以自動處理這種轉換,將 Rust 的 &str 編碼成整數陣列,傳遞給 JavaScript,然後再轉換回 JavaScript 的字串。
這是因為 Rust 的字串是 UTF-8 編碼,而 JavaScript 的字串是 UTF-16 編碼。整數陣列被用作一個簡單的位元組集合,可以在兩種語言之間傳遞。這只是資料表示轉換複雜性的其中一個例子,也說明瞭為什麼需要一個好的函式庫來處理這些轉換。
除錯與錯誤處理
當 Rust 程式碼發生 panic 時,在瀏覽器的控制檯中只會看到一個通用的 Wasm 錯誤訊息。為了獲得更多有用的除錯資訊,可以使用 console_error_panic_hook 功能,它會將 panic 的詳細資訊列印到瀏覽器的控制檯中。這個功能需要在 src/utils.rs 中進行初始化。
#### 內容解密:
- 使用
console_error_panic_hook功能來提供更好的除錯資訊。 - 在
src/utils.rs中提供了一個小函式來初始化這個功能。
高效能網頁前端開發:使用WebAssembly與Rust
建置Wasm套件
當你執行wasm-pack build指令時,wasm-pack會確保你具備正確的工具鏈(例如,使用rustup下載正確的編譯目標),並將你的程式碼編譯成Wasm。你將在pkg資料夾中看到輸出結果。wasm-pack會產生幾個檔案:
hello_wasm_bg.wasm:編譯後的Wasm二進位制檔案,包含你所暴露的Rust函式。hello_wasm.js:對Wasm函式的JavaScript包裝器,使傳遞值變得更容易。hello_wasm_bg.d.ts:TypeScript型別定義。如果你想使用TypeScript開發前端,這些定義會很有用。package.json:npm專案的元資料檔案。當你將套件釋出到npm時,這將很有用。README.md:對套件使用者的簡短介紹。如果您釋出此套件,它將顯示在npm網站上。
內容解密:
- Wasm二進位制檔案:這個檔案包含了Rust函式的實作,是編譯後的結果。
- JavaScript包裝器:使JavaScript能夠更容易地呼叫Wasm中的函式。
- TypeScript型別定義:對於使用TypeScript的前端開發者來說,這些定義提供了型別檢查的支援。
建立前端
現在你已經準備好了Wasm套件,但如何讓它在網頁上運作?由於Wasm尚不支援ECMAScript 6(ES6)的import陳述式,你需要執行fetch以下載.wasm檔案,然後呼叫WebAssembly.instantiateStreaming() Web API來例項化它。這很麻煩,並不符合npm樣式的工作流程。相反,你可以使用Webpack來簡化將Wasm套件匯入JavaScript應用程式的方式。
使用Webpack
Webpack是一個多功能的工具,用於封裝你的JavaScript檔案。它可以分析你的各種JavaScript檔案和從npm安裝的套件之間的依賴關係,並將它們封裝成一個單獨的.js檔案。這減少了下載多個JavaScript檔案的開銷,並降低了執行時缺少依賴的風險。
設定Webpack以支援Wasm
為了簡化設定,你可以使用另一個範本:create-wasm-app。這個範本建立了一個前端網頁專案,並組態了Webpack以支援Wasm。要根據此範本啟動一個專案,只需在命令列中,在hello-wasm資料夾內執行以下命令:
npm init wasm-app client
這將要求你安裝create-wasm-app;請批准安裝。然後,該命令將下載create-wasm-app範本,並在名為client的資料夾中建立專案。
修改前端程式碼
在client資料夾中,你可以找到一個index.html檔案,如下所示:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Hello wasm-pack!</title>
</head>
<body>
<noscript>
This page contains webassembly and javascript content,
please enable javascript in your browser.
</noscript>
<script src="./bootstrap.js"></script>
</body>
</html>
這個HTML頁面非常簡單。它包含了bootstrap.js檔案,該檔案非同步匯入了index.js檔案。
修改index.js
在index.js中,範本匯入了一個名為hello-wasm-pack的演示Wasm套件。但是你想使用剛剛在父目錄中建立的Wasm專案。你需要開啟package.json檔案並新增一個dependencies段:
{
"name": "create-wasm-app",
// ...
"dependencies": {
"hello-wasm": "file:../pkg"
},
"devDependencies": {
// Removed the hello-wasm-pack package
"webpack": "^4.29.3",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5",
// ...
}
}
內容解密:
create-wasm-app範本:簡化了建立前端專案和設定Webpack以支援Wasm的過程。index.html和bootstrap.js:基本的HTML頁面和匯入JavaScript檔案的指令碼。- 修改
package.json:新增對本地Wasm套件的依賴,使前端能夠使用它。
將 WebAssembly 整合到前端專案
在前面的章節中,我們已經成功建立了一個簡單的 Hello World 專案,將 Rust 編譯成 WebAssembly(Wasm)並在網頁中執行。本章節將進一步探討如何利用 Wasm 提升前端應用的效能,特別是在影像處理方面。
設定 Webpack 與安裝依賴
首先,我們需要在 package.json 中定義新的依賴套件 hello-wasm,並指定其路徑為 file:../pkg。這表示 hello-wasm 套件位於上層目錄的 pkg 資料夾中。同時,記得移除未使用的 hello-wasm-pack 示範套件。
"dependencies": {
"hello-wasm": "file:../pkg"
}
接下來,在 index.js 中匯入 hello-wasm 套件並呼叫其匯出的 greet 函式:
import * as wasm from "hello-wasm";
wasm.greet();
為了讓上述程式碼正常運作,我們需要安裝並設定 Webpack。執行以下指令安裝相關依賴:
npm install
npm run build
npm run start
這些指令會安裝必要的套件、建置專案,並啟動開發伺服器。開發伺服器會監控原始碼的變化,並在需要時重新建置和提供最新的程式碼。
內容解密:
- npm install:安裝
package.json中定義的所有依賴套件。 - npm run build:使用 Webpack 將原始碼封裝到
./dist資料夾中。 - npm run start:啟動
webpack-dev-server,提供即時的程式碼封裝和服務。
使用 WebAssembly 調整影像大小
現在我們已經有了一個簡單的 Hello World 專案,接下來要建立一個基本的影像處理工具,展示 Wasm 在效能敏感任務中的優勢。影像處理是前端應用中常見的效能瓶頸,而 Wasm 可以有效地提升這類別任務的效能。
影像表示法
電腦中表示影像最常見的方式是儲存每個畫素的顏色值。顏色可以透過不同強度的紅、綠、藍光組合而成。如果使用 8 位元整數表示每個顏色分量的強度,那麼總共可以表示 $2^8 \times 2^8 \times 2^8 = 1,677,216$ 種不同的顏色。
最簡單的影像儲存方式是使用三個 8 位元數字的三元組來表示每個畫素(總共 24 位元或 3 位元組)。然而,這種格式會佔用大量的儲存空間,因此通常會使用壓縮格式,如 PNG、JPEG 和 GIF。
影像處理流程
@startuml
skinparam backgroundColor #FEFEFE
skinparam componentStyle rectangle
title Rust 與 WebAssembly 高效能網頁前端影像處理
package "Rust 記憶體管理" {
package "所有權系統" {
component [Owner] as owner
component [Borrower &T] as borrow
component [Mutable &mut T] as mutborrow
}
package "生命週期" {
component [Lifetime 'a] as lifetime
component [Static 'static] as static_lt
}
package "智慧指標" {
component [Box<T>] as box
component [Rc<T>] as rc
component [Arc<T>] as arc
component [RefCell<T>] as refcell
}
}
package "記憶體區域" {
component [Stack] as stack
component [Heap] as heap
}
owner --> borrow : 不可變借用
owner --> mutborrow : 可變借用
owner --> lifetime : 生命週期標註
box --> heap : 堆積分配
rc --> heap : 引用計數
arc --> heap : 原子引用計數
stack --> owner : 棧上分配
note right of owner
每個值只有一個所有者
所有者離開作用域時值被釋放
end note
@enduml此圖示展示了基本的影像處理流程。
內容解密:
- 原始影像:輸入的影像檔案。
- 解壓縮:將壓縮的影像資料解壓縮成畫素資料。
- 畫素處理:進行影像處理操作,例如縮放、濾鏡等。
- 壓縮:將處理後的畫素資料壓縮成指定的格式。
- 輸出影像:最終輸出的影像檔案。
在本章中,我們學習瞭如何將 Rust 編譯成 WebAssembly,並與前端 JavaScript 專案整合,從而提升效能敏感任務的效能。我們首先建立了一個簡單的 Hello World 專案,接著探討瞭如何利用 Wasm 進行影像處理。未來,我們可以進一步探索更多 Wasm 的應用場景和最佳化技巧,以充分發揮其在前端開發中的潛力。
利用 Rust 與 WebAssembly 進行高效能網頁前端影像處理
本章節將探討如何結合 Rust 與 WebAssembly(Wasm)技術來實作高效能的網頁前端影像處理。我們將使用 Rust 的 image 套件來處理影像,並將其編譯為 Wasm 以在網頁中使用。
建立 Wasm 專案與加入影像處理套件
首先,我們需要建立一個新的 Wasm 專案。執行以下命令:
wasm-pack new wasm-image-processing
接著,在 wasm-image-processing 目錄下,執行以下命令以加入 image 套件:
cargo add image
確保 Cargo.toml 中的 edition 設定為 "2021",並檢查相依套件的版本是否正確。
定義 JavaScript API 與 Rust 函式
我們希望在 JavaScript 中能夠呼叫 Rust 函式來調整影像大小。首先,我們需要了解 image 套件中的 resize 函式簽章:
pub fn resize<I: GenericImageView>(
image: &I,
nwidth: u32,
nheight: u32,
filter: FilterType
) -> ImageBuffer<I::Pixel, Vec<<I::Pixel as Pixel>::Subpixel>>
where
I::Pixel: 'static,
<I::Pixel as Pixel>::Subpixel: 'static,
內容解密:
resize函式接受一個實作GenericImageView特徵的影像物件、新的寬度、新的高度,以及一個FilterType列舉值。- 它傳回一個
ImageBuffer物件,可以轉換為 JavaScript 可理解的影像格式。 FilterType列舉允許選擇縮放演算法,此處我們選擇NearestNeighbor以其簡單和快速著稱。
建立前端專案與處理影像
在 wasm-image-processing 目錄下,建立一個新的前端專案:
npm init wasm-app client
在 client/index.html 中加入以下 HTML 程式碼:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Cat image processor</title>
</head>
<body>
<noscript>
This page contains webassembly and javascript content,
please enable javascript in your browser.
</noscript>
<input type="file" name="image-upload" id="image-upload" value="">
<br>
<button id="shrink">Shrink</button>
<br>
<canvas id="preview"></canvas>
<script src="./bootstrap.js"></script>
</body>
</html>
內容解密:
<input type="file">:用於選擇本地影像檔案。<button id="shrink">Shrink</button>:點選後呼叫 Wasm 函式來縮小影像。<canvas id="preview">:用於顯示影像。
載入影像檔案到 Canvas
在 index.js 中加入以下程式碼以載入影像檔案到 <canvas>:
function setup(event) {
const fileInput = document.getElementById('image-upload')
fileInput.addEventListener('change', function(event) {
const file = event.target.files[0]
const imageUrl = window.URL.createObjectURL(file)
const image = new Image()
image.src = imageUrl
image.addEventListener('load', (loadEvent) => {
const canvas = document.getElementById('preview')
canvas.width = image.naturalWidth
canvas.height = image.naturalHeight
canvas.getContext('2d').drawImage(
image,
0,
0,
canvas.width,
canvas.height
)
})
})
}
內容解密:
- 使用
<input type="file">載入本地影像檔案。 - 將影像繪製到
<canvas>上,以準備後續的影像處理。
此圖示說明:
- 使用者首先上傳影像檔案。
- 影像被載入到
<canvas>元素中。 - 從
<canvas>中提取影像資料並傳遞給 Wasm 函式進行縮放。 - Wasm 函式處理完畢後,將縮放後的影像資料傳回給 JavaScript。
- 最終將縮放後的影像顯示在
<canvas>上。