WebAssembly 和 JavaScript 該怎麼選?
大家好,我是 ConardLi。
JavaScript
已經長久以來並且目前依然是瀏覽器運行時的主流開發語言,然而近年來,WebAssembly
的誕生爲我們提供了一個全新的選擇。這就引出了一個值得我們探索的問題:在瀏覽器運行環境中,哪個語言的性能更優越,JavaScript
還是 WebAssembly
?
筆者最近在工作中正好面臨了這樣的選擇,我需要在瀏覽器運行時動態插入一些策略,用於在用戶的瀏覽器運行時實現一些安全功能,例如網站請求的 CSRF 防護,網站存儲數據的加解密等等,那麼這種動態的運行時策略到底該使用 JavaScript
還是 WebAssembly
呢,爲了找到答案,我做了一些驗證,本文將詳細對比兩者在各項性能指標上的表現。
基礎概念
-
JavaScript
,誕生於1995
年的一種高級編程語言,最初用於在Web
瀏覽器中添加交互式元素。互動效果如彈出新的窗口,響應按鈕點擊,改變網頁內容等,幾乎都離不開JavaScript
。它的核心設計理念是 " 簡單易懂”,語言本身易於上手,對新手友好。隨着Node.js
的出現,JavaScript
已不僅限於前端開發,而是成爲一種全棧編程語言。 -
WebAssembly
,或者簡稱Wasm
,是一種在瀏覽器環境下執行的新型二進制指令集,這就讓瀏覽器擁有了執行其他代碼(如C、C++、Rust、Java
)的能力。相較於JavaScript
的文本格式,WebAssembly
以二進制格式表達代碼,使得其具有較高的執行效率。WebAssembly
是爲了滿足對高性能和低級功能的需求而產生的,比如遊戲,音頻視頻編輯等。與JavaScript
一樣,Wasm
可以在幾乎所有現代瀏覽器中運行。
測試代碼
JavaScript
我們首先添加一個用於測試密集 CPU
計算的 cycle
函數,其他按照安全策略格式增加 20
個其他的函數(用於測試體積)。
window.StrategySet = {
cycle: {
key: 'cycle',
name: '循環計算測試',
expression: function (n) {
let result = 0;
for (let i = 0; i < n; i++) {
result += i;
}
return result;
}
},
detectTextHttp: {
key: 'detectTextHttp',
name: '檢測網頁明文傳輸請求',
expression: function (event) {
const { payload } = event;
if (payload.url.startsWith('http://')) {
console.log({ action: "REPORT_ONLY", reason: "HTTP REQUEST" }, event);
} else {
console.log({ action: "PASS", }, event);
}
}
}
// 剩餘 20 個函數 ...
}
window.Strategys = StrategyGroup = {
NETWORK_RESOURCE_REQUEST: ['detectTextHttp'],
PAGE_INITIALIZED: ['fibonacci'],
NETWORK_XHR_REQUEST: [],
API_LOCALSTORAGE_GET: ['cycle'],
API_CLIPBOARD_READ: [],
}
WebAssembly(Rust)
同 JS
實現完全一樣的邏輯:添加一個用於測試密集 CPU
計算的 cycle
函數,其他按照相同的格式增加 20 個函數。
#[no_mangle]
pub fn cycle(n: u64) -> u64 {
let mut result = 0;
for i in 0..n {
result += i;
}
result
}
struct DetectTextHttp {
key: &'static str,
name: &'static str,
expression: Box<dyn Fn(Event)>,
}
struct Event {
payload: Payload,
}
struct Payload {
url: String,
}
impl DetectTextHttp {
fn new(key: &'static str, name: &'static str, expr: Box<dyn Fn(Event)>) -> DetectTextHttp {
DetectTextHttp {
key: key,
name: name,
expression: expr,
}
}
}
fn main() {
let detect_text_http = DetectTextHttp::new(
"detectTextHttp",
"檢測網頁明文傳輸請求",
Box::new(|event: Event| {
if event.payload.url.starts_with("http://") {
println!("{{ action: \"REPORT_ONLY\", reason: \"HTTP REQUEST\" }}, {:?}, event");
} else {
println!("{{ action: \"PASS\" }}, event");
}
}),
);
// 剩餘 20 個檢測規則 ...
}
資源體積
JavaScript
JavaScript
在未經過任何壓縮的情況下,代碼體積爲 1.8KB
:
WebAssembly(Rust)
Rust
源代碼行數爲 259
行,使用 cargo build --target wasm32-unknown-unknown
打包爲 wasm
代碼,最終網頁中的加載的體積爲 1.7MB
:
但這個是未經過任何優化和壓縮的代碼,我們使用 Rust
編譯參數對產物的編譯體積進行優化後:
[profile.release]
opt-level = 'z' # 代碼大小最小化
lto = true # 啓用鏈接時優化,可以減少代碼體積
panic = 'abort' # 拋棄默認的包含堆棧展開的恐慌處理器
最終壓縮後的代碼大小爲 4.6KB:
所以,在同樣的代碼情況下,瀏覽器中可執行的代碼文件體積上 JavaScript
更勝一籌。
代碼初始化
因爲是需要動態執行的策略,代碼需要有一個動態拉取的過程,而不能直接打包在業務代碼內部。
我們先添加一個測試的 HTML
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta >
<title>WebAssembly 測試</title>
<!-- 基礎 SDK -->
<script id="" src="./basic.js"></script>
<!-- 一個外部 CDN JS -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js"></script>
<!-- 一個內聯的 Script 腳本 -->
<script>
// 調用 localStorage API,觸發 localStorage Hook
localStorage.setItem('name', 'ConardLi');
// 調用 fetch API ,觸發 fetch Hook
fetch('https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.1/jquery.min.js');
</script>
</head>
<body>
<img src="https://pic0.sucaisucai.com/11/50/11050520_2.jpg">
</body>
</html>
然後我們在基礎 SDK 中添加一些運行時的 Hook:
function initHook() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
log('[Hook] PerformanceObserver:', entry.name, window.Strategys);
}
});
observer.observe({ entryTypes: ['resource'] });
var originalFetch = window.fetch;
window.fetch = function () {
log('[Hook] Fetch:', arguments, window.Strategys);
return originalFetch.apply(this, arguments);
};
var originalSetItem = localStorage.setItem;
localStorage.setItem = function (key, value) {
log('[Hook] localStorage.setItem:' + key, window.Strategys);
};
}
JavaScript
策略拉取邏輯:
async function initStrategy() {
log('[initStrategy] start download')
document.write(`
<script
src="https://lf3-static.bytednsdoc.com/obj/eden-cn/kyhuvjeh7pxpozps/snoopy/security-strategy4.js"
onload="console.log('動態策略已經加載並執行完畢!', performance.now() - window.time)">
</script>`
);
log('[initStrategy] end download', performance.now() - window.time)
}
可見拉取、解析策略共花費的時間爲 34ms
,且後續同步執行的 JavaScript Hook
都可以拿到策略:
WebAssembly(Rust)
策略拉取邏輯(執行 WebAssembly 前還需要進行 ArrayBuffer 的轉換、實例創建等流程,均爲異步動作):
async function initStrategy() {
log('[initStrategy] start download')
const response = await fetch('https://lf3-static.bytednsdoc.com/obj/eden-cn/kyhuvjeh7pxpozps/snoopy/security-strategy1.wasm');
log('[initStrategy] end download', performance.now() - window.time)
const buffer = await response.arrayBuffer();
log('[initStrategy] to arrayBuffer', performance.now() - window.time)
const module = await WebAssembly.instantiate(buffer);
log('[initStrategy] WebAssembly.instantiate', performance.now() - window.time)
const cycle = module.instance.exports.cycle;
window.Strategys = cycle;
}
-
從開始到資源下載完成花費
142ms
-
ArrayBuffer 數據結構轉換花費
363ms
-
WebAssembly 實例化花費
23ms
從開始拉取 WebAssembly
模塊到最終可執行策略共消耗 528ms
。然後使用進行編譯體積優化後的模塊進行測試:
-
從開始到資源下載完成花費
75ms
-
ArrayBuffer 數據結構轉換花費
242ms
-
WebAssembly 實例化花費
24ms
整個過程均爲異步,在這段時間頁面上下載並解析的 JS 還是會繼續執行的,在這期間 Hook 點位上拿不到策略。
長任務測試
爲了讓這段異步下載的過程更加直觀,在業務代碼中模擬一個純 CPU
計算的長任務:
<script>
// 模擬一個長任務,用於體現策略拉取的異步動作
console.log('[業務] start cpu', performance.now() - window.time);
for (let i = 0; i < 999999999; i++) {
}
console.log('[業務] end cpu', performance.now() - window.time);
</script>
可見 WebAssembly
模塊實例化一定在業務長任務執行完後執行:
而 JavaScript
則會先解析好策略後再開始執行後續的 Script
邏輯:
代碼執行
JavaScript
測試代碼,調用 cycle 函數:
log('[initStrategy] 策略計算性能測試 init', performance.now() - window.time);
const result = window.StrategySet[window.Strategys['API_LOCALSTORAGE_GET'][0]].expression(999999999);
log('[initStrategy] 策略計算性能測試 end', result, performance.now() - window.time);
WebAssembly(Rust)
測試代碼,調用 cycle 函數:
async function initStrategy() {
log('[initStrategy] start download')
const response = await fetch('https://lf3-static.bytednsdoc.com/obj/eden-cn/kyhuvjeh7pxpozps/snoopy/security-strategy1.wasm');
log('[initStrategy] end download', performance.now() - window.time)
const buffer = await response.arrayBuffer();
log('[initStrategy] to arrayBuffer', performance.now() - window.time)
const module = await WebAssembly.instantiate(buffer);
log('[initStrategy] WebAssembly.instantiate', performance.now() - window.time)
const cycle = module.instance.exports.cycle;
window.Strategys = cycle;
console.time('策略計算性能測試')
const result = cycle(999999999n);
log('[initStrategy] 策略計算性能測試', result, performance.now() - window.time);
console.timeEnd('策略計算性能測試')
}
最終結論
-
代碼體積:
-
JavaScript
:1.8KB ✅ VSWebAssembly(Rust)
4.6KB ❌ -
初始化時間:
-
JavaScript
:34ms ✅ VSWebAssembly(Rust)
528ms ❌ -
10 億次循環代碼執行時間:
-
JavaScript
:1.3s ❌ VSWebAssembly(Rust)
0.1 ms ✅
JavaScript
:首屏加載快、可同步加載、計算性能差:需要在業務首屏渲染前執行的策略、計算邏輯簡單的策略,優先考慮使用 JavaScript 執行,例如 CSRF 防護、API 調用鑑權等策略。
WebAssembly
:首屏初始化慢、只能異步加載、計算性能好:可以在業務首屏渲染完成後異步執行的策略,計算邏輯非常複雜、有密集 CPU 計算的策略,考慮使用 WebAssembly 模塊執行,例如需要給業務圖片在前端增加水印,需要對圖片數據進行重寫等策略。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/1VZxzb-SbnPyvruJUBzhQA