本文由得物技術(shù)WWQ分享祷舀,原題“基于IM場(chǎng)景下的Wasm初探:提升Web應(yīng)用性能”,下文進(jìn)行了排版和內(nèi)容優(yōu)化烹笔。
1裳扯、什么是Wasm
Wasm,全稱?WebAssembly谤职,官網(wǎng)描述是一種用于基于堆棧的虛擬機(jī)的二進(jìn)制指令格式饰豺。Wasm被設(shè)計(jì)為一個(gè)可移植的目標(biāo),用于編譯C/C++/Rust等高級(jí)語言允蜈,支持在Web上部署客戶端和服務(wù)器應(yīng)用程序冤吨。
簡(jiǎn)單的來說,Wasm就是使用C/C++/Rust等語言編寫的代碼饶套,經(jīng)過編譯后得到匯編指令漩蟆,再通過JavaScript相關(guān)API將文件加載到Web容器中(即運(yùn)行在Web容器中的匯編代碼)。Wasm是一種可移植妓蛮、體積小爆安、加載快速的二進(jìn)制格式,可以將各種編程語言的代碼編譯成Wasm模塊,這些模塊可以在現(xiàn)代瀏覽器中直接運(yùn)行扔仓。尤其在涉及到GPU或CPU計(jì)算時(shí)優(yōu)勢(shì)相對(duì)比較明顯褐奥。
2、Wasm有什么用
JavaScript是解釋型語言翘簇,相比于編譯型語言需要在運(yùn)行時(shí)轉(zhuǎn)換撬码,所以解釋型語言的執(zhí)行速度要慢于編譯型語言。
編譯型語言和解釋型語言代碼執(zhí)行的大致流程如下:
如上流程圖所示:解釋型語言每次執(zhí)行都需要把源碼轉(zhuǎn)換一次才能執(zhí)行版保,而轉(zhuǎn)換過程非常耗費(fèi)時(shí)間和性能呜笑,所以在 JavaScript背景下,Web執(zhí)行一些高性能應(yīng)用是非常困難的彻犁,如視頻剪輯叫胁、3D游戲等。
Wasm具有緊湊的二進(jìn)制格式汞幢,可以接近原生的性能運(yùn)行驼鹅,并為C/C++等語言提供一個(gè)編譯目標(biāo),以便它們可以在Web上運(yùn)行森篷。被設(shè)計(jì)為可以與JavaScript共存输钩,允許兩者一起工作。在特定的業(yè)務(wù)場(chǎng)景下可以完美的彌補(bǔ)JavaScript的缺陷仲智。
3买乃、Wasm的優(yōu)勢(shì)和限制
優(yōu)勢(shì):
1)性能優(yōu)異:相比JavaScript代碼,Wasm使用節(jié)省內(nèi)存钓辆,快速加載和解釋的二進(jìn)制代碼剪验,具備更快執(zhí)行速度,它是直接在底層虛擬機(jī)中運(yùn)行的前联。這使得Web應(yīng)用程序可以更高效地處理復(fù)雜的計(jì)算任務(wù)功戚,例如圖形渲染、物理模擬等宋税;
2)跨平臺(tái)兼容:Wasm可以在幾乎所有現(xiàn)代瀏覽器中運(yùn)行轧膘,兼容性可參考caniuse,無論是桌面還是移動(dòng)設(shè)備。這意味著開發(fā)者可以使用各種編程語言來編寫Web應(yīng)用程序绎秒,而不僅僅局限于JavaScript;
3)安全性:Wasm運(yùn)行在沙箱環(huán)境中汗盘,提供了良好的安全性矩欠。使用了一系列安全措施,如內(nèi)存隔離和沙箱限制幌羞,以防止惡意代碼對(duì)系統(tǒng)的攻擊寸谜;
4)模塊化:Wasm模塊可以作為獨(dú)立的組件進(jìn)行開發(fā)和部署,開發(fā)者可以更好地管理和維護(hù)代碼庫属桦。模塊化的設(shè)計(jì)也為將來的性能優(yōu)化和增量更新提供了便利熊痴。
局限性:
1)生態(tài)系統(tǒng)不夠完善:盡管Wasm已經(jīng)成為Web開發(fā)中的關(guān)鍵技術(shù)之一他爸,但生態(tài)系統(tǒng)仍然不夠完善。Wasm的工具果善、框架和庫的數(shù)量遠(yuǎn)不如JavaScript诊笤;
2)開發(fā)門檻較高:Wasm的開發(fā)門檻相對(duì)較高。Wasm需要使用一種新的語言來編寫巾陕,如C或C++等讨跟。這使得學(xué)習(xí)和使用Wasm的成本相對(duì)較高。尤其是在內(nèi)存管理等方面會(huì)增加開發(fā)的復(fù)雜性鄙煤;
3)與JavaScript集成問題:Wasm與JavaScript之間的集成問題是一個(gè)挑戰(zhàn)晾匠。開發(fā)人員需要解決如何在Web應(yīng)用程序中同時(shí)使用Wasm和JavaScript的問題;
4)兼容性問題:雖然現(xiàn)代瀏覽器已經(jīng)開始支持Wasm梯刚,但是在一些老舊的瀏覽器中可能存在兼容性問題凉馆,需要開發(fā)者進(jìn)行額外的處理來確保代碼的兼容性。
4乾巧、Wasm工作原理
通過上述的編譯型語言和解釋型語言代碼執(zhí)行的大致流程我們可以知道Wasm是不需要被解釋的句喜,是由開發(fā)者提前編譯為WebAssembly二進(jìn)制格式(如下圖所示)。
由于變量類型都是預(yù)知的沟于,因此瀏覽器加載WebAssembly文件時(shí)咳胃,JavaScript引擎無須監(jiān)測(cè)代碼。它可以簡(jiǎn)單地將這段代碼的二進(jìn)制格式編譯為機(jī)器碼旷太。
從這個(gè)流程中我們也可以看出:如果將每種編程語言都直接編譯為機(jī)器碼的各個(gè)版本展懈,這樣效率是不是更高呢?想法是好的供璧,但實(shí)現(xiàn)過程確實(shí)復(fù)雜不堪的存崖。由于瀏覽器是可以在若干不同的處理器(比如手機(jī)和平板等設(shè)備)上運(yùn)行,因此為每個(gè)可能的處理器發(fā)布一個(gè)WebAssembly代碼的編譯后版本會(huì)很難做到睡毒。
我們可以通過替代方法即取得IR代碼来惧。IR即為中間代碼(Intermediate Representation),它是編譯器中很重要的一種數(shù)據(jù)結(jié)構(gòu)演顾。編譯器在做完前端工作以后供搀,首先就生成IR,并在此基礎(chǔ)上執(zhí)行各種優(yōu)化算法钠至,最后再生成目標(biāo)代碼葛虐。
可以簡(jiǎn)化為如下流程:
編譯器將IR代碼轉(zhuǎn)換為一種專用字節(jié)碼并放入后綴為.wasm的文件中。此時(shí)Wasm文件中的字節(jié)碼還不是機(jī)器碼棉钧,它只是支持WebAssembly的瀏覽器能夠理解的一組虛擬指令屿脐。當(dāng)加載到支持WebAssembly的瀏覽器中時(shí),瀏覽器會(huì)驗(yàn)證這個(gè)文件的合法性,然后這些字節(jié)碼會(huì)繼續(xù)編譯為瀏覽器所運(yùn)行的設(shè)備上的機(jī)器碼的诵。
更加詳情的原理和使用方式可以前往https://developer.mozilla.org/en ... avaScript_interface查閱万栅。
5、Wasm應(yīng)用場(chǎng)景
在Web開發(fā)中奢驯,可以使用Wasm來提高應(yīng)用程序的性能申钩。
以下是一些使用Wasm的常見場(chǎng)景:
1)高性能計(jì)算:如果應(yīng)用程序需要進(jìn)行大量的數(shù)值計(jì)算、圖像處理或者復(fù)雜的算法運(yùn)算瘪阁,可以將這部分代碼編譯成Wasm模塊撒遣,以提高計(jì)算性能;
2)游戲開發(fā):Wasm可以用于創(chuàng)建高性能的HTML5游戲管跺,通過將游戲邏輯編譯成Wasm模塊义黎,可以實(shí)現(xiàn)更流暢的游戲體驗(yàn);
3)跨平臺(tái)應(yīng)用:使用Wasm可以實(shí)現(xiàn)跨平臺(tái)的應(yīng)用程序豁跑,無論是桌面還是移動(dòng)設(shè)備廉涕,用戶都可以通過瀏覽器來訪問和使用;
4)移植現(xiàn)有代碼:如果已經(jīng)有用其他編程語言編寫的代碼艇拍,可以通過將其編譯成Wasm模塊狐蜕,將其集成到現(xiàn)有的Web應(yīng)用程序中,而無需重寫整個(gè)應(yīng)用程序卸夕。
6层释、Wasm的應(yīng)用案例
1)設(shè)計(jì)工具Figma-Wasm文件大小為27.7M:
2)Google Earth-Wasm文件總計(jì)大小為192.M(支持各大瀏覽器的3D地圖,而且運(yùn)行流暢):
3)B站-視頻處理和播放也有使用Wasm快集,Wasm文件大小為344kb:
4)跨平臺(tái)的OpenGL圖形引擎Magnum-Wasm文件大小為844kb:
7贡羔、得物的Wasm實(shí)踐
7.1準(zhǔn)備
這里我們通過使用Rust + Wasm實(shí)現(xiàn)Wasm與JavaScript之間的數(shù)據(jù)調(diào)用,理解Rust和Wasm的交互過程个初。
使用Rust就需要做一些前置的環(huán)境配置乖寒,詳情的步驟可參考Rust官網(wǎng):https://www.rust-lang.org/zh-CN/tools/install。
安裝wasm-pack院溺,wasm-pack是一個(gè)構(gòu)建楣嘁、測(cè)試和發(fā)布Wasm的Rust CLI工具,我們將使用wasm-pack相關(guān)的命令來構(gòu)建Wasm二進(jìn)制內(nèi)容珍逸。這有助于將代碼編譯為WebAssembly逐虚,并生成在瀏覽器中使用的正確包。
7.2Rust項(xiàng)目初始化
執(zhí)行cargo new rust_wasm初始化Rust項(xiàng)目弄息,自動(dòng)生成配置文件Cargo.toml痊班。
項(xiàng)目結(jié)構(gòu)如下:
/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會(huì)自動(dòng)下載依賴摹量。
具體是:
1)crate-type = ["cdylib"],表示編譯時(shí)候使用C標(biāo)準(zhǔn)的動(dòng)態(tài)庫;
2)#[wasm_bindgen]是一個(gè)屬性宏缨称,來自于wasm_bindgen這個(gè)crate凝果,是一個(gè)簡(jiǎn)化Rust WASM與JS之間交互的庫。
[lib]
crate-type?= ["cdylib"]
[dependencies]
wasm-bindgen = { version =?"0.2.89", features = [] }
7.4編寫代碼
編寫代碼之前我們先明確Rust中crate包的概念睦尽,Rust中包管理系統(tǒng)將crate包分為二進(jìn)制包(Binary)和庫包(Library)兩種器净,二者可以在同一個(gè)項(xiàng)目中同時(shí)存在。
二進(jìn)制包:
1)main.rs是二進(jìn)制項(xiàng)目的入口当凡;
2)二進(jìn)制項(xiàng)目可直接執(zhí)行山害;
3)一個(gè)項(xiàng)目中二進(jìn)制包可以有多個(gè),所以在Cargo.toml中通過雙方括號(hào)標(biāo)識(shí) [[bin]]沿量。
庫包:
1)lib.rs是庫包的入口浪慌;
2)庫項(xiàng)目不可直接執(zhí)行,通常用來作為一個(gè)模塊被其他項(xiàng)目引用朴则;
3)一個(gè)項(xiàng)目中庫包僅有1個(gè)权纤,在Cargo.toml中通過單方括號(hào)標(biāo)識(shí) [lib]。
因?yàn)槲覀冞@里希望將 Wasm 轉(zhuǎn)為一個(gè)可以在JS項(xiàng)目中使用的模塊乌妒,所以需要使用庫包 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執(zhí)行編譯
這里我們要使用到wasm-pack撤蚊,將上述的Rust代碼編譯為能夠被JS導(dǎo)入的模塊古掏,根據(jù)wasm-pack提供的target方式可以指定構(gòu)建的產(chǎn)物。
如截圖所示:
編譯過程效果:
編譯完成后拴魄,我們會(huì)發(fā)現(xiàn)根目錄下多了一個(gè)pkg/ 文件夾冗茸,里面就是我們的Wasm產(chǎn)物所在的npm包了。
目錄結(jié)構(gòu)如下:
/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文件內(nèi)容:
/* 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打包不僅輸出一個(gè)ESM規(guī)范的模塊匹中,而且還支持自動(dòng)生成d.ts文件夏漱,對(duì)模塊的使用者非常友好。
如下:
7.6在前端項(xiàng)目中引入使用
'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代碼計(jì)算結(jié)果:{addResult}
????????</div>
??????????二分法方式{calculateTime}
????????</div>
??????</div>
????</main>
??);
}
7.7在IM場(chǎng)景下的性能比較
在IM場(chǎng)景下顶捷,聊天消息中核心的處理流程在于數(shù)據(jù)的排序挂绰、去重,大量的數(shù)據(jù)查找會(huì)非常耗時(shí)服赎,在這里我們通過二分法的方式對(duì)Rust和JavaScript兩種實(shí)現(xiàn)方式的耗時(shí)進(jìn)行一個(gè)簡(jiǎn)單的對(duì)比葵蒂。
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,
????????}
????}
}
//?獲取原始數(shù)據(jù)
fn get_origin_data(num: i32) -> Vec<Data> {
????let?mut data: Vec = vec![];?//?存儲(chǔ)數(shù)據(jù)的向量
????....?//?創(chuàng)建 num 個(gè)數(shù)據(jù)
????data
}
//?初始化結(jié)構(gòu)體數(shù)據(jù)
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!("插入{}條數(shù)據(jù)執(zhí)行耗時(shí):{}微秒", num, duration);
????result
}
數(shù)據(jù)對(duì)比分析:
可以看到:在數(shù)據(jù)量不大的場(chǎng)景下,Wasm的耗時(shí)是比純JavaScript長的重虑,這是因?yàn)闉g覽器需要在VM容器中對(duì) Wasm模塊進(jìn)行實(shí)例化践付,這一部分會(huì)消耗相當(dāng)?shù)臅r(shí)間,導(dǎo)致性能不如純JavaScript的執(zhí)行缺厉。但隨著運(yùn)算規(guī)模變大永高,Wasm的優(yōu)化越來越明顯隧土。這是因?yàn)閃ebAssembly是一種低級(jí)別的二進(jìn)制格式,經(jīng)過高度優(yōu)化命爬,并且能夠更好地利用系統(tǒng)資源曹傀。相比之下,JavaScript是一種解釋性語言饲宛,性能可能會(huì)受到解釋器的限制皆愉。
8、本文小結(jié)
在大多數(shù)場(chǎng)景下我們都不需要用到WebAssembly艇抠。因?yàn)閂8等JS引擎的優(yōu)化帶來了巨大的性能提升幕庐,已經(jīng)足夠讓JavaScript應(yīng)對(duì)絕大多數(shù)的普通場(chǎng)景了,如果要做進(jìn)一步優(yōu)化密集計(jì)算任務(wù)時(shí)使用Web worker也都能解決掉家淤。只有在以上的少數(shù)場(chǎng)景下翔脱,我們才需要做這種“二次提升”。
WebAssembly雖然有天然的優(yōu)勢(shì)媒鼓,但也有自己的局限性届吁,在使用時(shí)我們也需要考慮多方面因素,例如生態(tài)绿鸣、開發(fā)成本等等疚沐。不過我們依然可以持續(xù)關(guān)注WebAssembly的發(fā)展。
9潮模、相關(guān)資料
[1]?一文讀懂前端技術(shù)演進(jìn):盤點(diǎn)Web前端20年的技術(shù)變遷史
[2]?新手入門貼:史上最全Web端即時(shí)通訊技術(shù)原理詳解
[3]?Web端即時(shí)通訊技術(shù)盤點(diǎn):短輪詢亮蛔、Comet、Websocket擎厢、SSE
[4]?新手快速入門:WebSocket簡(jiǎn)明教程
[5]?WebSocket詳解(六):刨根問底WebSocket與Socket的關(guān)系
[6]?WebSocket從入門到精通究流,半小時(shí)就夠!
[7]?搞懂現(xiàn)代Web端即時(shí)通訊技術(shù)一文就夠:WebSocket动遭、socket.io芬探、SSE
[8]?詳解Web端通信方式的演進(jìn):從Ajax、JSONP 到 SSE厘惦、Websocket
[9]?從理論到實(shí)踐偷仿,詳細(xì)對(duì)比Electron和Tauri的優(yōu)劣
[10]?快速對(duì)比跨平臺(tái)框架Electron、Flutter宵蕉、Tauri酝静、React Native等
10、得物技術(shù)團(tuán)隊(duì)其它文章
得物從0到1自研客服IM系統(tǒng)的技術(shù)實(shí)踐之路
得物自研客服IM中收發(fā)聊天消息背后的技術(shù)邏輯和思考實(shí)現(xiàn)
得物從零構(gòu)建億級(jí)消息推送系統(tǒng)的送達(dá)穩(wěn)定性監(jiān)控體系技術(shù)實(shí)踐
得物基于Electron開發(fā)客服IM桌面端的技術(shù)實(shí)踐
得物自研移動(dòng)端弱網(wǎng)診斷工具的技術(shù)實(shí)踐分享