Blazor WebAssembly本番環境でJS呼び出しが動かない原因と解決策(IndexedDB・wasm-tools・ILトリミング)

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 ワークロードの破損・不整合が根本原因でした。

状態挙動備考
DebugJS 呼び出し 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&lt;object&gt;("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 = () =&gt; {
    console.log("(TS) My_Put: SUCCESS");
  };

  request.onerror = event =&gt; {
    console.error("My_Put: ERROR", event);
  };
}

Debug ビルドではこのコードが問題なく動作していても、Publish/Release 版では (TS) My_Put Start のログさえ出ず、「そもそも My_Put が呼ばれていない?」ように見えてしまう、というのが今回のパターンです。

有効だった解決手順(実際に行ったこと)

今回のトラブルを解消するまでに実施した手順と、最終的に有効だったものを整理します。

  1. wasm-tools ワークロードを(再)インストール これが決め手になりました。 dotnet workload install wasm-tools インストール後に、dotnet clean → 再ビルド → 再 Publish を行い、ブラウザキャッシュもクリアしてから動作確認します。
  2. 余計な DynamicDependency を削除 トリミングを回避しようとして、広すぎる範囲に [DynamicDependency] を付けていた場合は見直します。不要な指定は削除し、必要な型・メンバーのみに絞り込むことで、ビルド・実行時の予期せぬ副作用を減らせます。
  3. トリミング設定は <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 の確認高
AOTAOT ON/OFF での挙動差を確認中
静的ファイルJS ファイルの配信パス・キャッシュ・script タグを確認高
JS 相互運用返り値・Promise を正しく返しているか高
データ型DateTime・複合型のシリアライズ/デシリアライズ中

トリミングの影響確認

まずは、IL トリミングが悪さをしていないかを確認します。最も簡単な方法は、一時的にトリミングを無効化することです。

&lt;PropertyGroup&gt;
  &lt;PublishTrimmed&gt;false&lt;/PublishTrimmed&gt;
&lt;/PropertyGroup&gt;

この状態で 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

ワークロードの更新/再インストール後は、次のような一連の操作を行ってクリーンな状態を作ると安全です。

  1. dotnet clean
  2. Visual Studio からソリューションを再ビルド
  3. Publish(フォルダー出力でも可)
  4. ブラウザキャッシュと bin/obj の古い成果物をクリアして再確認

AOT 利用時の要件

AOT(RunAOTCompilation)を有効にする場合は、wasm-tools が必須です。とはいえ、非 AOT でもワークロードが壊れていると、ビルドや実行時に不具合が出ることがあります。

&lt;PropertyGroup&gt;
  &lt;RunAOTCompilation&gt;true&lt;/RunAOTCompilation&gt;
&lt;/PropertyGroup&gt;

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 に次のように記述します。

&lt;script src="js/indexeddb.js"&gt;&lt;/script&gt;

発行先でこのファイルが存在せず 404 になっていると、当然 My_Put は定義されません。

JS 相互運用の返り値仕様

IJSRuntime.InvokeAsync<T> を使う場合、JS 側の関数は値または Promise を返す必要があります。返り値を待たないつもりで InvokeAsync<object> を使っていても、JS 側が一切 return しないと、環境によって微妙な差が出ることがあります。

より堅牢にするには、次のように Promise を返す実装にしておくのがおすすめです。

function My_Put(myClass) {
  return new Promise((resolve, reject) =&gt; {
    console.log("(TS) My_Put Start");

    try {
      const request = myDatabase
        .transaction(myStoreName, "readwrite")
        .objectStore(myStoreName)
        .put(myClass);

      request.onsuccess = () =&gt; {
        console.log("(TS) My_Put: SUCCESS");
        resolve("(TS) SUCCESS");
      };

      request.onerror = event =&gt; {
        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&lt;string&gt;("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) =&gt; {
    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 = () =&gt; {
      console.log("[My_Put] SUCCESS");
      resolve("SUCCESS");
    };

    request.onerror = event =&gt; {
      console.error("[My_Put] ERROR", event);
      reject("ERROR");
    };

    tx.oncomplete = () =&gt; {
      console.log("[My_Put] Transaction completed");
    };

    tx.onerror = event =&gt; {
      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 側・ビルド設定・ツールチェーンなど多岐にわたるため、やみくもに対処すると時間ばかりかかってしまいます。本記事のチェックリストと手順をベースに、落ち着いて一つずつ切り分けていけば、同様の問題に遭遇しても短時間で原因にたどり着けるはずです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次