Blazor WebAssembly で IndexedDB へオブジェクトを保存する JS 呼び出しが、Visual Studio の Debug では正常に動くのに、Publish/Release した本番版では一切動かない──そんな症状に悩まされることがあります。本記事では、実際に遭遇した事例をもとに、原因となった wasm-tools ワークロード不整合と、その解消手順、さらに再発防止のチェックリストまで詳しく解説します。
Blazor WebAssembly + IndexedDB + JS 呼び出しで起きた症状
まずは、実際に発生した症状を整理しておきます。
- Blazor WebAssembly アプリから
IJSRuntime.InvokeAsyncで JavaScript 関数My_Putを呼び出し、IndexedDB にオブジェクトを保存している。 - Visual Studio の Debug ビルド(F5 起動・IIS Express など)では、問題なく JS が実行され、IndexedDB への保存も成功する。
- ところが、Publish(Release)で発行したアプリをブラウザで開くと、C# 側の呼び出しは通っているように見えるのに、JS 側の処理が全く動かない。
- JS 関数内に仕込んだ
console.logも一切出てこないため、「そもそも JS が呼ばれていない?」ように見える。
このような状況に陥ると、多くの方が次のような疑いを持ちます。
- IL トリミング(
PublishTrimmed)で必要な型が削られた? - AOT(
RunAOTCompilation)を有効にしたせい? - JS 側のファイルが発行されていない?
もちろん、これらも重要なチェックポイントですが、今回のケースでは、真の原因は別のところ(wasm-tools ワークロード)にありました。
結論:wasm-tools ワークロードの不整合が原因だった
最終的に効いた対処は非常にシンプルで、次のコマンドで wasm-tools ワークロードを再インストールすることでした。
dotnet workload install wasm-tools
この操作を行った後にプロジェクトを再発行したところ:
- Debug と同じく、Publish/Release 版でも
console.logが出力される - IndexedDB への保存処理も正常に動作
<PublishTrimmed>true</PublishTrimmed>のままでも問題なし- AOT 有効(
RunAOTCompilationあり)・無効どちらでも同じく正常動作
つまり、IL トリミングや AOT 自体は直接の犯人ではなく、それらを支える wasm-tools ワークロードの破損・不整合が根本原因でした。
| 状態 | 挙動 | 備考 |
|---|---|---|
| Debug | JS 呼び出し OK / IndexedDB 保存 OK | 開発中は問題に気づきにくい |
| Publish(wasm-tools 不整合) | JS が実行されない / log も出ない | IL トリミングや AOT を疑い始める |
| Publish(wasm-tools 再インストール後) | Debug と同じ挙動 | トリミング有効でも問題なし |
なお、wasm-tools をインストールすると、Visual Studio の NuGet パッケージ マネージャーに複数の関連パッケージが表示される場合がありますが、これは SDK のワークロード構成物です。アンインストールしたくなっても、NuGet から手動で削除するのは NG です。削除するときは必ず次のコマンドを使います。
dotnet workload uninstall wasm-tools
再現パターンの例:IJSRuntime.InvokeAsync と IndexedDB 保存
ここで、典型的なコード例を整理しておきます。
C# 側:IJSRuntime から JS 関数 My_Put を呼び出す
public class MyClass
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
@inject IJSRuntime JS
@code {
private async Task SaveAsync()
{
var myClass = new MyClass
{
Id = 1,
Name = "Sample",
CreatedAt = DateTime.Now
};
// IndexedDB へ保存する JS 関数を呼び出す
await JS.InvokeAsync<object>("My_Put", myClass);
}
}
JavaScript 側:IndexedDB へオブジェクトを put
function My_Put(myClass) {
console.log("(TS) My_Put Start");
const request = myDatabase
.transaction(myStoreName, "readwrite")
.objectStore(myStoreName)
.put(myClass);
request.onsuccess = () => {
console.log("(TS) My_Put: SUCCESS");
};
request.onerror = event => {
console.error("My_Put: ERROR", event);
};
}
Debug ビルドではこのコードが問題なく動作していても、Publish/Release 版では (TS) My_Put Start のログさえ出ず、「そもそも My_Put が呼ばれていない?」ように見えてしまう、というのが今回のパターンです。
有効だった解決手順(実際に行ったこと)
今回のトラブルを解消するまでに実施した手順と、最終的に有効だったものを整理します。
- wasm-tools ワークロードを(再)インストール これが決め手になりました。
dotnet workload install wasm-toolsインストール後に、dotnet clean→ 再ビルド → 再 Publish を行い、ブラウザキャッシュもクリアしてから動作確認します。 - 余計な DynamicDependency を削除 トリミングを回避しようとして、広すぎる範囲に
[DynamicDependency]を付けていた場合は見直します。不要な指定は削除し、必要な型・メンバーのみに絞り込むことで、ビルド・実行時の予期せぬ副作用を減らせます。 - トリミング設定は
<PublishTrimmed>true</PublishTrimmed>のままで OK 最終的には、トリミングを有効にしたままでも Debug と Publish で同じ挙動になりました。AOT 有効/無効どちらでも問題なく、wasm-tools の健全性が確保されれば IL トリミングや AOT が直接の問題にはならないことが確認できました。
wasm-tools ワークロードとは? IL トリミング・AOT との関係
wasm-tools は、.NET の WebAssembly サポートに必要なツール一式を含むワークロードです。Blazor WebAssembly や AOT コンパイルを利用する際に内部で利用されます。
| 要素 | 役割 | wasm-tools との関係 |
|---|---|---|
IL トリミングPublishTrimmed | 未使用の型・メンバーを削除して、アセンブリや WASM のサイズを小さくする | トリミング結果をもとに WASM を生成する際にツールが利用される |
AOT コンパイルRunAOTCompilation | CIL を事前にネイティブコード相当へ変換することで、実行速度を向上させる | AOT 有効時は wasm-tools が必須 |
| wasm-tools ワークロード | WASM ビルド・最適化・デバッグなどの工具セット | 破損やバージョン不整合があると、Debug/Release 間で挙動差が出ることがある |
今回の事例のように、wasm-tools の不整合は、IL トリミングや AOT の挙動を間接的におかしくすることがあります。その結果として、「Debug では動くのに Publish では JS が動かない」という現象につながることがあります。
同様の症状が出たときのチェックリスト
次に、同じようなトラブルに遭遇したときの切り分け手順と代替策をチェックリスト形式でまとめます。
| 観点 | 確認ポイント | 優先度 |
|---|---|---|
| トリミング | PublishTrimmed を一時的に false にして挙動を比較 | 高 |
| ワークロード | dotnet workload list / install / restore の確認 | 高 |
| AOT | AOT ON/OFF での挙動差を確認 | 中 |
| 静的ファイル | JS ファイルの配信パス・キャッシュ・script タグを確認 | 高 |
| JS 相互運用 | 返り値・Promise を正しく返しているか | 高 |
| データ型 | DateTime・複合型のシリアライズ/デシリアライズ | 中 |
トリミングの影響確認
まずは、IL トリミングが悪さをしていないかを確認します。最も簡単な方法は、一時的にトリミングを無効化することです。
<PropertyGroup>
<PublishTrimmed>false</PublishTrimmed>
</PropertyGroup>
この状態で Publish し、JS 呼び出しが動くようになる場合は、トリミング起因の可能性が高くなります。その場合は次のような対策を検討します。
- 本当に必要な型・メンバーに対してだけ
[DynamicDependency]を付与する - トリマーの設定(
TrimmerRootAssemblyなど)でルート指定を調整する - リフレクションを多用しているコードの構造を見直す
注意点として、[Preserve] 属性は Xamarin/Mono 向けのものであり、Blazor では有効ではありません。Blazor WebAssembly でトリミング制御をしたい場合は、System.Diagnostics.CodeAnalysis 名前空間に含まれる属性(DynamicDependency など)を利用します。
using System.Diagnostics.CodeAnalysis;
public class MyService
{
// 例:特定の型のメンバーをトリミングから守る
[DynamicDependency(DynamicallyAccessedMemberTypes.PublicProperties, typeof(MyClass))]
public void Register()
{
// ...
}
}
ワークロード健全性の確認
次に、今回の事例で最重要だった wasm-tools ワークロードの状態を確認します。
# インストール済みワークロード一覧
dotnet workload list
# 必要に応じて修復
dotnet workload restore
# 明示的に再インストール
dotnet workload install wasm-tools
ワークロードの更新/再インストール後は、次のような一連の操作を行ってクリーンな状態を作ると安全です。
dotnet clean- Visual Studio からソリューションを再ビルド
- Publish(フォルダー出力でも可)
- ブラウザキャッシュと
bin/objの古い成果物をクリアして再確認
AOT 利用時の要件
AOT(RunAOTCompilation)を有効にする場合は、wasm-tools が必須です。とはいえ、非 AOT でもワークロードが壊れていると、ビルドや実行時に不具合が出ることがあります。
<PropertyGroup>
<RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
AOT を ON/OFF しても症状が変わらない場合は、「AOT そのもの」よりも「AOT を支えるツールチェーン(つまり wasm-tools)」に問題がある可能性を疑うと切り分けがスムーズです。
静的ファイル/JS の読み込み確認
Publish 後だけ JS が動かない場合、単純に JS ファイルが配信されていない・間違ったパスを参照していることもよくあります。以下を確認しましょう。
- 発行先のフォルダーに、対象の JS ファイルが存在するか
index.html(スタンドアロン)または_Host.cshtml(ASP.NET Core ホスト)での<script>タグのパスが正しいか- CDN を使っている場合は、CORS やキャッシュ設定に問題がないか
- バンドル/圧縮/ファイル名ハッシュ化(例:
my.js→my.abcd1234.js)によって参照が外れていないか
例えば、Blazor WebAssembly スタンドアロンでは、wwwroot/index.html に次のように記述します。
<script src="js/indexeddb.js"></script>
発行先でこのファイルが存在せず 404 になっていると、当然 My_Put は定義されません。
JS 相互運用の返り値仕様
IJSRuntime.InvokeAsync<T> を使う場合、JS 側の関数は値または Promise を返す必要があります。返り値を待たないつもりで InvokeAsync<object> を使っていても、JS 側が一切 return しないと、環境によって微妙な差が出ることがあります。
より堅牢にするには、次のように Promise を返す実装にしておくのがおすすめです。
function My_Put(myClass) {
return new Promise((resolve, reject) => {
console.log("(TS) My_Put Start");
try {
const request = myDatabase
.transaction(myStoreName, "readwrite")
.objectStore(myStoreName)
.put(myClass);
request.onsuccess = () => {
console.log("(TS) My_Put: SUCCESS");
resolve("(TS) SUCCESS");
};
request.onerror = event => {
console.error("My_Put: ERROR", event);
reject("(TS) ERROR");
};
} catch (e) {
console.error("My_Put: EXCEPTION", e);
reject("(TS) EXCEPTION");
}
});
}
C# 側では次のように受け取ります。
var result = await JS.InvokeAsync<string>("My_Put", myClass);
Console.WriteLine($"My_Put result: {result}");
こうしておくことで、
- JS 側で失敗した場合に .NET 側で例外として受け取れる
- Debug/Release の挙動差が出にくくなる
- どこで失敗したかをログから追いやすくなる
といったメリットがあります。
データ型の扱い(特に DateTime)
Blazor の JS 相互運用では、DateTime 型は既定では ISO 8601 形式の文字列としてシリアライズされます。つまり、JS 側では単なる文字列として受け取ることになります。
function My_Put(myClass) {
// C# の DateTime は文字列として届く
console.log(myClass.createdAt); // 例: "2025-01-01T12:34:56.789Z"
const created = new Date(myClass.createdAt);
console.log("Parsed Date:", created);
// ...
}
もし JS 側で myClass.createdAt を Date オブジェクトのように扱っていると、Debug/Release の挙動差以前にロジック上の不具合が発生することがあります。文字列 → Date への変換を明示的に書いておくのが安全です。
IndexedDB 操作用 JS をより堅牢にする実装例
IndexedDB は非同期 API であり、失敗時の原因も多岐にわたるため、ログとエラーハンドリングを手厚くしておくとトラブルシュートが非常に楽になります。先ほどの Promise 版 My_Put をベースに、もう少し丁寧に書いた例です。
function My_Put(myClass) {
return new Promise((resolve, reject) => {
console.log("[My_Put] Start", myClass);
if (!myDatabase) {
console.error("[My_Put] myDatabase is null");
reject("Database is not initialized.");
return;
}
const tx = myDatabase.transaction(myStoreName, "readwrite");
const store = tx.objectStore(myStoreName);
const request = store.put(myClass);
request.onsuccess = () => {
console.log("[My_Put] SUCCESS");
resolve("SUCCESS");
};
request.onerror = event => {
console.error("[My_Put] ERROR", event);
reject("ERROR");
};
tx.oncomplete = () => {
console.log("[My_Put] Transaction completed");
};
tx.onerror = event => {
console.error("[My_Put] Transaction ERROR", event);
};
});
}
ここまでログを仕込んでおけば、「Release 版では JS が全く動いていないのか、それとも途中でエラーになっているのか」を細かく切り分けることができます。
開発フローを見直して Debug/Release 差分を減らす
今回のような「Debug では動くのに本番では動かない」問題を減らすには、日常的に Publish 版での挙動を確認する習慣が有効です。
| タイミング | 実施すること | 目的 |
|---|---|---|
| 機能追加後 | ローカルフォルダーに Publish(Release)してテスト | Debug/Release の挙動差を早期に検出 |
| パッケージ更新後 | トリミング・AOT 有効で再 Publish | ライブラリ更新に起因するトリミング問題を検出 |
| SDK 更新後 | dotnet workload restore / dotnet workload install wasm-tools の確認 | ワークロード不整合を防ぐ |
| 本番リリース前 | 本番と同条件の環境で最終確認 | 構成差異・環境差異による問題を排除 |
特に、SDK や Visual Studio をアップデートしたタイミングでは、必ず一度 Clean + Publish を行い、必要であれば wasm-tools の再インストールも検討することをおすすめします。
よくある勘違い・NG パターン
Blazor WebAssembly と JS 相互運用まわりで、実際に目にしがちな勘違い・NG パターンをいくつか挙げておきます。
[Preserve]属性を Blazor で使おうとする
Xamarin/Mono 向けの属性であり、Blazor WebAssembly のトリミング制御には使えません。DynamicDependencyなど適切な属性を使いましょう。- wasm-tools 関連のパッケージを NuGet から削除してしまう
ワークロード構成物を NuGet 管理と勘違いしてアンインストールすると、工具セットそのものが壊れます。削除したい場合はdotnet workload uninstall wasm-toolsを使います。 - JS 側でエラーハンドリング・ログを一切入れていない
何もログが出ないと、「そもそも JS が呼ばれていないのか」「呼ばれたが途中で失敗しているのか」が分かりません。最低限console.logとconsole.errorは入れておきましょう。 - Debug のみでテストして安心してしまう
トリミングや AOT、最適化は主に Release で効いてきます。Publish 版での確認をサボると、本番リリース直前になって大きくハマる原因になります。 - IndexedDB の初期化エラーを見逃す
DB オープン時のエラー(バージョン不整合など)が起きていても、その場でログを出していないと、後続のMy_Putが失敗しても原因が分からなくなります。
まとめ:Blazor WASM 本番トラブルを防ぐポイント
最後に、本記事のポイントを整理します。
- Blazor WebAssembly で「Debug では動くのに Publish/Release では JS が動かない」場合、IL トリミングや AOT だけでなく、wasm-tools ワークロードの不整合も強く疑うべきです。
- 今回の事例では、
dotnet workload install wasm-toolsによる再インストールで、トリミング有効・AOT 有無に関わらず、Debug と同じ挙動に戻りました。 - トリミングが怪しいときは、まず
PublishTrimmedを一時的にfalseにして比較し、必要な場合だけDynamicDependencyなどで調整します。[Preserve]は Blazor では使えません。 - JS 相互運用は、Promise を返す実装にしておくと、.NET 側でのエラー捕捉が容易になり、Debug/Release の挙動差も出にくくなります。
- IndexedDB への保存処理では、
DateTimeのシリアライズ形式(ISO 8601 文字列)や、DB 初期化時のエラー処理にも注意しましょう。 - SDK 更新後やパッケージ更新後には、Clean + Publish でのテストと wasm-tools ワークロードの健全性確認をルーチン化すると、本番直前のトラブルを大幅に減らせます。
Blazor WebAssembly の本番トラブルは、原因が JS 側・.NET 側・ビルド設定・ツールチェーンなど多岐にわたるため、やみくもに対処すると時間ばかりかかってしまいます。本記事のチェックリストと手順をベースに、落ち着いて一つずつ切り分けていけば、同様の問題に遭遇しても短時間で原因にたどり着けるはずです。

コメント