WebAssembly 功能偵測

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

Ingvar Stepanyan
Ingvar Stepanyan

WebAssembly 1.0 已於四年前發布,但開發工作並未就此停止。新增功能會透過提案標準化程序新增。網頁上的新功能通常也是如此,不同引擎的實作順序和時程可能大不相同。如果您想使用這些新功能,請務必確保所有使用者都能使用。本文將說明達成這個目標的方法。

部分新功能會為常見作業新增指令,進而縮減程式碼大小;部分功能則會新增強大的效能原始碼;其他功能則可改善開發人員體驗,並與其他網頁整合。

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

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

挑選及分組地圖項目

讓我們挑選一些任意功能集做為範例,逐步完成這些步驟。假設您是基於大小和效能考量,決定在程式庫中使用 SIMD、執行緒和例外狀況處理功能。瀏覽器支援如下:

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

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

  • 以 Chrome 為基礎的瀏覽器:支援執行緒、SIMD 和例外狀況處理。
  • Firefox:支援執行緒和 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 程式庫執行這項操作。搭配動態匯入功能使用,您就能在任何瀏覽器中載入最佳化程度最高的套件:

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 功能建構及載入程式碼時,仍只能使用這個方法。