本文由得物技術WWQ分享,原題“基于IM場景下的Wasm初探:提升Web應用性能”,下文進行了排版和內容優化。
1、什么是Wasm
Wasm,全稱 WebAssembly,官網描述是一種用于基于堆棧的虛擬機的二進制指令格式。Wasm被設計為一個可移植的目標,用于編譯C/C++/Rust等高級語言,支持在Web上部署客戶端和服務器應用程序。

簡單的來說,Wasm就是使用C/C++/Rust等語言編寫的代碼,經過編譯后得到匯編指令,再通過JavaScript相關API將文件加載到Web容器中(即運行在Web容器中的匯編代碼)。Wasm是一種可移植、體積小、加載快速的二進制格式,可以將各種編程語言的代碼編譯成Wasm模塊,這些模塊可以在現代瀏覽器中直接運行。尤其在涉及到GPU或CPU計算時優勢相對比較明顯。
技術交流:
2、Wasm有什么用
JavaScript是解釋型語言,相比于編譯型語言需要在運行時轉換,所以解釋型語言的執行速度要慢于編譯型語言。
編譯型語言和解釋型語言代碼執行的大致流程如下:

如上流程圖所示:解釋型語言每次執行都需要把源碼轉換一次才能執行,而轉換過程非常耗費時間和性能,所以在 JavaScript背景下,Web執行一些高性能應用是非常困難的,如視頻剪輯、3D游戲等。
Wasm具有緊湊的二進制格式,可以接近原生的性能運行,并為C/C++等語言提供一個編譯目標,以便它們可以在Web上運行。被設計為可以與JavaScript共存,允許兩者一起工作。在特定的業務場景下可以完美的彌補JavaScript的缺陷。
3、Wasm的優勢和限制
優勢:
1)性能優異:相比JavaScript代碼,Wasm使用節省內存,快速加載和解釋的二進制代碼,具備更快執行速度,它是直接在底層虛擬機中運行的。這使得Web應用程序可以更高效地處理復雜的計算任務,例如圖形渲染、物理模擬等;
2)跨平臺兼容:Wasm可以在幾乎所有現代瀏覽器中運行,兼容性可參考caniuse,無論是桌面還是移動設備。這意味著開發者可以使用各種編程語言來編寫Web應用程序,而不僅僅局限于JavaScript;
3)安全性:Wasm運行在沙箱環境中,提供了良好的安全性。使用了一系列安全措施,如內存隔離和沙箱限制,以防止惡意代碼對系統的攻擊;
4)模塊化:Wasm模塊可以作為獨立的組件進行開發和部署,開發者可以更好地管理和維護代碼庫。模塊化的設計也為將來的性能優化和增量更新提供了便利。
局限性:
1)生態系統不夠完善:盡管Wasm已經成為Web開發中的關鍵技術之一,但生態系統仍然不夠完善。Wasm的工具、框架和庫的數量遠不如JavaScript;
2)開發門檻較高:Wasm的開發門檻相對較高。Wasm需要使用一種新的語言來編寫,如C或C++等。這使得學習和使用Wasm的成本相對較高。尤其是在內存管理等方面會增加開發的復雜性;
3)與JavaScript集成問題:Wasm與JavaScript之間的集成問題是一個挑戰。開發人員需要解決如何在Web應用程序中同時使用Wasm和JavaScript的問題;
4)兼容性問題:雖然現代瀏覽器已經開始支持Wasm,但是在一些老舊的瀏覽器中可能存在兼容性問題,需要開發者進行額外的處理來確保代碼的兼容性。
4、Wasm工作原理
通過上述的編譯型語言和解釋型語言代碼執行的大致流程我們可以知道Wasm是不需要被解釋的,是由開發者提前編譯為WebAssembly二進制格式(如下圖所示)。

由于變量類型都是預知的,因此瀏覽器加載WebAssembly文件時,JavaScript引擎無須監測代碼。它可以簡單地將這段代碼的二進制格式編譯為機器碼。
從這個流程中我們也可以看出:如果將每種編程語言都直接編譯為機器碼的各個版本,這樣效率是不是更高呢?想法是好的,但實現過程確實復雜不堪的。由于瀏覽器是可以在若干不同的處理器(比如手機和平板等設備)上運行,因此為每個可能的處理器發布一個WebAssembly代碼的編譯后版本會很難做到。
我們可以通過替代方法即取得IR代碼。IR即為中間代碼(Intermediate Representation),它是編譯器中很重要的一種數據結構。編譯器在做完前端工作以后,首先就生成IR,并在此基礎上執行各種優化算法,最后再生成目標代碼。
可以簡化為如下流程:
編譯器將IR代碼轉換為一種專用字節碼并放入后綴為.wasm的文件中。此時Wasm文件中的字節碼還不是機器碼,它只是支持WebAssembly的瀏覽器能夠理解的一組虛擬指令。當加載到支持WebAssembly的瀏覽器中時,瀏覽器會驗證這個文件的合法性,然后這些字節碼會繼續編譯為瀏覽器所運行的設備上的機器碼。
更加詳情的原理和使用方式可以前往https://developer.mozilla.org/en ... avaScript_interface查閱。
5、Wasm應用場景
在Web開發中,可以使用Wasm來提高應用程序的性能。
以下是一些使用Wasm的常見場景:
- 1)高性能計算:如果應用程序需要進行大量的數值計算、圖像處理或者復雜的算法運算,可以將這部分代碼編譯成Wasm模塊,以提高計算性能;
- 2)游戲開發:Wasm可以用于創建高性能的HTML5游戲,通過將游戲邏輯編譯成Wasm模塊,可以實現更流暢的游戲體驗;
- 3)跨平臺應用:使用Wasm可以實現跨平臺的應用程序,無論是桌面還是移動設備,用戶都可以通過瀏覽器來訪問和使用;
- 4)移植現有代碼:如果已經有用其他編程語言編寫的代碼,可以通過將其編譯成Wasm模塊,將其集成到現有的Web應用程序中,而無需重寫整個應用程序。
6、Wasm的應用案例
1)設計工具Figma-Wasm文件大小為27.7M:

2)Google Earth-Wasm文件總計大小為192.M(支持各大瀏覽器的3D地圖,而且運行流暢):

3)B站-視頻處理和播放也有使用Wasm,Wasm文件大小為344kb:

4)跨平臺的OpenGL圖形引擎Magnum-Wasm文件大小為844kb:
7、得物的Wasm實踐
7.1準備
這里我們通過使用Rust + Wasm實現Wasm與JavaScript之間的數據調用,理解Rust和Wasm的交互過程。
使用Rust就需要做一些前置的環境配置,詳情的步驟可參考Rust官網:https://www.rust-lang.org/zh-CN/tools/install。
安裝wasm-pack,wasm-pack是一個構建、測試和發布Wasm的Rust CLI工具,我們將使用wasm-pack相關的命令來構建Wasm二進制內容。這有助于將代碼編譯為WebAssembly,并生成在瀏覽器中使用的正確包。
7.2Rust項目初始化
執行cargo new rust_wasm初始化Rust項目,自動生成配置文件Cargo.toml。
項目結構如下:
/Users/admin/RustroverProjects/rust_wasm
├── Cargo.lock
├── Cargo.toml
├── src
| └── lib.rs
└── target
├── CACHEDIR.TAG
└── debug
├── build
├── deps
├── examples
└── incremental
7.3配置包文件
我們可以在Cargo.toml文件中加上下列代碼并保存,保存之后Cargo會自動下載依賴。
具體是:
- 1)crate-type = ["cdylib"],表示編譯時候使用C標準的動態庫;
- 2)#[wasm_bindgen]是一個屬性宏,來自于wasm_bindgen這個crate,是一個簡化Rust WASM與JS之間交互的庫。
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = { version = "0.2.89", features = [] }
7.4編寫代碼
編寫代碼之前我們先明確Rust中crate包的概念,Rust中包管理系統將crate包分為二進制包(Binary)和庫包(Library)兩種,二者可以在同一個項目中同時存在。
二進制包:
- 1)main.rs是二進制項目的入口;
- 2)二進制項目可直接執行;
- 3)一個項目中二進制包可以有多個,所以在Cargo.toml中通過雙方括號標識 [[bin]]。
庫包:
- 1)lib.rs是庫包的入口;
- 2)庫項目不可直接執行,通常用來作為一個模塊被其他項目引用;
- 3)一個項目中庫包僅有1個,在Cargo.toml中通過單方括號標識 [lib]。
因為我們這里希望將 Wasm 轉為一個可以在JS項目中使用的模塊,所以需要使用庫包 lib.rs 的命名,代碼如下。
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub extern "C" fn rust_add(left: i32, right: i32) -> i32 {
println!("Hello from Rust!");
left + right
}
7.5執行編譯
這里我們要使用到wasm-pack,將上述的Rust代碼編譯為能夠被JS導入的模塊,根據wasm-pack提供的target方式可以指定構建的產物。
如截圖所示:

編譯過程效果:

編譯完成后,我們會發現根目錄下多了一個pkg/ 文件夾,里面就是我們的Wasm產物所在的npm包了。
目錄結構如下:
/Users/admin/RustroverProjects/rust_wasm/pkg
├── package.json
├── rust_wasm.d.ts
├── rust_wasm.js
├── rust_wasm_bg.wasm
└── rust_wasm_bg.wasm.d.ts
rust_wasm.d.ts文件內容:
/* tslint:disable */
/* eslint-disable */
/**
* @param {number} num
* @returns {string}
*/
export function msg_insert(num: number): string;
/**
* @param {number} left
* @param {number} right
* @returns {number}
*/
export function rust_add(left: number, right: number): number;
/**
*/
export function rust_thread(): void;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly msg_insert: (a: number, b: number) => void;
readonly rust_add: (a: number, b: number) => number;
readonly rust_thread: () => void;
readonly __wbindgen_add_to_stack_pointer: (a: number) => number;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {SyncInitInput} module
*
* @returns {InitOutput}
*/
export function initSync(module: SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {InitInput | Promise<InitInput>} module_or_path
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
wasm-pack打包不僅輸出一個ESM規范的模塊,而且還支持自動生成d.ts文件,對模塊的使用者非常友好。
如下:
7.6在前端項目中引入使用
'use client'
/*
* @Author: wangweiqiang
* @Date: 2024-06-18 17:03:34
* @LastEditors: wangweiqiang
* @LastEditTime: 2024-06-18 23:09:55
* @Description: app.tsx
*/
import Image from "next/image";
import { useCallback, useEffect, useState } from "react";
import init, * as rustLibrary from 'rust_wasm'
export default function Home() {
const [addResult, setAddResult] = useState<number | null>(null)
const [calculateTime, setCalculateTime] = useState<string>('')
const initRustLibrary = useCallback(() => {
init().then(() => {
const result = rustLibrary.rust_add(5, 6)
const timeStamp = rustLibrary.msg_insert(50000)
setCalculateTime(timeStamp)
setAddResult(result)
})
}, [])
useEffect(() => {
initRustLibrary()
}, [initRustLibrary]);
return (
<main className="flex min-h-screen flex-col items-center p-24">
{/* .... */}
<div className="mt-32 grid text-center lg:mb-0 lg:w-full lg:max-w-5xl lg:grid-cols-4 lg:text-left">
<div>
rust代碼計算結果:{addResult}
</div>
二分法方式{calculateTime}
</div>
</div>
</main>
);
}

7.7在IM場景下的性能比較
在IM場景下,聊天消息中核心的處理流程在于數據的排序、去重,大量的數據查找會非常耗時,在這里我們通過二分法的方式對Rust和JavaScript兩種實現方式的耗時進行一個簡單的對比。
Rust代碼如下:
use chrono::{DateTime, Utc};
use rand::Rng;
#[derive()]
#[allow(dead_code)]
struct Data {
content: String,
from: String,
head: String,
msg_id: String,
seq: i32,
sid: String,
topic: String,
ts: DateTime<Utc>,
}
impl Data {
fn new(
content: String,
from: String,
head: String,
msg_id: &str,
seq: i32,
sid: String,
topic: String,
ts: DateTime<Utc>,
) -> Self {
Data {
content,
from,
head,
msg_id: msg_id.to_string(),
seq,
sid,
topic,
ts,
}
}
}
// 獲取原始數據
fn get_origin_data(num: i32) -> Vec<Data> {
let mut data: Vec = vec![]; // 存儲數據的向量
.... // 創建 num 個數據
data
}
// 初始化結構體數據
fn init_struct_data(num: i32, text: &str) -> Data {
let mut rng = rand::thread_rng();
let content = format!("{}_{}", rng.gen_range(1000..=9999), text).to_string();
....
let ts = Utc::now();
Data::new(content, from, head, &msg_id.as_str(), seq, sid, topic, ts)
}
// 二分法插入
fn binary_insert(data: &mut Vec<Data>, new_data: Data) {
let _insert_pos = match data.binary_search_by_key(&new_data.seq, |d| d.seq) {
Ok(pos) => {
data[pos] = new_data;
pos
}
Err(pos) => {
data.insert(pos, new_data);
pos
}
};
}
#[wasm_bindgen]
pub extern "C" fn msg_insert(num: i32) -> String {
let mut data: Vec<Data> = get_origin_data(1000);
let test_mode = [num];
let start_time = Utc::now().naive_utc().timestamp_micros();
for test_num in 0..test_mode.len() {
for num in 0..test_mode[test_num] {
let data_list = init_struct_data(num, "test");
binary_insert(&mut data, data_list);
}
}
let duration = Utc::now().naive_utc().timestamp_micros() - start_time;
let result = format!("插入{}條數據執行耗時:{}微秒", num, duration);
result
}
數據對比分析:

可以看到:在數據量不大的場景下,Wasm的耗時是比純JavaScript長的,這是因為瀏覽器需要在VM容器中對 Wasm模塊進行實例化,這一部分會消耗相當的時間,導致性能不如純JavaScript的執行。但隨著運算規模變大,Wasm的優化越來越明顯。這是因為WebAssembly是一種低級別的二進制格式,經過高度優化,并且能夠更好地利用系統資源。相比之下,JavaScript是一種解釋性語言,性能可能會受到解釋器的限制。
8、本文小結
在大多數場景下我們都不需要用到WebAssembly。因為V8等JS引擎的優化帶來了巨大的性能提升,已經足夠讓JavaScript應對絕大多數的普通場景了,如果要做進一步優化密集計算任務時使用Web worker也都能解決掉。只有在以上的少數場景下,我們才需要做這種“二次提升”。
WebAssembly雖然有天然的優勢,但也有自己的局限性,在使用時我們也需要考慮多方面因素,例如生態、開發成本等等。不過我們依然可以持續關注WebAssembly的發展。
9、相關資料
[1] 一文讀懂前端技術演進:盤點Web前端20年的技術變遷史
[2] 新手入門貼:史上最全Web端即時通訊技術原理詳解
[3] Web端即時通訊技術盤點:短輪詢、Comet、Websocket、SSE
[4] 新手快速入門:WebSocket簡明教程
[5] WebSocket詳解(六):刨根問底WebSocket與Socket的關系
[6] WebSocket從入門到精通,半小時就夠!
[7] 搞懂現代Web端即時通訊技術一文就夠:WebSocket、socket.io、SSE
[8] 詳解Web端通信方式的演進:從Ajax、JSONP 到 SSE、Websocket
[9] 從理論到實踐,詳細對比Electron和Tauri的優劣
[10] 快速對比跨平臺框架Electron、Flutter、Tauri、React Native等
10、得物技術團隊其它文章
得物從0到1自研客服IM系統的技術實踐之路
得物自研客服IM中收發聊天消息背后的技術邏輯和思考實現
得物從零構建億級消息推送系統的送達穩定性監控體系技術實踐
得物基于Electron開發客服IM桌面端的技術實踐
得物自研移動端弱網診斷工具的技術實踐分享
得物移動端常見白屏問題優化(網絡優化篇)
(本文已同步發布于:http://www.52im.net/thread-4742-1-1.html)