WebAssembly 功能偵測

瞭解如何使用最新的 WebAssembly 功能,同時支援所有瀏覽器的使用者。

Ingvar Stepanyan
Ingvar Stepanyan

WebAssembly 1.0 雖然四年前推出,但開發並未停下腳步。透過提案標準化程序加入新功能。如果是在網路上推出新功能的情況,不同引擎的導入順序和時程也可能有顯著差異。如要使用這些新功能,請務必確保沒有使用者會流失。本文將說明達成這個目標的方法。

部分新功能可增加常見作業的新指示,藉此縮減程式碼大小,並加入強大的效能基元,以及改善開發人員體驗,並與其他網路整合。

您可以在官方存放區查看提案的完整清單和相應階段,也可以前往官方的功能藍圖頁面,在引擎中追蹤提案的實作狀態。

如要確保所有瀏覽器的使用者都能使用您的應用程式,您必須找出要使用的功能。然後根據瀏覽器支援功能分成不同群組。然後分別編譯每個群組的程式碼集。最後,您必須在瀏覽器端偵測支援的功能,並載入對應的 JavaScript 和 Wasm 套件。

選擇和分組特徵

以下將挑選任意功能集做為範例,逐步說明這些步驟。假設您是基於大小和效能考量,決定在程式庫中使用 SIMD、執行緒和例外狀況處理功能。以下是該公司的瀏覽器支援

瀏覽器支援所選功能的表格。
前往 webassembly.org/roadmap 查看這個功能表格。

您可以將瀏覽器分成下列同類群組,以確保每位使用者都能享有最優質的體驗:

  • Chrome 式瀏覽器:支援執行緒、SIM 卡和例外狀況處理功能。
  • Firefox:支援 Thread 和 SIMD,不支援例外狀況處理。
  • Safari:支援執行緒,不支援 SIMD 和例外狀況處理。
  • 其他瀏覽器:僅假設 WebAssembly 支援基準。

這項細目會根據每個瀏覽器最新版本的功能支援細分。新世代瀏覽器不會退流行,且會自動更新,因此在大多數情況下,您只需擔心最新版本的瀏覽器。不過,只要納入基準 WebAssembly 做為備用同類群組,即可提供正常運作的應用程式,就算使用者使用過舊的瀏覽器也一樣。

編譯不同的特徵集

WebAssembly 的內建功能無法偵測執行階段中支援的功能,因此模組中的所有指示都必須在目標上獲得支援。因此,您必須針對每個不同的功能集,分別將原始碼編譯到 Wasm 中。

每個工具鍊和建構系統都不相同,您必須參閱自己的編譯器的說明文件,瞭解如何調整這些功能。為求簡單起見,我會在以下範例中使用單一檔案 C++ 程式庫,並示範如何使用 Emscripten 進行編譯。

我將透過 SSE2 模擬功能使用 SIMD、透過 Pthread 程式庫支援的執行緒,然後選擇 Wasm 例外狀況處理備用 JavaScript 實作

# First bundle: threads + SIMD + Wasm exceptions
$ emcc main.cpp -o main.threads-simd-exceptions.mjs -pthread -msimd128 -msse2 -fwasm-exceptions
# Second bundle: threads + SIMD + JS exceptions fallback
$ emcc main.cpp -o main.threads-simd.mjs -pthread -msimd128 -msse2 -fexceptions
# Third bundle: threads + JS exception fallback
$ emcc main.cpp -o main.threads.mjs -pthread -fexceptions
# Fourth bundle: basic Wasm with JS exceptions fallback
$ emcc main.cpp -o main.basic.mjs -fexceptions

C++ 程式碼本身可以使用 #ifdef __EMSCRIPTEN_PTHREADS__#ifdef __SSE2__,在編譯期間有條件地選擇相同函式的平行 (執行緒和 SIMD) 實作項目。程式碼應會如下所示:

void process_data(std::vector<int>& some_input) {
#ifdef __EMSCRIPTEN_PTHREADS__
#ifdef __SSE2__
  // …implementation using threads and SIMD for max speed
#else
  // …implementation using threads but not SIMD
#endif
#else
  // …fallback implementation for browsers without those features
#endif
}

例外狀況處理作業不需要使用 #ifdef 指令,因為無論透過編譯旗標選擇的基礎實作方式為何,都可以透過 C++ 以相同方式使用例外狀況處理功能。

正在載入正確的套件

為所有功能同類群組建立套裝組合後,您必須從主要的 JavaScript 應用程式載入正確的套件。為此,請先偵測目前瀏覽器支援的功能。您可以使用 wasm-feature-detect 程式庫來進行這項操作。只要結合動態匯入功能,即可在任何瀏覽器中載入最佳化程度最高的套件:

import { simd, threads, exceptions } from 'https://unpkg.com/wasm-feature-detect?module';

let initModule;
if (await threads()) {
  if (await simd()) {
    if (await exceptions()) {
      initModule = import('./main.threads-simd-exceptions.mjs');
    } else {
      initModule = import('./main.threads-simd.mjs');
    }
  } else {
    initModule = import('./main.threads.mjs');
  }
} else {
  initModule = import('./main.basic.mjs');
}

const Module = await initModule();
// now you can use `Module` Emscripten object like you normally would

最後幾個字

這篇文章將說明如何選擇、建構及切換不同功能集的套件。

隨著功能數量增加,特徵同類群組的數量可能會無法維護。為減輕這個問題的影響,您可以根據實際使用者資料選擇功能同類群組,略過較不熱門的瀏覽器,改為使用較不理想的同類群組。只要應用程式仍能供所有使用者使用,這種方法就能在漸進強化與執行階段效能之間提供合理的平衡。

在未來,WebAssembly 可能會取得內建方法來偵測支援的功能,並在模組中切換相同函式的不同實作。不過,這類機製本身就是一種 MVP 後的功能,您需要使用上述方法有條件地偵測及載入。在此之前,在所有瀏覽器中使用新的 WebAssembly 功能建構及載入程式碼時,仍只能使用這個方法。