ASP.NET Core Razor コンポーネントのプリレンダリング

この記事では、Blazor Web アプリでのサーバーでレンダリングされるコンポーネントの Razor コンポーネント プリレンダリング シナリオについて説明します。

プリレンダリングは、レンダリングされたコントロールのイベント ハンドラーを有効にせずに、最初にサーバーにページ コンテンツをレンダリングするプロセスです。 サーバーは、最初の要求に応じてできるだけ早くページの HTML UI を出力します。これにより、アプリはユーザーに対してより応答性が高くなります。 プリレンダリングでは、検索エンジンによってページ ランクの計算に使用される初期 HTTP 応答のコンテンツをレンダリングすることで、検索エンジンの最適化 (SEO) を向上させることもできます。

プリレンダリングされた状態を保持する

プリレンダリングされた状態を保持しないと、プリレンダリング中に使用された状態は失われ、アプリが完全に読み込まれたときに再作成する必要があります。 いずれかの状態が非同期的に作成された場合、コンポーネントの再レンダリング時にプリレンダリングされた UI が置き換えられると、UI がちらつくことがあります。

次の PrerenderedCounter1 カウンター コンポーネントについて検討します。 コンポーネントは、OnInitializedライフサイクル メソッドでのプリレンダリング中に初期ランダム カウンター値を設定します。 クライアントへの SignalR 接続が確立されると、コンポーネントが再レンダリングされ、OnInitialized の 2 回目の実行時に初期カウント値が置き換えられます。

PrerenderedCounter1.razor:

@page "/prerendered-counter-1"
@rendermode @(new InteractiveServerRenderMode(prerender: true))
@inject ILogger<PrerenderedCounter1> Logger

<PageTitle>Prerendered Counter 1</PageTitle>

<h1>Prerendered Counter 1</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;

    protected override void OnInitialized()
    {
        currentCount = Random.Shared.Next(100);
        Logger.LogInformation("currentCount set to {Count}", currentCount);
    }

    private void IncrementCount()
    {
        currentCount++;
    }
}

アプリを実行し、コンポーネントのログを調査します。 以下に、出力の例を示します。

Note

アプリで対話型 (拡張) ルーティングが採用されていて、内部ナビゲーションを介してページに到達した場合、プリレンダリングは行われません。 したがって、次の出力を表示するには、PrerenderedCounter1 コンポーネントのページ全体の再読み込みを行う必要があります。

info: BlazorSample.Components.Pages.PrerenderedCounter1[0]
currentCount set to 41
info: BlazorSample.Components.Pages.PrerenderedCounter1[0]
currentCount set to 92

最初のログ カウントはプリレンダリング中に生じます。 コンポーネントが再レンダリングされると、プリレンダリング後にカウントが再び設定されます。 カウントが 41 から 92 に更新されるときにも、UI にちらつきが生じます。

プリレンダリング中にカウンターの初期値を保持するために、Blazor では、PersistentComponentState サービス (さらに、Razor Pages または MVC アプリのページまたはビューに埋め込まれたコンポーネントの場合は、コンポーネントの状態保持タグ ヘルパー) を使用してプリレンダリングされたページ内の状態の永続化をサポートしています。

プリレンダリングされた状態を保持するには、PersistentComponentState サービスを使用して、どの状態を永続化するかを決定します。 アプリが一時停止される前に、PersistentComponentState.RegisterOnPersisting によってコールバックが登録され、コンポーネントの状態が保持されます。 状態は、アプリの再開時に取得されます。

次の例は、一般的なパターンを示しています。

  • {TYPE} プレースホルダーは、永続化するデータの種類を表します。
  • {TOKEN} プレースホルダーは、状態識別子の文字列です。 nameof({VARIABLE}) の使用を検討してください。ここで、{VARIABLE} プレースホルダーは状態を保持する変数の名前です。 状態識別子に nameof() を使うと、引用符で囲まれた文字列の使用が回避されます。
@implements IDisposable
@inject PersistentComponentState ApplicationState

...

@code {
    private {TYPE} data;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override async Task OnInitializedAsync()
    {
        persistingSubscription = 
            ApplicationState.RegisterOnPersisting(PersistData);

        if (!ApplicationState.TryTakeFromJson<{TYPE}>(
            "{TOKEN}", out var restored))
        {
            data = await ...;
        }
        else
        {
            data = restored!;
        }
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson("{TOKEN}", data);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose()
    {
        persistingSubscription.Dispose();
    }
}

次のカウンター コンポーネントでは、プリレンダリング中にカウンターの状態を保持し、コンポーネントを初期化するためにその状態を取得します。

PrerenderedCounter2.razor:

@page "/prerendered-counter-2"
@implements IDisposable
@inject ILogger<PrerenderedCounter2> Logger
@inject PersistentComponentState ApplicationState

<PageTitle>Prerendered Counter 2</PageTitle>

<h1>Prerendered Counter 2</h1>

<p role="status">Current count: @currentCount</p>

<button class="btn btn-primary" @onclick="IncrementCount">Click me</button>

@code {
    private int currentCount;
    private PersistingComponentStateSubscription persistingSubscription;

    protected override void OnInitialized()
    {
        persistingSubscription =
            ApplicationState.RegisterOnPersisting(PersistCount);

        if (!ApplicationState.TryTakeFromJson<int>(
            nameof(currentCount), out var restoredCount))
        {
            currentCount = Random.Shared.Next(100);
            Logger.LogInformation("currentCount set to {Count}", currentCount);
        }
        else
        {
            currentCount = restoredCount!;
            Logger.LogInformation("currentCount restored to {Count}", currentCount);
        }
    }

    private Task PersistCount()
    {
        ApplicationState.PersistAsJson(nameof(currentCount), currentCount);

        return Task.CompletedTask;
    }

    void IDisposable.Dispose() => persistingSubscription.Dispose();

    private void IncrementCount()
    {
        currentCount++;
    }
}

コンポーネントが実行されると、プリレンダリング中に currentCount が 1 回だけ設定されます。 この値は、コンポーネントが再レンダリングされると復元されます。 以下に、出力の例を示します。

Note

アプリで対話型ルーティングが採用されていて、ページに内部ナビゲーション経由で到達した場合、プリレンダリングは行われません。 したがって、次の出力を表示するには、PrerenderedCounter2 コンポーネントのページ全体の再読み込みを行う必要があります。

info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
currentCount set to 96
info: BlazorSample.Components.Pages.PrerenderedCounter2[0]
currentCount restored to 96

プリレンダリング中に使用されたのと同じ状態でコンポーネントを初期化することにより、負荷の高い初期化ステップが 1 回だけ実行されます。 レンダリングされた UI もプリレンダリングされた UI に一致するので、ブラウザーでちらつきは発生しません。

ページとビューに埋め込まれたコンポーネント (Razor Pages/MVC)

Razor Pages または MVC アプリのページまたはビューに埋め込まれたコンポーネントの場合は、アプリのレイアウトの終了 </body> タグ内に <persist-component-state /> HTML タグを含むコンポーネントの状態保持タグ ヘルパーを追加する必要があります。 これは、Razor Pages アプリと MVC アプリでのみ必要です。 詳細については、「ASP.NET Core でのコンポーネントの状態保持タグ ヘルパー」を参照してください。

Pages/Shared/_Layout.cshtml:

<body>
    ...

    <persist-component-state />
</body>

対話型ルーティングとプリレンダリング

対話型ルーティングの内部ナビゲーションでは、サーバーに新しいページ コンテンツを要求する必要はありません。 そのため、内部ページ要求ではプリレンダリングは行われません。

PersistentComponentState サービスが機能するのは、初期ページの読み込みであり、拡張ページ ナビゲーション イベント全体ではありません。 永続的なコンポーネントの状態を利用するページへの完全な (拡張されていない) ナビゲーションをアプリが実行すると、永続化された状態が対話型になったときにアプリで使用できるようになります。 ただし、対話型回線が既に確立されていて、永続化されたコンポーネントの状態をレンダリングするページに拡張ナビゲーションが実行されている場合、その状態は既存の回線では使用できません。 PersistentComponentState サービスは拡張ナビゲーションを認識しておらず、既に実行中のコンポーネントに状態の更新を通知するメカニズムはありません。

プリレンダリング ガイダンス

プリレンダリング ガイダンスは、Blazor ドキュメント内で主題別に整理されています。 次のリンクは、主題別に設定された、ドキュメント全体のすべてのプリレンダリング ガイダンスを含んでいます。