すべてのブラウザでユーザーをサポートしながら、最新の WebAssembly 機能を利用する方法を学びます。
WebAssembly 1.0 は 4 年前にリリースされましたが、開発はそこで止まりませんでした。新機能は、提案の標準化プロセスを通じて追加されます。ウェブの新機能の場合と同様に、実装の順序とタイムラインはエンジンによって大きく異なる場合があります。これらの新機能を利用する場合は、どのユーザーも取り残されないようにする必要があります。この記事では、この目標を達成するためのアプローチについて説明します。
新機能には、一般的なオペレーションの新しい命令を追加してコードサイズを削減するものや、強力なパフォーマンス プリミティブを追加するもの、デベロッパー エクスペリエンスとウェブの他の部分との統合を改善するものなどがあります。
提案とそれぞれのステージの一覧については、公式リポジトリをご覧ください。また、エンジンへの実装ステータスは、公式の機能ロードマップページで確認できます。
すべてのブラウザのユーザーがアプリケーションを使用できるようにするには、使用する機能を把握する必要があります。次に、ブラウザのサポートに基づいてグループに分割します。次に、これらのグループごとにコードベースを個別にコンパイルします。最後に、ブラウザ側でサポートされている機能を検出し、対応する JavaScript と Wasm バンドルを読み込む必要があります。
特徴の選択とグループ化
これらの手順を例として、任意の特徴セットを選択して説明します。サイズとパフォーマンスの理由から、ライブラリで SIMD、スレッド、例外処理を使用するとします。ブラウザのサポートは次のとおりです。
ブラウザを次のコホートに分割して、各ユーザーに最適化されたエクスペリエンスを提供できます。
- 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 機能を使用してコードをビルドして読み込む唯一の方法は、このアプローチです。