WebAssembly 特徴検出

あらゆるブラウザでユーザーをサポートしながら、WebAssembly の最新機能を使用する方法について説明します。

WebAssembly 1.0 は 4 年前にリリースされましたが、開発はそれで終わりませんでした。新機能は、プロポーザル標準化プロセスを通じて追加されます。ウェブの新機能の場合と同様に、実装順序やタイムラインはエンジンによって大きく異なる場合があります。これらの新機能を使用するには、除外されたユーザーがいないことを確認する必要があります。この記事では、これを実現するためのアプローチについて説明します。

新しい機能には、一般的な操作に対する新しい命令を追加してコードサイズを改善するもの、パワフルなパフォーマンス プリミティブを追加するもの、デベロッパー エクスペリエンスを向上させ、ウェブの他の部分との統合を向上させるものがあります。

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

あらゆるブラウザのユーザーがアプリケーションを使用できるようにするには、どの機能を使用するかを検討する必要があります。次に、ブラウザのサポート状況に基づいてユーザーをグループ分けします。次に、これらのグループごとにコードベースを個別にコンパイルします。最後に、ブラウザ側では、サポートされている機能を検出し、対応する JavaScript と Wasm のバンドルを読み込む必要があります。

機能の選択とグループ化

任意の機能セットを例に、この手順を見ていきましょう。サイズとパフォーマンスの理由から、ライブラリで SIMD、スレッド、例外処理を使用するとします。ブラウザのサポートは次のとおりです。

選択した機能のブラウザ サポートを示す表。
この機能の表については、webassembly.org/roadmap をご覧ください。

各ユーザーが最適なエクスペリエンスを提供できるよう、ブラウザを以下のコホートに分類できます。

  • Chrome ベースのブラウザ: スレッド、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 ライブラリで行うことができます。動的インポートと組み合わせることで、どのブラウザでも最も最適化されたバンドルを読み込むことができます。

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 機能を使用してコードを構築し、読み込むには、このアプローチが唯一の方法であることに変わりはありません。