0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

BlazorのプレレンダリングでAPIが2回呼ばれる理由と正しい対処法

Last updated at Posted at 2025-10-19

PersistentComponentStateを使って1回の取得に最適化する

Blazorのプレレンダリングで発生する「二重データ取得」問題を解説し、PersistentComponentStateによる根本解決法を紹介します。

🧭 はじめに

Blazor Web Appにおいてプレレンダリングを有効にしたコンポーネントを実装したところ、ページロード時に同じデータを2回取得してしまい、パフォーマンスが著しく低下する問題に遭遇しました。

本記事では、

  • なぜこの現象が発生するのか
  • どのように回避すべきか
    を、実際に動作するサンプルコードをもとに詳しく解説します。

動作確認環境: .NET 8以降(Blazor Web App テンプレート)
サンプルコード: GitHub - BlazorPrerenderingSample


📝 要約(この記事の結論)

Blazor Web Appでプレレンダリングを有効にすると、
OnInitializedAsyncサーバーとクライアントの両方で実行される ため、
データ取得が2回行われます。

これを回避するには、PersistentComponentState を使って
サーバーで取得したデータをクライアントに引き継ぐのが最適解です。


⚙️ プレレンダリング設定の基本構成

Blazorでは、プレレンダリングとインタラクティブ動作を制御するために
2つの設定レベルがあります。

設定方法 適用範囲 主な用途 特徴
ページレベル設定 各ページに @rendermode を指定 学習・デモ・検証 ページ遷移ごとに再プレレンダリングが発生する
グローバル設定 App.razor で一括設定 本番運用 SPA内ページ遷移時に不要なプレレンダリングを回避

本記事では挙動をわかりやすく示すため「ページレベル設定」を使用します。
本番環境では App.razor でのグローバル設定を推奨 します。


🔍 Blazorの主要概念(復習)

プレレンダリング(Prerendering)

サーバーで静的HTMLを生成し、ユーザーに即時表示する仕組みです。

メリット

  • 初回表示が高速
  • SEOに有利

インタラクティブモード(Interactive Mode)

ユーザーの操作を処理できる状態。Blazorでは3種類あります。

  • InteractiveServer: SignalR経由でサーバーと通信
  • InteractiveWebAssembly: ブラウザ上で動作
  • InteractiveAuto: 初回はServer、以降はWASM
@rendermode InteractiveServer

グローバル設定例(App.razor)

<!-- App.razor(本番運用推奨)-->
<Routes @rendermode="InteractiveServer" />

ページレベル設定例

<!-- Pages/Home.razor -->
@page "/home"
@rendermode InteractiveServer
<h3>ホーム</h3>

💣 問題の再現

以下のようなシンプルなTodoリストを実装したとします。

<!-- Pages/Problem.razor -->
@page "/problem"
@rendermode InteractiveServer
@inject ITodoService TodoService

<h3>Todo リスト</h3>

@if (todos == null)
{
    <p>読み込み中...</p>
}
else
{
    <ul>
        @foreach (var todo in todos)
        {
            <li>@todo.Title</li>
        }
    </ul>
}

@code {
    private List<Todo>? todos;

    protected override async Task OnInitializedAsync()
    {
        todos = await TodoService.GetTodosAsync();
    }
}

出力ログ:

[14:30:15.123] GetTodosAsync called  // 1回目
[14:30:15.456] GetTodosAsync called  // 2回目!

🧩 原因:プレレンダリングの仕組み

Blazorは以下の2フェーズで動作します:

フェーズ1:サーバー側プレレンダリング

フェーズ2:クライアント側インタラクティブ化

つまり、OnInitializedAsync() が2回呼ばれる仕様です。


✅ 解決方法1:PersistentComponentState(推奨)

サーバーで取得したデータをHTMLに埋め込み、クライアントに引き継ぎます。

<!-- Pages/Solution1.razor -->
@page "/solution1"
@rendermode InteractiveServer
@inject ITodoService TodoService
@inject PersistentComponentState ApplicationState
@implements IDisposable

<h3>Todo リスト</h3>

@if (todos == null)
{
    <p>読み込み中...</p>
}
else
{
    <ul>
        @foreach (var todo in todos)
        {
            <li>@todo.Title</li>
        }
    </ul>
}

@code {
    private List<Todo>? todos;
    private PersistingComponentStateSubscription subscription;

    protected override async Task OnInitializedAsync()
    {
        subscription = ApplicationState.RegisterOnPersisting(PersistTodos);

        if (!ApplicationState.TryTakeFromJson<List<Todo>>("todos", out var restored))
        {
            Console.WriteLine("Fetching from API...");
            todos = await TodoService.GetTodosAsync();
        }
        else
        {
            Console.WriteLine("Restored from cache");
            todos = restored;
        }
    }

    private Task PersistTodos()
    {
        ApplicationState.PersistAsJson("todos", todos);
        return Task.CompletedTask;
    }

    public void Dispose() => subscription.Dispose();
}

結果

Fetching from API...
GetTodosAsync called  // 1回だけ!
Restored from cache

ポイント

  • プレレンダリングの高速性とSEOを維持
  • API呼び出しは1回のみ
  • 実運用に最も適した解法

✅ 解決方法2:プレレンダリングを無効化する

@page "/solution2"
@rendermode @(new InteractiveServerRenderMode(prerender: false))

特徴

  • 確実に1回だけAPI呼び出し
  • ただし初回表示が遅く、SEO効果がない

本番では非推奨。
一部管理画面など「SEO不要な画面」でのみ有効。


✅ 解決方法3:OnAfterRenderAsync制御

protected override async Task OnAfterRenderAsync(bool firstRender)
{
    if (firstRender)
    {
        todos = await TodoService.GetTodosAsync();
        StateHasChanged();
    }
}

特徴

  • シンプルだが初回レンダリング時は空白表示
  • 教材やデモ用途向き

🧭 ページレベル設定の落とし穴と使い分け

ページレベル設定の問題点

ケース 動作 API呼び出し
/todos に直接アクセス プレレンダリング + インタラクティブ化 2回
/home/todos にSPA遷移 再度プレレンダリング + インタラクティブ化 2回

→ ページごとにプレレンダリングが再実行され、パフォーマンスが低下します。


使い分け指針

用途 推奨設定
学習・デモ ページレベル設定
実運用 グローバル設定(App.razor
一部だけインタラクティブにしたい ページレベル指定で個別制御

推奨設定まとめ(本番)

<!-- App.razor -->
<Routes @rendermode="InteractiveServer" />

⚠ ページレベル設定は便利ですが、SPA遷移時のパフォーマンス低下に注意。
実運用では必ずグローバル設定を採用しましょう。


🧪 デバッグのコツ

protected override async Task OnInitializedAsync()
{
    Console.WriteLine($"[OnInitializedAsync] {DateTime.Now:HH:mm:ss.fff}");

    if (!ApplicationState.TryTakeFromJson<List<Todo>>("todos", out var restored))
    {
        Console.WriteLine("→ Fetching from API");
        todos = await TodoService.GetTodosAsync();
    }
    else
    {
        Console.WriteLine("→ Restored from cache");
        todos = restored;
    }
}
  • ブラウザコンソール + サーバーログ両方で呼び出し確認
  • GitHubサンプルでは全ページにログが仕込まれています

📚 まとめ

方法 API呼び出し プレレンダリング 初回表示 SEO 推奨度
PersistentComponentState 1回 ✅ 有効 ⚡ 高速 ⭐⭐⭐
プレレンダリング無効化 1回 ❌ 無効 🐢 遅い ⭐⭐
OnAfterRenderAsync 1回 ❌ 無効 🐢 遅い

結論:

Blazorのプレレンダリング問題を解決するには、
PersistentComponentState を使ってサーバーの状態を引き継ぐのが最も効率的です。


🔗 サンプルコード

👉 GitHub: satoshi3128/BlazorPrerenderingSample

# 実行方法
git clone https://github.com/satoshi3128/BlazorPrerenderingSample.git
cd BlazorPrerenderingSample
dotnet run

🙌 最後に

この記事は、実際の開発現場で遭遇した問題をもとに、
「なぜそうなるのか」を再現・分析した記録です。

技術が日々進化する中で、シニア世代のエンジニアとしても
現場で手を動かしながら学び続ける姿勢を大切にしています。

同じようにBlazorや.NETで開発している方の一助になれば幸いです。


参考リンク

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?