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 功能在所有瀏覽器中建構及載入程式碼的唯一方法。