Emscriptenでコンパイルされた出力は、コマンドラインからJSシェルで直接実行することも、ウェブページでホストすることもできます。asm.jsとWebAssemblyでコンパイルされたページをブラウザで実行するための.htmlとしてホストする場合、Emscriptenはコードを実行するためのランチャーとして機能するデフォルトの「HTMLシェルファイル」を提供します。これは開発開始を簡素化するためです。しかし、コンテンツをウェブサイトでリリースしてホストする準備をする際には、訪問者のエクスペリエンスを向上させるために、多くの追加機能とカスタマイズが必要になる可能性があります。このガイドでは、サイトを公開にデプロイする際に注意すべき点を強調します。
Emscriptenのビルド出力は、1)低レベルのコンパイル済みコードモジュールと、2)それとの対話するためのJavaScriptランタイムの2つの重要な部分で構成されています。-o out.html
でビルドする場合、コンパイル済みコードはout.wasm
ファイルに、ランタイムはout.js
ファイルに格納されます。asm.jsをターゲットとする場合、コンパイル済みコードの静的メモリセクションを含む追加のバイナリファイルout.mem
が存在します。WebAssemblyをターゲットとする場合、この部分はout.wasm
ファイルに埋め込まれます。
使用されている機能に応じて、追加のビルド出力ファイルも存在する場合があります。Emscriptenファイルパッケージャーを使用すると、バイナリout.data
パッケージと、それに関連するout.data.js
ローダーファイルが生成されます。また、EmscriptenのpthreadsとFetch APIには、それぞれ関連するWeb Worker関連のスクリプト.js
出力ファイルがあります。
開発者は、JavaScriptまたはHTMLのいずれかに出力を選択できます。JavaScriptに出力する場合(emcc -o out.js
)、開発者は、コードがブラウザで実行されるout.html
メインページを手動で作成する必要があります。emcc -o out.html
でHTMLをターゲットとする場合(推奨されるビルドモード)、EmscriptenはHTMLシェルファイルを自動的に生成します。このシェルファイルは、emcc -o out.html --shell-file path/to/custom_shell.html
リンカーディレクティブを使用してカスタマイズできます。カスタマイズされたシェルファイルの良い出発点となるテンプレートを入手するには、Emscriptenリポジトリからデフォルトの最小限のHTMLシェルファイルをプロジェクトツリーにコピーしてください。
次のセクションでは、サイトエクスペリエンスを向上させるためのヒントを紹介します。
ページの読み込み速度を遅くする最大の要因は、多くの場合、プロジェクトのアセットデータの大量ダウンロードの必要性です。特に、ページでWebGLテクスチャやジオメトリを多用している場合です。コンパイル済みコードは、手書きのJavaScriptよりも一般的に多くのスペースを占有しますが、機械コードは効率的に圧縮されます。したがって、asm.jsとWebAssemblyをホストする際には、すべてのコンテンツがgzip圧縮を使用して転送されることを確認することが重要です。これは、現在すべてのブラウザとCDNが組み込みでサポートしています。.wasm
ファイルをgzip圧縮すると、平均で60〜75%のサイズ削減が得られるため、非圧縮ファイルを配信することはほとんど意味がありません。
CDNでgzip圧縮されたアセットを配信するには、gzip圧縮ツールを使用して、アセットファイルをオフラインで事前に圧縮してからCDNにアップロードします。一部のWebサーバーは、ファイルをオンザフライで圧縮することをサポートしていますが、静的アセットコンテンツの場合、サーバーのCPUにファイルを再圧縮し続けるコストがかかるため、避ける必要があります。HTTPレスポンスヘッダーContent-Encoding: gzip
を使用して、事前に圧縮されたファイルをホストするようにWebサーバーの設定を調整します。これにより、Webブラウザは、ダウンロードされたコンテンツをページ自体にデータを渡す前に透過的に解凍するよう指示されます。
gzip圧縮がアセットが提供されるMIMEタイプと混同しないようにしてください。すべてのJavaScriptファイル(事前に圧縮されているかどうかにかかわらず)は、HTTPレスポンスヘッダーContent-Type: application/javascript
で提供するのが最適であり、すべてのアセットファイル(.data
、.mem
)はヘッダーContent-Type: application/octet-stream
で提供する必要があります。WebAssembly .wasm
ファイルはContent-Type: application/wasm
で提供する必要があります。
Emscriptenの--preload-file
リンカーフラグを使用して、事前にロードされるアセットデータの量を最小限に抑えてください。このデータファイルは、Emscriptenでコンパイルされたアプリケーションがmain()
関数の実行を開始する前にロードされるため、このパッケージに格納されているすべてのファイルは、起動時間を大幅に遅くする可能性があります。ダウンロードされたアセットファイルを複数の個別のパッケージに分割し、アプリケーションの実行中に動作できるEmscriptenの非同期アセットダウンロードAPIを使用することをお勧めします。
WebGLアプリケーションのアセットサイズは、多くの場合、テクスチャの量によって支配されるため、圧縮テクスチャ形式を使用すると、アセットサイズを縮小できます。Webは、ネイティブプラットフォームと比較して非常に異なるターゲットプラットフォームになる可能性があります。なぜなら、Webでは、特にモバイルとデスクトップブラウザの両方で動作するサイトを開発している場合、特定の圧縮テクスチャ形式が訪問者のハードウェアでサポートされるとは必ずしも想定できないからです。幅広いハードウェアをサポートするためのベストプラクティスは、サポートされている各プラットフォームに対して複数の圧縮テクスチャセットを生成し、WebGLコンテキストがサポートする形式に基づいて適切なセットをダウンロードすることです。
複数の画面サイズ(デスクトップとモバイルのフォームファクタなど)をターゲットにしている場合は、テクスチャをSDとHDのバリアントに分割して、小さなディスプレイ解像度を持つモバイルデバイスのページの読み込み速度を向上させることを検討してください。
ページのダウンロードに加えて、起動シーケンスの他の部分も遅い場合があります。ここで考慮すべき点は次のとおりです。
asm.jsをターゲットにしてFirefoxまたはEdgeで実行する場合、Webページコンソールには、asm.jsモジュールがコンパイルされた後にログメッセージが表示されます。このログメッセージには、コンパイルにかかった時間に関するタイミング情報が含まれています。asm.jsコンパイルは、asm.jsスクリプトソースファイルがDOMに追加された時点で開始され、完了すると、スクリプトタグのonload
イベントが呼び出されます。これは、Safari、Opera、Chromeでasm.jsコンパイルにかかった時間を計測するために使用できます。
ブラウザでのコンパイル済みコードの起動時間を短縮するために、WebAssemblyに移行することをお勧めします。WebAssemblyモジュールは、asm.jsと比較して、解析とコンパイルがはるかに高速です。さらに、コンパイルされたWebAssembly.Module
オブジェクトはIndexedDBに手動で永続化できるため、2回目の実行ではコンパイルステップ全体を回避できます。(次のセクションを参照)
起動が遅い原因をasm.js/WebAssemblyのコンパイルに誤って帰結することがありますが、実際にはアプリケーション自身のmain()
関数エントリポイントの実行に時間がかかっていることが原因である可能性があります。これは、この2つの処理が連続して実行されるためです。これら2つの処理を個別にプロファイルすることが重要です。src/preamble.js
内のfunction callMain()
を確認してください。これはアプリケーションのmain()
コードの実行を開始します。main()
の実行に時間がかかりすぎる場合は、複数のsetTimeout()
呼び出しまたはemscripten_set_main_loop()
イベントループによって駆動される個別の操作に分割することを検討してください。
ネットワーク転送速度を向上させるには、通常のネットワーク環境下では、(数が少ないと仮定して)すべてのネットワークダウンロードを同時に並行して開始するアグレッシブなアプローチが、例えば1つの入力ファイルをダウンロードしてから次のファイルをダウンロードするよりも高速であることが経験的に示されています。したがって、ネットワーク転送速度を最大化するには、アプリケーションのメインHTMLページで、必要なすべてのネットワークダウンロードを順番に実行するのではなく、並行して開始するように記述してください。
ページの初回読み込みがネットワーク転送によって支配されている場合、ダウンロードが完了するまでCPUがアイドル状態であることを利用することが有効です。このCPU時間を、他の重いタスクの実行に使用できます。これには、他のページアセットのダウンロード中に、asm.js/WebAssemblyモジュールのダウンロードとコンパイルを行うことが理想的です。
Windowsベースのシステムでは、WebGLシェーダーのコンパイルが遅いという問題が現在知られています。これも、ページの他のアセットをダウンロード中に並行して実行する候補です。
ページを初めて訪問したときの初回実行には、すべてのダウンロードを完了するのに時間がかかる可能性がありますが、2回目のページ訪問では、初回訪問の結果がブラウザによってキャッシュされるようにすることで、速度を大幅に向上させることができます。
すべてのブラウザには、アセットに対する実装定義の制限(20MBまたは50MB)があり、それよりも大きいファイルはブラウザの組み込みWebキャッシュを完全にバイパスします。そのため、大きな.data
ファイルは、メインページによってIndexedDBに手動でキャッシュすることをお勧めします。Emscriptenリンカーオプション--use-preload-cache
を使用すると、Emscriptenでこれを実装できますが、アセットをキャッシュするデータベースと、データの削除に使用するスキームを制御できるため、カスタムの方法でHTMLページで手動で管理することが望ましい場合があります。
.wasm
ファイルは、他のリソースと同様に自動的にキャッシュされますが、インスタンス化するにはブラウザでコンパイルする必要があります。幸いなことに、Chromiumベースのブラウザは、コンパイル済みのWebAssemblyモジュールの自動キャッシュをサポートしています(このv8.devブログ記事を参照)。以前は、IndexedDBによるコンパイル済みWebAssemblyモジュールのマニュアルキャッシュが推奨されていましたが、現在はほとんどサポートされていません(このWebAssembly仕様チケットの詳細)。
コンパイルされたC/C++コード自体が、2回目の読み込み時にスキップできるmain()
などでの計算を実行する場合は、IndexedDBまたはlocalStorage APIを使用して、この計算の結果をページ実行間でキャッシュします。IndexedDBは大規模なファイルの保存に適していますが、非同期的に動作します。一方、localStorage APIは完全に同期していますが、使用は小さなCookieスタイルのデータフィールドの保存に制限されています。
IndexedDBベースのキャッシングを実装する際には、非同期APIでありディスクアクセスを実行するため、IndexedDB操作にもある程度のレイテンシがあることに注意してください。したがって、起動時に複数の読み取り操作を実行する場合は、可能な限りすべてを並行して実行して、レイテンシを削減することが重要です。
データを永続化することにおけるもう1つの重要な点は、ユーザーにとって最良の方法として、IndexedDBまたはlocalStorageを使用して大量のデータを永続化する場合に、明示的な視覚的な識別を提供し、そのデータをクリアまたはアンインストールするための簡単なメカニズムを提供することです。これは、現在、ブラウザがこれらのストレージからのデータの細かい削除のための便利なUIを実装していないためですが、データのクリアは多くの場合、「すべてのページからキャッシュをクリア」タイプのオプションとして提示されます。
asm.jsとWebAssemblyアプリケーションの本質的な特性は、アプリケーションのヒープを表す線形メモリブロックが必要になることです。これは、Emscriptenでコンパイルされたページが実行する最大のメモリ割り当てであることが多く、したがって、ユーザーのシステムのメモリが不足している場合に失敗するリスクが最も高いものです。
このメモリ割り当ては連続している必要があるため、ユーザーのブラウザプロセスに十分なメモリがあっても、プロセスのアドレス空間が断片化されているだけで、割り当てを満たすのに十分な線形アドレス空間がない可能性があります。この問題を回避するには、メインページの先頭で、他の割り当てやページスクリプトの読み込みアクションが行われる前に、WebAssembly.Memory
オブジェクト(asm.jsの場合はArrayBuffer
)を事前に割り当てるのが最善策です。これにより、割り当てが成功する可能性が高まります。Module['buffer']
とModule['wasmMemory']
のフィールドの詳細を参照してください。
さらに、この種の大きな割り当てを必要とするWebページに対して、コンテンツプロセスの分離をオプトインできます。この機構を利用するには、メインHTMLページを提供する際に、HTTPレスポンスヘッダーLarge-Allocation: <MBytes>
を指定します。このサポートは、現在Firefox 53で実装されています。
最後に、ページが読み込まれた後も、不要な大きなメモリブロックに誤って固執することがあります。たとえば、WebAssemblyでは、WebAssemblyモジュールがWebAssembly.Instance
オブジェクトにインスタンス化されると、元のWebAssembly.Module
オブジェクトはメモリ内で不要になり、ガベージコレクタがそれを回収できるように、そのすべての参照をクリアするのが最善です。なぜなら、Moduleオブジェクトのサイズは数十メガバイトになる可能性があるからです。同様に、使用されていない場合は、すべてのXHRファイル、アセットデータ、大きなスクリプトが参照されないようにしてください。ブラウザのメモリプロファイリングツールと、Firefoxのabout:memory
ページを確認してメモリプロファイリングを行い、メモリが無駄になっていないことを確認してください。
可能な限り最高のユーザーエクスペリエンスを提供するには、ページが失敗する可能性のあるさまざまな方法を考慮し、ユーザーに適切なエラーレポートを提供する必要があります。特に、ベストプラクティスについては、次のチェックリストに従ってください。
できるだけ早くエラーを検出することを目指してください。ユーザーにとって大きな不満の原因となるのは、ユーザーのシステムが特定のページを実行する準備ができていないのに、そのエラーが100MBのアセットのダウンロードを待った後になって初めて明らかになるようなシナリオです。たとえば、ページを実際に読み込む前に、必要なヒープメモリを事前に割り当ててみてください。このようにすれば、メモリ割り当てに失敗した場合、失敗は即座に発生し、アセットのダウンロードを試みる必要はまったくありません。
特定のブラウザがサポートされていないことがわかっている場合、可能であれば、navigator.userAgent
フィールドを読み取ってそのブラウザのユーザーを制限するという誘惑に抵抗してください。たとえば、ページでWebGL 2が必要だが、Safariがそれをサポートしていないことがわかっている場合、次のタイプのチェックを使用してSafariユーザーを除外しないでください。
if (navigator.userAgent.indexOf('Safari') != -1) alert('Your browser does not support WebGL 2!');
代わりに、実際のエラーを検出します。
if (!canvas.getContext('webgl2')) alert('Your browser does not support WebGL 2!'); // And look for webglcontextcreationerror here for an error reason.
このようにすることで、後で特定の機能のサポートが利用可能になったときに、ページは将来互換性を維持します。
さまざまなエラーケースを事前にシミュレートして、さまざまな問題やブラウザの制限をテストします。たとえば、Firefoxでは、about:config
に移動して、プリファレンスwebgl.enable-webgl2
をfalse
に設定することで、WebGL 2を手動で無効にすることができます。これにより、そのようなシナリオでページがユーザーに提示するエラーレポートの種類をデバッグできます。テストのためにWebGLのサポートを完全に無効にするには、プリファレンスwebgl.disabled
をtrue
に設定します。
IndexedDBを使用する場合は、ユーザーが空きディスク容量を使い果たそうとしているか、ドメインの許可されたクォータを使い果たそうとしている場合に、クォータ超過エラーを処理する準備をしておいてください。
WebAssembly.Memory
オブジェクトと、プリロードされたファイルパッケージ(使用している場合)に対して非現実的に多くのメモリを割り当てることで、メモリ不足エラーをシミュレートします。メモリ不足エラーが正しくフラグ付けされ(そしてユーザーまたはエラーデータベースに報告される)ことを確認してください。
XHRダウンロードをプログラムで中止したり、ネットワークアクセスを物理的に切断したり、Fiddlerなどの外部ツールを使用したりすることで、ダウンロードタイムアウトを侵入的にシミュレートします。これらのタイプのツールは、予期しない多くのエラーケースを表示し、そのようなシナリオのエラー処理パスが望ましいものであることを診断するのに役立ちます。
ネットワークリミッターツールを使用して、ダウンロードまたはアップロードの帯域幅速度を制限し、低速なネットワーク接続をシミュレートします。これにより、ネットワーク転送のタイミング依存性に関するバグが明らかになる可能性があります。たとえば、小さなネットワーク転送は、大きなネットワーク転送の前に完了すると暗黙的に想定される場合がありますが、必ずしもそうとは限りません。
ページをローカルで開発する場合は、file://
URLではなく、ローカルWebサーバーを使用してテストを実行してください。Emscriptenソースツリー内のスクリプトemrun.py
は、この目的のためにアドホックWebサーバーとして機能するように設計されています。Emrunは、gzip圧縮ファイル(接尾辞.gz
付き)を提供するように事前に構成されており、Large-Allocation
ヘッダーのサポートを有効にし、コンパイル済みページのコマンドライン自動化実行を許可します。
コンパイル済みのasm.jsおよびWebAssemblyコードを呼び出すエントリポイント内から発生するすべての例外をキャッチします。コンパイル済みコードは、3つの異なる例外クラスをスローできます。
C++例外で、整数として送出され、C++プログラムで捕捉されないもの。この整数は、送出されたオブジェクトへのポインタを含むアプリケーションヒープ内のメモリの位置を示しています。
Emscriptenランタイムが
abort()
関数を呼び出すことによって発生する例外。これらは、コンパイルされたコードの実行が回復できない致命的なエラーに相当します。たとえば、無効な関数ポインタを呼び出す場合に発生する可能性があります。コンパイルされたWebAssemblyコードによって発生するトラップ。これらは、WebAssembly VMからの致命的なエラーに相当します。たとえば、ゼロによる整数除算を実行する場合、または大きな浮動小数点数を整数に変換する際に、浮動小数点がその整数型で表現可能な数の範囲外である場合に発生する可能性があります。
window.onerror
スクリプトを実装することで、ページに最終的な「キャッチオール」エラーハンドラを実装します。これは、ページで発生した例外を他のソースが処理しなかった最後の手段として呼び出されます。MDNのwindow.onerrorドキュメントを参照してください。
HTMLページを「フリーズ」させて、エラーメッセージをウェブページコンソールに埋め込まないようにしてください。ほとんどのユーザーはそこでエラーメッセージを見つける方法を知らないからです。メインのHTMLページで、できれば対処方法のヒント付きで、ユーザーに意味のあるエラーレポートを提供するように努めてください。ブラウザのバージョンやGPUドライバの更新、またはディスクの空き容量の確保がページの実行に役立つ可能性がある場合は、ユーザーに試せることを知らせます。問題のエラーが完全に予期しないものである場合は、問題を報告するためのリンクまたはメールアドレスを提供することを検討してください。
読み込みの進行状況がまだ続いているかどうか、次に何が起こるかをユーザーに示すために、意味のあるインタラクティブな読み込み進行状況インジケータを提供します。「まだ読み込み中なのか、それともハングしたのか?」という状態にユーザーを導かないようにしてください。
サイトを公開する前にテストマトリックスを計画する際には、以下の項目を確認することをお勧めします。
ウェブページの動作は、最上位ウィンドウとして実行する場合とiframe内で実行する場合で微妙に異なる場合があります。これらのシナリオが適用される場合は、両方のシナリオをテストしてください。
32ビットブラウザと64ビットブラウザの両方でテストし、特に32ビットブラウザでメモリ不足のシナリオをシミュレートします。
HTTP Cross-Origin Access Controlのルールとそのルールがホスティングしているサイトアーキテクチャにどのように関連しているかを認識してください。
Content Security Policyのルールを認識し、サイトが実行する予定のCSPポリシーの種類をメモしてください。
ブラウザが課すMixed Content Securityの制限を意識してください。
プライベートブラウジング(シークレット)モードでもサイトが正常に動作することを確認してください。たとえば、これにより、サイトがIndexedDBにデータを永続化することが防止されます。
バックグラウンドタブに配置した場合でもページが正常に動作することをテストします。blur
、focus
、visibilitychange
DOMイベントを使用して、ページの非表示と表示イベントに対応します。これは、オーディオ再生を実行するアプリケーションにとって特に関連します。
ページでWebGLを使用する場合は、WebGLコンテキストの喪失イベントを適切に処理できることを確認してください。テスト時にプログラムでコンテキストの喪失イベントをトリガーするには、WebGL_lose_context開発者エクステンションを使用します。
異なるwindow.devicePixelRatio
(DPI)設定のディスプレイで、特にWebGLを使用する場合に、ページが意図したとおりに動作することを確認します。Khronos.org: HandlingHighDPIを参照してください。WindowsとmacOSでは、デスクトップのディスプレイ倍率設定を変更して、ブラウザが報告するwindow.devicePixelRatio
の異なる値をテストしてみてください。
さまざまなページのズームレベルでサイトレイアウトが壊れないことをテストします。特に、ブラウザウィンドウが既にプリズームされている状態でページに移動する場合です。
同様に、ブラウザウィンドウのサイズ変更時、またはブラウザウィンドウを非常に小さいサイズ、非常に大きいサイズ、または不均衡なアスペクト比に最初にサイズ変更した状態でサイトにアクセスした場合でも、ページレイアウトが壊れないことを確認します。
特にモバイルをターゲットとする場合は、モバイルで適切に動作するサイトレイアウトを開発する方法について<meta viewport>タグを認識してください。
ページでWebGLを使用する場合は、ターゲットプラットフォームでさまざまなGPUをテストします。特に、必要なWebGL拡張機能がないこと、および圧縮テクスチャ形式のサポートがないことをシミュレートした場合のサイトの動作を確認します。
requestAnimationFrame()
API(つまりemscripten_set_main_loop()
関数)を使用してレンダリングを駆動する場合、関数が呼び出される頻度は常に60Hzとは限らず、実行時に変化する可能性があることに注意してください。たとえば、マルチモニタ設定でブラウザウィンドウを1つのディスプレイから別のディスプレイに移動する場合、ディスプレイのリフレッシュレートが異なる場合などです。75Hz、90Hz、100Hz、120Hz、144Hz、200Hzなどの更新間隔がますます一般的になっています。
ページが必要とする可能性のある特別なAPI(例:ゲームパッド、加速度、タッチイベント)がないことをシミュレートし、そのような場合でも適切なエラーフローが処理されることを確認します。
役立つヒントや提案がありましたら、Emscriptenバグトラッカーまたはemscripten-discussメーリングリストにフィードバックを投稿して、このガイドの改善にご協力ください。