Blazor WASMにてAOTコンパイルを使いつつBrotli圧縮配信で最速起動を目指す
はじめに
Blazor WebAssembly(WASM)、いいですよね。
しかしながら、実際の実装では内部処理が複雑であったり、性能が低いデバイスでの利用を前提とすることが多く、SPAの動作がもっさりと感じることがあります。(私が関わったプロジェクトでは偶然そういう場面が多かっただけかもしれませんが。。)
そのような場合、AOT(Ahead-of-Time)コンパイルを使用してリリースすることがありますが、今度は初回接続時のダウンロード起動に異常に時間がかかるといった問題が発生し、なかなかうまくいきません。
そこで、AOTコンパイルを使用しつつ、さらに圧縮配信を活用することで、動作の高速化と初回ダウンロードの短縮も行っておいしいとこどりをしよう、というなんとも欲張りな発想から本記事の作成にいたります。
Blazor WASMについて
WebAssemblyを使ってブラウザ内で処理が行われます。
アプリケーション全体でこのモードしか利用しないケースでは、サーバーサイドの処理が完全に不要になるため、静的なWebサーバー上にBlazorアプリケーションを配置することもできます。
(上記は「豆蔵デベロッパー」様より引用)
Blazor WASMは、WebAssembly技術を活用することで、ブラウザ上で.NETランタイムを実行し、JavaScriptに依存せずに高性能なアプリケーション開発が可能となります。これにより、サーバーとの常時接続が不要なオフライン対応のアプリケーションも実現できます。
ですが実運用では、DBなどと連携することがほとんどと思いますので、別途WebAPIを用意してクライアントブラウザ上で動作するBlazor WASMアプリがhttp通信でWebAPIと通信することになるかと考えます。
AOTコンパイルとは
Blazor WebAssembly(WASM)におけるAOT(Ahead-of-Time)コンパイルとは、.NETコードを事前にWebAssembly(WASM)形式にコンパイルする手法です。これにより、アプリケーションの実行時にインタープリターを介さず、ネイティブなWASMコードとして直接実行されるため、パフォーマンスの向上が期待できます。
(上記はAI検索より引用)
メリット
-
実行時のパフォーマンス向上: インタープリターを介さずにネイティブコードとして実行されるため、処理速度が向上します。
-
CPU負荷の高い処理に適している: 画像処理や数値計算など、計算量の多い処理で効果を発揮します。
デメリット
-
アプリサイズの増加: AOTコンパイルにより生成されるWASMコードは、ILコードよりもサイズが大きくなる傾向があります。
-
初回ロード時間の延長: アプリサイズの増加に伴い、初回のダウンロード時間が長くなることがあります。
ということで、一度、初回接続のダウンロードが終わってしまえば、然したる問題はないのですが、デメリットに記載のようにダウンロード時間が、ひっっじょうに長く遅く、大変ストレスとなります。
配信をするアプリサイズにもよりますが、経験上、2.4GHzを使う無線通信での一斉ダウンロードやスペックが十分でないデバイスでのダウンロードは、30秒~1分程度かかってしまうので、運用によっては耐えられない、由々しき課題となります。
Brotli圧縮とは
Brotli圧縮は、Googleが開発した高効率な可逆圧縮アルゴリズムで、主にウェブコンテンツの配信に利用されています。この技術は、従来のgzip圧縮と比較して、より高い圧縮率と同等の展開速度を提供し、ウェブページの読み込み速度向上や通信コストの削減に寄与します。
(上記はAI検索より引用)
AOTコンパイルを使用する点は外せないし、あとはロード時間を可能な限り短くすることを考えるわけですが、そんなときに出会ったのがBrotli圧縮です。解決策の調査の果てに初めてBrotli圧縮にたどり着いたときは、求めていた解決策にぴったりすぎて、神か何かに導かれてるとさえ感じました。
Brotli圧縮に必要な条件は以下2点です。
- https通信の使用
- クライアントブラウザの対応
(Chrome 49以降、Firefox 44以降、Edge 15以降、Safari 11以降)
環境
- .NET 9
- Blazor WASM スタンドアロン
- デプロイ先
以下2パターンでBrotli圧縮配信のダウンロードを試します。- IIS
- Azure Static Web Apps (SWA)
下記はデプロイを行うサンプルBlazorアプリです。わざとアプリサイズを大きくするために、UIフレームワークのRadzenを使用しています。(CopilotにざっくりテキトーにRadzenを使用するように指示して作成したので、見た目感が微妙なのはご容赦ください。。)
動作確認
発行時に.br拡張子のファイルが出来上がりますが、.brファイルこそがBrotli圧縮配信で使用されるファイルです。
まずは、AOTコンパイルをONとして、ローカルPC上のフォルダ上に発行を行った後に、IIS、Azure Static Web Appsに配置デプロイを行うとします。
AOTコンパイル有効化と発行
AOTコンパイルとして発行を行います。前項までで記載の通り、AOTコンパイルを行うと事前に実行されるファイルを作成しますので、Webアプリとして配信を行うコンテンツのサイズが大きくなります。
VSのWebアプリプロジェクトの発行より、フォルダへ発行とします。
VS2022の公開設定では、AOTコンパイルはチェックONとします。
AOTコンパイルはチェックONとすると、作成される以下pubxml上のRunAOTCompilationの値がtrueとなります。
公開設定で生成されるpubxml
<?xml version="1.0" encoding="utf-8"?>
<!-- https://go.microsoft.com/fwlink/?LinkID=208121. -->
<Project>
<PropertyGroup>
<DeleteExistingFiles>true</DeleteExistingFiles>
<ExcludeApp_Data>false</ExcludeApp_Data>
<LaunchSiteAfterPublish>true</LaunchSiteAfterPublish>
<LastUsedBuildConfiguration>Release</LastUsedBuildConfiguration>
<LastUsedPlatform>Any CPU</LastUsedPlatform>
<PublishProvider>FileSystem</PublishProvider>
<PublishUrl>bin\Release\net9.0\browser-wasm\publish\</PublishUrl>
<WebPublishMethod>FileSystem</WebPublishMethod>
<_TargetId>Folder</_TargetId>
<SiteUrlToLaunchAfterPublish />
<TargetFramework>net9.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<RunAOTCompilation>true</RunAOTCompilation><!-- AOTコンパイルを行う場合はこのKeyValueがtrue -->
<ProjectGuid>XXX</ProjectGuid><!-- 自動付与される -->
<SelfContained>true</SelfContained>
</PropertyGroup>
</Project>
「発行」を押下でリリースビルド後、AOTコンパイルとして指定フォルダへ発行を行います。
発行を行った成果物のフォルダサイズは86.5Mbとのこと。帯域のか細いネットワークでダウンロードするとなると、それなりに大きいサイズかと思います。
1. IISでの動作確認
IISでBrotli圧縮配信を行う場合は以下2点が必要です。
- 圧縮配信用にIIS拡張機能のインストール
- 圧縮配信用にweb.configの変更
IIS圧縮配信の使用
IISを動作させるサーバー上で、iiscompression_amd64.msiを実行し、IIS拡張機能をインストールします。
インストールでの選択肢はデフォルトで問題ありません。
web.configの変更
上記の「Brotli と Gzip の圧縮」項目よりリンク先のweb.configをダウンロードし、IIS上で公開している本サンプルアプリのweb.configと差し替えます。
いくつかの.gz,.brファイルも配信対象にしているような内容になっているかと思います。
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<system.webServer>
<staticContent>
<remove fileExtension=".dll" />
<remove fileExtension=".json" />
<remove fileExtension=".woff" />
<remove fileExtension=".woff2" />
<remove fileExtension=".wasm" />
<mimeMap fileExtension=".json" mimeType="application/json" />
<mimeMap fileExtension=".woff" mimeType="font/woff" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
<mimeMap fileExtension=".dat" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dll" mimeType="application/octet-stream" />
<mimeMap fileExtension=".wasm" mimeType="application/wasm" />
<mimeMap fileExtension=".blat" mimeType="application/octet-stream" />
<mimeMap fileExtension=".js.gz" mimeType="application/javascript" />
<mimeMap fileExtension=".dat.gz" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dll.gz" mimeType="application/octet-stream" />
<mimeMap fileExtension=".json.gz" mimeType="application/json" />
<mimeMap fileExtension=".wasm.gz" mimeType="application/wasm" />
<mimeMap fileExtension=".blat.gz" mimeType="application/octet-stream" />
<mimeMap fileExtension=".html.gz" mimeType="text/html" />
<mimeMap fileExtension=".css.gz" mimeType="text/css" />
<mimeMap fileExtension=".ico.gz" mimeType="image/x-icon" />
<mimeMap fileExtension=".svg.gz" mimeType="image/svg+xml" />
<mimeMap fileExtension=".js.br" mimeType="application/javascript" />
<mimeMap fileExtension=".dat.br" mimeType="application/octet-stream" />
<mimeMap fileExtension=".dll.br" mimeType="application/octet-stream" />
<mimeMap fileExtension=".json.br" mimeType="application/json" />
<mimeMap fileExtension=".wasm.br" mimeType="application/wasm" />
<mimeMap fileExtension=".blat.br" mimeType="application/octet-stream" />
<mimeMap fileExtension=".html.br" mimeType="text/html" />
<mimeMap fileExtension=".css.br" mimeType="text/css" />
<mimeMap fileExtension=".ico.br" mimeType="image/x-icon" />
<mimeMap fileExtension=".svg.br" mimeType="image/svg+xml" />
</staticContent>
<httpCompression>
<dynamicTypes>
<remove mimeType="text/*" />
<remove mimeType="application/javascript" />
<remove mimeType="image/svg+xml" />
</dynamicTypes>
<staticTypes>
<remove mimeType="text/*" />
<remove mimeType="application/javascript" />
<remove mimeType="image/svg+xml" />
</staticTypes>
</httpCompression>
<rewrite>
<outboundRules rewriteBeforeCache="true">
<rule name="Add Vary Accept-Encoding" preCondition="PreCompressedFile" enabled="true">
<match serverVariable="RESPONSE_Vary" pattern=".*" />
<action type="Rewrite" value="Accept-Encoding" />
</rule>
<rule name="Add Encoding Brotli" preCondition="PreCompressedBrotli" enabled="true" stopProcessing="true">
<match serverVariable="RESPONSE_Content_Encoding" pattern=".*" />
<action type="Rewrite" value="br" />
</rule>
<rule name="Add Encoding Gzip" preCondition="PreCompressedGzip" enabled="true" stopProcessing="true">
<match serverVariable="RESPONSE_Content_Encoding" pattern=".*" />
<action type="Rewrite" value="gzip" />
</rule>
<preConditions>
<preCondition name="PreCompressedFile">
<add input="{HTTP_URL}" pattern="\.(gz|br)$" />
</preCondition>
<preCondition name="PreCompressedBrotli">
<add input="{HTTP_URL}" pattern="\.br$" />
</preCondition>
<preCondition name="PreCompressedGzip">
<add input="{HTTP_URL}" pattern="\.gz$" />
</preCondition>
</preConditions>
</outboundRules>
<rules>
<rule name="Serve subdir">
<match url=".*" />
<action type="Rewrite" url="wwwroot\{R:0}" />
</rule>
<rule name="Rewrite brotli file" stopProcessing="true">
<match url="(.*)"/>
<conditions>
<add input="{HTTP_ACCEPT_ENCODING}" pattern="br" />
<add input="{REQUEST_FILENAME}" pattern="\.(js|dat|dll|json|wasm|blat|htm|html|css|ico|svg)$" />
<add input="{REQUEST_FILENAME}.br" matchType="IsFile" />
</conditions>
<action type="Rewrite" url="{R:1}.br" />
</rule>
<rule name="Rewrite gzip file" stopProcessing="true">
<match url="(.*)"/>
<conditions>
<add input="{HTTP_ACCEPT_ENCODING}" pattern="gzip" />
<add input="{REQUEST_FILENAME}" pattern="\.(js|dat|dll|json|wasm|blat|htm|html|css|ico|svg)$" />
<add input="{REQUEST_FILENAME}.gz" matchType="IsFile" />
</conditions>
<action type="Rewrite" url="{R:1}.gz" />
</rule>
<rule name="SPA fallback routing" stopProcessing="true">
<match url=".*" />
<conditions logicalGrouping="MatchAll">
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
</conditions>
<action type="Rewrite" url="wwwroot\" />
</rule>
</rules>
</rewrite>
</system.webServer>
</configuration>
ダウンロード時間実測
ブラウザで公開しているBlazorサンプルアプリにアクセスし、初回アクセス時のファイルダウンロード時間を確認します。
以下ブラウザの開発者ツールのネットワークタブより実測します。
Brotli圧縮無し
ダウンロードサイズは大きいファイルで29.4MBでした。
Brotli圧縮有効
Brotoli圧縮が適用されている場合は、httpヘッダーのContent-Encoding列にbrと記載されます。
ダウンロードサイズは大きいファイルで5.9MBでした。
圧縮率はファイルの内容によるとは思いますが、約1/5となり、効果のほどは申し分ないです。
2. Azure Static Web Appsでの動作確認
Azure Static Web AppsはデフォルトでBrotli圧縮配信に対応しているため、明示的に発行ファイルを制限せずbrファイルをデプロイしている、かつブラウザがBrotliに対応していれば自動でBrotliが適用されます。ですので、実運用で確認してみるとすでに圧縮配信に対応していることもあるかと思いますが、本記事ではAzure Static Web AppsでもBrotli圧縮配信がされていることを再確認します。
Azure Static Web Apps のリソース作成
Azure Portalにサインインし、Azure Static Web AppsのアプリリソースをAzure上に作成します。
次項で説明しますが、「デプロイの詳細」項目は、「その他」選択します。
作成したAzure Static Web Appsの概要ページ内、「デプロイトークンの管理」からデプロイトークンを保持しておきます。デプロイトークンは、名前の通りアプリのデプロイ時に使用します。
Azure Static Web Apps CLI でデプロイ
本記事では、Azure Static Web Apps CLIを使用してデプロイを行います。正攻法ではgithub Actionを設定し、github RepositoryへのプッシュをトリガーにAzure Static Web Appsへデプロイを行うことが主と考えます。が、今回はAzure Static Web Apps CLIに頑張ってもらうとします。決してリポジトリを作ったり、プッシュすることが面倒だったわけではありません。断じてありえません。
node.jsはインストールされてる前提とします。
以下コマンドにて、Azure Static Web Apps CLIをインストールします。
npm install -g @azure/static-web-apps-cli
Azure Static Web Appsのデプロイは以下コマンドで行います。
swa deploy <デプロイを行いたいwwwrootのフォルダパス> --deployment-token <取得したトークン>
ダウンロード時間実測
Blazorサンプルアプリのデプロイを行ったAzure Static Web Appsへブラウザで接続し、初回アクセス時のファイルダウンロード時間を実測します。
brファイルを除いてデプロイ
明示的にbrファイルを除いてAzure Static Web Appsへデプロイした場合は、通常の配信となります。今回は力技で、発行後のファイルの中から*.brファイルを検索で削除を行った後のフォルダwwwrootをAzure Static Web Appsへデプロイするとします。
いくつかのファイルはEncodeingがbrとなってますが、発行を行った成果物はEncoding指定なしです。(私の操作ミスによる削除漏れの可能性もあります。。)
ダウンロードサイズは大きいファイルで29.4MBでした。
brファイルを含めてデプロイ
Brotoli圧縮が適用されている場合は、httpヘッダーのContent-Encoding列にbrと記載されます。
ダウンロードサイズは大きいファイルで5.9MBでした。
IISの場合と同じく、大きいファイルのサイズは約1/5となり、初回ダウンロード時間が短縮されていることがわかります。
まとめ
Blazor WASM スタンドアロンアプリにて、AOTコンパイルかつBrotli圧縮を使用した際の初回ダウンロード時間について検証しました。
最近のBlazorではInteractive Auto モードが追加され、初回起動の遅延に対して速度改善が図られるとは思いますが、いずれにしてもBrotli圧縮を使用すればパフォーマンス改善に繋がることは間違いないと考えます。
お役に立てれば幸いです。
参考
- Blazor入門:ASP.NET Coreで始める最新Web開発
- Microsoft Learn ASP.NET Core Blazor WebAssembly ビルド ツールと事前 (AOT) コンパイル
- Brotli - wiki
- Qiita Blazor WebAssembly 製マークシートシステム Mark2 のダウンロードサイズ 100 MB を、20 MB に削減する
Discussion