WebAssembly 功能偵測

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

Ingvar Stepanyan
Ingvar Stepanyan

WebAssembly 1.0 已於 4 年前推出,但開發並未止步於此。我們會透過提案標準化程序加入新功能。和一般的網頁一樣,網頁版新功能就是這樣,每種引擎的導入順序和時程可能有明顯差異。如要使用這些新功能,您必須確保所有使用者都不會遺漏任何使用者。本文將說明如何達成這個目標。

有些新功能可新增常用作業的操作說明,藉此改善程式碼大小,有些則新增強大的效能基本功能,其他部分則能改善開發人員體驗,並與網路上的其他內容整合。

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

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

挑選及分組功能

讓我們一起挑選任意功能組合做為範例,逐步說明這些步驟。假設我就大小和效能考量,希望在程式庫中使用 SIMD、執行緒和例外狀況處理。他們的瀏覽器支援如下:

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

您可以將瀏覽器分為下列同類群組,確保每位使用者都能獲得最佳體驗:

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

此細目會根據各瀏覽器的最新版本支援的功能支援細分資料。新式瀏覽器會不斷更新且會自動更新,在大多數情況下,您只需考慮最新版本。不過,只要您將基準 WebAssembly 新增為備用同類群組,即使瀏覽器版本過舊,您還是可以提供可正常運作的應用程式。

編譯不同的功能集

WebAssembly 沒有內建在執行階段中偵測支援的功能的方式,因此模組中的所有指示都必須支援目標。因此,您必須針對各個不同的功能集,將原始碼分別編譯至 Wasm。

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

我將透過 SSE2 模擬使用 SIMD,透過 Pthreads 程式庫支援使用執行緒,並選擇 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 程式庫來執行此操作。結合這個 API 與動態匯入功能,您就能在任何瀏覽器中載入成效最佳的套件:

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 功能來建構及載入程式碼。