WebAssembly 特徴検出

すべてのブラウザでユーザーをサポートしながら、最新の WebAssembly 機能を利用する方法を学びます。

WebAssembly 1.0 は 4 年前にリリースされましたが、開発はそこで止まりませんでした。新機能は、提案の標準化プロセスを通じて追加されます。ウェブの新機能の場合と同様に、実装の順序とタイムラインはエンジンによって大きく異なる場合があります。これらの新機能を利用する場合は、どのユーザーも取り残されないようにする必要があります。この記事では、この目標を達成するためのアプローチについて説明します。

新機能には、一般的なオペレーションの新しい命令を追加してコードサイズを削減するものや、強力なパフォーマンス プリミティブを追加するもの、デベロッパー エクスペリエンスとウェブの他の部分との統合を改善するものなどがあります。

提案とそれぞれのステージの完全なリストについては、公式リポジトリをご覧ください。また、公式の機能ロードマップのページで、エンジンでの実装ステータスを追跡することもできます。

すべてのブラウザのユーザーがアプリケーションを使用できるようにするには、使用する機能を把握する必要があります。次に、ブラウザのサポートに基づいてグループに分割します。次に、これらのグループごとにコードベースを個別にコンパイルします。最後に、ブラウザ側ではサポートされている機能を検出し、対応する 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
}

例外処理では、コンパイル フラグで選択された基盤実装に関係なく、C++ から同様に使用できるため、#ifdef ディレクティブは必要ありません。

正しいバンドルを読み込む

すべての特徴コホートのバンドルを作成したら、メインの 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 機能を使用してコードをビルドして読み込む唯一の方法は、このアプローチです。