Blazor WebAssembly 用のライブラリに IsTrimmable = true を指定して使われないコードを削除、コンテンツサイズを削減
Blazor WebAssembly 用の Razor クラスライブラリを作るとき、プロジェクトファイル (*.csproj) 中に以下のように IsTrimmable
MSBuild プロパティに true
を指定しておくと、
...
<PropertyGroup>
...
<IsTrimmable>true</IsTrimmable>
...
</PropertyGroup>
...
そのライブラリを使用している Blazor WebAssembly の発行時、そのライブラリのアセンブリ (.dll) 中に含まれる各種コードのうち、どこからも参照されていない・使われていない機能のコードが削除されて、そのライブラリのアセンブリ (.dll) のサイズが小さくなります。JavaScript モジュールバンドラーで言うところの "Tree Shaking" に相当する仕組みですね。Blazor WebAssembly プログラミングにおいては、"Tree Shaking" とは呼称せず、"アセンブリのトリミング (Trimming)" と呼ぶようです。公式ドキュメントサイトでの説明は下記リンク先にあります。
一般的に Blazor WebAssembly の弱点のひとつとして、コンテンツサイズがどうしても膨らみがちになるという点があります。しかしこのアセンブリのトリミングを上手く使うことで、少しでもコンテンツサイズを縮小できるといいですね。
どれくらい .dll のサイズが小さくなるか試してみた
実際のところ、どのようにサイズが小さくなるか、サンプルプログラムのレベルですが、試してみました。
まずはトリミングの有無でどれだけ .dll ファイルのサイズが変わるかを試すための Razor クラスライブラリプロジェクトを新規作成します。今回は対象の .NET のバージョンは 7 とし、プロジェクト名は "RazorClassLib1" としてみました。
dotnet new razorclasslib -n RazorClassLib1 -f net7.0
そしてもうひとつ別に .NET 7 を対象とした Blazor WebAssembly プロジェクトを新規作成し、その Blazor WebAssembly プロジェクトから、先に作成した "RazorClassLib1" をプロジェクト参照するようにします。下図のような感じですね。
上図からもわかりますが、dotnet new razoclasslib ~
コマンドで作成された Razor クラスライブラリプロジェクトには、
- Razor コンポーネントがひとつ (
Component1
) と、 - JavaScript 相互運用機能を介して、Web ブラウザの prompt でユーザー入力を受け付けるサービスがひとつ (
ExampleJsInterp
)
の、2つのクラスが収録されています。Blazor WebAssembly プロジェクトではこのうち、Razor コンポーネント Component1
だけを使用して、サービス ExampleJsInterop
は未使用とします。
この状態で Blazor WebAssembly プロジェクトを Release 構成で発行し、発行先フォルダに配置される RazorClassLib1.dll
のサイズが、トリミングの有り無しでどのように変化するか、見てみました。
トリミングなしの場合
まずはトリミングの指定なし、つまり、以上の実装手順のまま、Blazor WebAssmbly プロジェクトを Release 構成で発行した場合です。この場合、発行先フォルダに配置された RazorClassLib1.dll
のファイルサイズは 8KB になりました。Brotli 圧縮後で 3.25 KB 程です。
PS> ls RazorClassLibrary1.*
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 12/10/2022 9:54 AM 8192 RazorClassLibrary1.dll
-a--- 12/10/2022 9:59 AM 3332 RazorClassLibrary1.dll.br
-a--- 12/10/2022 9:59 AM 3746 RazorClassLibrary1.dll.gz
念のため、.NET のアセンブリからソースコードを逆生成するデコンパイラのひとつである ILSpy を使って、この発行先フォルダに配置された RazorClassLib1.dll
の内容を確認してみました。すると、この Blazor WebAssembly アプリでは使用していない ExampleJsInternal
サービスクラスも、RazorClassLib1.dll
内に収録されていることがわかります (下図、ILSpy によるデコンパイル結果のスクリーンショット)。
Razor コンポーネントのソースコード _Imports.razor
も、その目的はすべてのコンポーネントで予め名前空間を開いておく用途が主目的とはいえ .razor ファイルには違いないことから、ちゃんと (?) Razor コンポーネントにコンパイルされてアセンブリ内に収録されているのも興味深いところです。
トリミングありの場合
続けて、Razor クラスライブラリ側のプロジェクトファイル RazorClassLib1.csproj
に <IsTrimmable>true</IsTrimmable>
の指定を書き加えてから、改めて、これをプロジェクト参照している Blazor WebAssembly プロジェクトを Release 構成で発行しなおします。
そうして発行先フォルダに配置された RazorClassLib1.dll
のファイルサイズを確認してみると 4.5KB、Brotli 圧縮後で約 1.43 KB になっていました。
PS> ls RazorClassLibrary1.*
Mode LastWriteTime Length Name
---- ------------- ------ ----
-a--- 12/10/2022 10:30 AM 4608 RazorClassLibrary1.dll
-a--- 12/10/2022 10:31 AM 1468 RazorClassLibrary1.dll.br
-a--- 12/10/2022 10:31 AM 1712 RazorClassLibrary1.dll.gz
トリミング有効前のファイルサイズに対し、60% 以下のサイズに削減されました。Brotli 圧縮後のサイズで比べてもトリミング有効後は有効前と比較して 40% 少々のサイズとなっています。
ILSpy で確認したところ、Blazor WebAssembly アプリから参照されていない _Imports
や ExampleJsInterop
クラスは RazorClassLib1.dll
内から削除されていることがわかります (下図 ILSpy のスクリーンショット)。
たしかに、トリミングを有効にすると、どこからも参照されていないコードを .NET アセンブリファイル (.dll) 内から除去することによって、アセンブリファイルのサイズをなるべく小さく抑える効果があることがわかりました。
トリミングを有効化することの注意点
ただし、トリミングの有効化には、当然のことながら注意が必要です。
例えば JavaScript 側から呼ばれるだけのメソッドが定義されていた場合を考えてみて下さい。言い換えると、そのメソッドは発行時に生成される .NET のコード間の呼び出し関係上はどこからも参照されていません。つまり、トリミングが有効だと発行時にそのメソッドが .dll 内から除去されてしまいます。結果として、実行時に、そのメソッドが JavaScript 側から呼び出されたタイミングで実行時例外となってしまいます。
トリミングを有効としつつ、上記のような事態を避けるには、開発者がソースコード上で明示的に「このメンバーは、トリミングが有効でも、アセンブリから除去したダメですよ」と指定する必要があります。具体的には、[DynamicDependency]
属性を用いてその旨をコーディングします。
この [DynamicDependency]
属性の指定をはじめ、ライブラリ向けにトリミングの動作を調整する各種注意ポイントや実装方法については、下記リンク先の公式ドキュメントサイトをよくよく参照されるのがよいかと思います。
あと、この記事で確認したのは、.NET 7 SDK における Blazor WebAssembly アプリ開発における挙動です。.NET 6 など他の .NET バージョンですと挙動が変わってきますので、その点もご注意ください。
まとめ
一般に Razor クラスライブラリは、再利用可能なコンポーネントやサービス、機能をいろいろと収録することになります。しかしそのいっぽうで、個々の Blazor WebAssembly アプリケーションプロジェクトでは、その Razor クラスライブラリに含まれているすべてのコードを使用・参照することは希でしょう。そのような状況において、アセンブリのトリミング機能は、最終的に発行で生成されるコードから未使用のコードを除去することによって、コンテンツサイズの削減に寄与することが期待できます。
実際、.NET 7 SDK 標準の Razor クラスライブラリプロジェクトテンプレートが生成したライブラリには、コンポーネント1つとサービス1つの、2つのクラスが実装されていますが、このうちコンポーネントだけを参照している .NET 7 Blazor WebAssembly プロジェクトの場合、発行後に生成されたライブラリのアセンブリのサイズは、トリミング有効前は 8.0 KB だったところ、トリミング有効後は 4.5 KB にまで削減されました。
もっとも、アセンブリのトリミングはうまく使いこなさないと、本当は必要なコードがトリミングによって除去されてしまって開発中は問題ないのに運用環境で実行時例外が発生する、といった危険もあります。 とはいえ、総コンテンツサイズにおいて他のフレームワークより不利な Blazor WebAssembly においては、ぜひ活用したい機能であるとも言えます。
Blazor では、再利用可能なコンポーネントや機能をライブラリ化、NuGet パッケージ化して配付し再利用するのもすごく簡単ですから (※こちらの Qiita 記事など参照)、それとあわせて、アセンブリのトリミングの有効化にも取り組めるといいですね!
Learn, Practice, Share!