Blazor WebAssembly - どこからも参照されていないコードが削除されるようにして .dll のサイズを小さくする

## Blazor WebAssembly 用のライブラリに IsTrimmable = true を指定して使われないコードを削除、コンテンツサイズを削減

Blazor WebAssembly 用の Razor クラスライブラリを作るとき、プロジェクトファイル (*.csproj) 中に以下のように **`IsTrimmable` MSBuild プロパティに `true` を指定**しておくと、

```xml:*.csproj
...
<PropertyGroup>
  ...
  <IsTrimmable>true</IsTrimmable>
  ...
</PropertyGroup>
...
```
そのライブラリを使用している Blazor WebAssembly の発行時、そのライブラリのアセンブリ (.dll) 中に含まれる各種コードのうち、**どこからも参照されていない・使われていない機能のコードが削除**されて、そのライブラリのアセンブリ (.dll) の**サイズが小さく**なります。JavaScript モジュールバンドラーで言うところの ["Tree Shaking"](https://developer.mozilla.org/ja/docs/Glossary/Tree_shaking) に相当する仕組みですね。Blazor WebAssembly プログラミングにおいては、"Tree Shaking" とは呼称せず、"アセンブリの**トリミング (Trimming)**" と呼ぶようです。公式ドキュメントサイトでの説明は下記リンク先にあります。

https://learn.microsoft.com/ja-jp/aspnet/core/blazor/host-and-deploy/configure-trimmer

一般的に Blazor WebAssembly の弱点のひとつとして、コンテンツサイズがどうしても膨らみがちになるという点があります。しかしこのアセンブリのトリミングを上手く使うことで、少しでもコンテンツサイズを縮小できるといいですね。

## どれくらい .dll のサイズが小さくなるか試してみた

実際のところ、どのようにサイズが小さくなるか、サンプルプログラムのレベルですが、試してみました。

まずはトリミングの有無でどれだけ .dll ファイルのサイズが変わるかを試すための Razor クラスライブラリプロジェクトを新規作成します。今回は対象の .NET のバージョンは 7 とし、プロジェクト名は "RazorClassLib1" としてみました。

```
dotnet new razorclasslib -n RazorClassLib1 -f net7.0
```

そしてもうひとつ別に .NET 7 を対象とした Blazor WebAssembly プロジェクトを新規作成し、その Blazor WebAssembly プロジェクトから、先に作成した "RazorClassLib1" をプロジェクト参照するようにします。下図のような感じですね。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/c3b1751d-b563-3f8d-df16-49f86b2b30cb.png)
上図からもわかりますが、`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 程です。

```powershell
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](https://github.com/icsharpcode/ILSpy) を使って、この発行先フォルダに配置された `RazorClassLib1.dll` の内容を確認してみました。すると、この Blazor WebAssembly アプリでは使用していない `ExampleJsInternal` サービスクラスも、`RazorClassLib1.dll` 内に収録されていることがわかります (下図、ILSpy によるデコンパイル結果のスクリーンショット)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/8edaf3af-af5e-5ed0-98f3-fa3050808bc1.png)
Razor コンポーネントのソースコード `_Imports.razor` も、その目的はすべてのコンポーネントで予め名前空間を開いておく用途が主目的とはいえ .razor ファイルには違いないことから、ちゃんと (?) Razor コンポーネントにコンパイルされてアセンブリ内に収録されているのも興味深いところです。

### トリミングありの場合

続けて、Razor クラスライブラリ側のプロジェクトファイル `RazorClassLib1.csproj` に `<IsTrimmable>true</IsTrimmable>` の指定を書き加えてから、改めて、これをプロジェクト参照している Blazor WebAssembly プロジェクトを Release 構成で発行しなおします。

そうして発行先フォルダに配置された `RazorClassLib1.dll` のファイルサイズを確認してみると **4.5KB**、Brotli 圧縮後で約 1.43 KB になっていました。

```powershell
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 のスクリーンショット)。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/58450/b2fb9ba2-8d76-085e-fb28-6eac213bbe4b.png)
たしかに、トリミングを有効にすると、どこからも参照されていないコードを .NET アセンブリファイル (.dll) 内から除去することによって、アセンブリファイルのサイズをなるべく小さく抑える効果があることがわかりました。

## トリミングを有効化することの注意点

ただし、トリミングの有効化には、当然のことながら注意が必要です。
例えば JavaScript 側から呼ばれるだけのメソッドが定義されていた場合を考えてみて下さい。言い換えると、そのメソッドは発行時に生成される .NET のコード間の呼び出し関係上はどこからも参照されていません。つまり、トリミングが有効だと発行時にそのメソッドが .dll 内から除去されてしまいます。結果として、実行時に、そのメソッドが JavaScript 側から呼び出されたタイミングで実行時例外となってしまいます。

トリミングを有効としつつ、上記のような事態を避けるには、開発者がソースコード上で明示的に「このメンバーは、トリミングが有効でも、アセンブリから除去したダメですよ」と指定する必要があります。具体的には、`[DynamicDependency]` 属性を用いてその旨をコーディングします。

この `[DynamicDependency]` 属性の指定をはじめ、ライブラリ向けにトリミングの動作を調整する各種注意ポイントや実装方法については、下記リンク先の公式ドキュメントサイトをよくよく参照されるのがよいかと思います。

https://learn.microsoft.com/ja-jp/dotnet/core/deploying/trimming/prepare-libraries-for-trimming

あと、この記事で確認したのは、.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 記事](https://qiita.com/jsakamoto/items/52df935950e925f6f4e4)など参照)、それとあわせて、アセンブリのトリミングの有効化にも取り組めるといいですね!

_Learn, Practice, Share!_