WebAssembly 特徴検出

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

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

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

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

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

特徴のピッキングとグループ化

例として、任意の機能セットを取り上げて、これらのステップを見てみましょう。たとえば、サイズとパフォーマンスの理由から、ライブラリで SIMD、スレッド、例外処理を使用することがわかったとします。各ユーザーのブラウザ サポートは次のとおりです。

<ph type="x-smartling-placeholder">
</ph> 選択した機能のブラウザ サポートを示す表。 <ph type="x-smartling-placeholder">
</ph> この機能の表は 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
}

例外処理では、コンパイル フラグで選択された基盤実装に関係なく、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 機能を使用してコードを構築し、読み込むには、この方法が唯一の方法であり続けます。