.NET 9 および .NET 10 での Blazor の対話性: 完全ガイド

· 6分で読める

過去数年間 Blazor を使用して Web アプリケーションを構築してきた場合は、このフレームワークが 長い進歩を遂げたことをご存知でしょう。 Blazor Server と Blazor WebAssembly のどちらかを選択することから始まったものは、アプリケーション内の各コンポーネントに適切な対話性戦略を選択できるようにする、統合された柔軟なレンダリング モデルに進化しました。

.NET 8 では、デフォルトとしてのレンダリング モードと静的サーバーサイド レンダリング (SSR) の導入という、根本的な変更が加えられました。現在、.NET 9 と .NET 10 はその基盤の上に構築されており、開発者のエクスペリエンスをよりスムーズにし、エンドユーザーのエクスペリエンスを高速化する改良が加えられています。

この投稿では、現時点での Blazor のインタラクティブ性の全体像、つまりレンダリング モードの仕組み、ストリーミング SSR がもたらすもの、強化されたナビゲーションとフォーム処理がゲームにどのような変化をもたらすか、最新リリースの新機能について説明したいと思います。新しい Blazor プロジェクトを計画している場合、またはアップグレードを検討している場合、これは、これらすべてを詳しく調べ始めたときに私が入手したかったガイドです。

簡単に振り返ってみましょう: ここに至るまでの経緯

.NET 8 より前は、プロジェクト レベルでホスティング モデルにコミットする必要がありました。 Blazor サーバーとは、すべてが SignalR 接続を介してサーバー上で実行されることを意味します。 Blazor WebAssembly とは、すべてがブラウザーで実行されることを意味します。それぞれにトレードオフがあり、それらを組み合わせるのは困難でした。

.NET 8 では、両方のモデルを統合する単一のプロジェクト テンプレート (Blazor Web App) を導入することで状況が変わりました。重要な概念は レンダリング モードです。コンポーネントごとにどのようにレンダリングするか、どこでインタラクティブ性を発揮するかを決定します。デフォルトは静的 SSR になりました。これは、コンポーネントがサーバー上でレンダリングし、プレーン HTML をブラウザーに送信することを意味します。SignalR も WebAssembly も使用せず、高速 HTML だけを使用します。

.NET 9 では、これらの概念が洗練され、開発者のエクスペリエンスが向上し、パフォーマンスが最適化されました。 .NET 10 では、再接続処理の改善、レンダリング モード全体でのコンポーネントの状態の永続化、Blazor スクリプト自体の配信方法の改善により、さらに進化しています。

すべてを分解してみましょう。

.NET 9 のレンダリング モード

最新の Blazor の中心となるのは、レンダリング モードの概念です。知っておくべきモードが 4 つあります。

1. 静的 SSR (デフォルト)

新しい Blazor Web アプリを作成すると、コンポーネントは既定でサーバー上で静的にレンダリングされます。サーバーは Razor コンポーネントを処理し、HTML を生成してブラウザーに送信します。永続的な接続や WebAssembly ランタイムはなく、従来のリクエスト/レスポンスだけです。

これは、コンテンツの多いページ、主にデータを表示するダッシュボード、またはリアルタイムの操作を必要としないページに最適です。

@page "/products"

<h1>Our Products</h1>

@foreach (var product in products)
{
    <div class="product-card">
        <h3>@product.Name</h3>
        <p>@product.Description</p>
        <span class="price">@product.Price.ToString("C")</span>
    </div>
}

@code {
    private List<Product> products = new();

    protected override async Task OnInitializedAsync()
    {
        products = await ProductService.GetAllAsync();
    }
}

このコンポーネントはサーバー上でレンダリングされ、HTML が生成され、それだけです。継続的な接続はありません。初期読み込みが速く、SEO に最適で、サーバー リソースの使用量が最小限に抑えられます。

2. インタラクティブサーバーボタンのクリックの処理、ユーザー入力の処理、UI の動的更新など、リアルタイムの対話性が必要な場合は、Interactive Server モードを選択できます。これにより、ブラウザーとサーバーの間に SignalR 接続が確立され、UI の更新はその接続を介して行われます。

@page "/counter"
@rendermode InteractiveServer

<h1>Counter</h1>

<p>Current count: @currentCount</p>

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

@code {
    private int currentCount = 0;

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

必要なのは @rendermode InteractiveServer ディレクティブだけです。コンポーネントは最初にサーバー上でレンダリングされ、その後、SignalR 接続が確立されて後続の対話が処理されます。 C# イベント ハンドラーはサーバー上で実行され、UI の差分がブラウザーに送信されます。

使用する場合: インタラクティブ性が必要な場合、コンポーネントはサーバー側のリソース (データベース、API、ファイル システム) にアクセスする必要があり、WebAssembly のダウンロードを待たずに高速な初期読み込みが必要な場合です。

3. インタラクティブな WebAssembly

サーバー接続を維持せずに対話性が必要な場合は、Interactive WebAssembly は、.NET WebAssembly ランタイムを使用してブラウザーでコンポーネント ロジックを直接実行します。

@page "/search"
@rendermode InteractiveWebAssembly

<h1>Product Search</h1>

<input @bind="searchTerm" @bind:event="oninput" placeholder="Search products..." />

@if (filteredProducts.Any())
{
    <ul>
        @foreach (var product in filteredProducts)
        {
            <li>@product.Name  @product.Price.ToString("C")</li>
        }
    </ul>
}

@code {
    private string searchTerm = string.Empty;
    private List<Product> allProducts = new();
    private IEnumerable<Product> filteredProducts => allProducts
        .Where(p => p.Name.Contains(searchTerm, StringComparison.OrdinalIgnoreCase));

    protected override async Task OnInitializedAsync()
    {
        allProducts = await Http.GetFromJsonAsync<List<Product>>("api/products") ?? new();
    }
}

トレードオフ: .NET ランタイムとアセンブリの初期ダウンロード コストがかかります。ただし、コンポーネントは一度ロードされると、UI 対話のためのサーバーのラウンドトリップなしで完全にブラウザー内で実行されます。

使用する場合: 遅延が重要となる高度にインタラクティブなコンポーネント (リッチ テキスト エディター、描画ツール、リアルタイム フィルタリングなど) の場合、サーバーの負荷を軽減したい場合、またはプログレッシブ Web アプリ (PWA) を構築している場合。

4. インタラクティブオート

これは実用的な中間点であり、私のお気に入りの機能の 1 つです。 Interactive Auto は、最初の読み込み時に SignalR を介したサーバー側のレンダリングから開始し、バックグラウンドで WebAssembly ランタイムをサイレントにダウンロードします。次回以降のアクセスでは、コンポーネントは WebAssembly 上で実行されます。

@page "/dashboard"
@rendermode InteractiveAuto

<h1>Dashboard</h1>

<DashboardWidget Title="Sales" Value="@salesTotal" />
<DashboardWidget Title="Users" Value="@activeUsers" />

<button @onclick="RefreshData">Refresh</button>

@code {
    private decimal salesTotal;
    private int activeUsers;

    protected override async Task OnInitializedAsync()
    {
        await RefreshData();
    }

    private async Task RefreshData()
    {
        var data = await DashboardService.GetSummaryAsync();
        salesTotal = data.SalesTotal;
        activeUsers = data.ActiveUsers;
    }
}

これにより、最初のレンダリングが高速になり (WASM ダウンロードを待つ必要がなく)、最終的にコンポーネントがクライアント側で実行されるという、両方の長所が得られます。ユーザーはその変化に気づきません。

いつ使用するか: 高速な初期ロードと最終的なクライアント側の実行の両方が必要な場合。これは、多くの対話型コンポーネントにとって優れたデフォルトの選択肢です。

ストリーミング SSR: 両方の長所

ストリーミング SSR は、単純そうに見えますが、知覚されるパフォーマンスに大きな違いをもたらす機能の 1 つです。アイデアは次のとおりです。HTML を送信する前にすべてのデータが読み込まれるのを待つ代わりに、サーバーはページ シェルをすぐに送信し、データが利用可能になるとコンテンツ更新を ストリーミング します。

@page "/reports"
@attribute [StreamRendering]

<h1>Monthly Reports</h1>

@if (reports is null)
{
    <div class="loading-spinner">
        <p>Loading reports...</p>
    </div>
}
else
{
    <table class="table">
        <thead>
            <tr>
                <th>Month</th>
                <th>Revenue</th>
                <th>Growth</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var report in reports)
            {
                <tr>
                    <td>@report.Month</td>
                    <td>@report.Revenue.ToString("C")</td>
                    <td class="@(report.Growth >= 0 ? "text-success" : "text-danger")">
                        @report.Growth.ToString("P1")
                    </td>
                </tr>
            }
        </tbody>
    </table>
}

@code {
    private List<MonthlyReport>? reports;

    protected override async Task OnInitializedAsync()
    {
        // This might take a couple of seconds
        reports = await ReportService.GetMonthlyReportsAsync();
    }
}

[StreamRendering] 属性を使用すると、次のことが起こります。

  1. サーバーは、ローディング スピナーを使用して HTML を直ちに送信します。
  2. OnInitializedAsync が実行され、データがフェッチされます。
  3. データが到着すると、サーバーは更新された HTML (テーブル) をブラウザにストリーミングします。
  4. ブラウザは、ページ全体をリロードせずに DOM にパッチを適用します。

ユーザーには読み込みインジケーターが 即座に表示され、その後コンテンツが入力されます。JavaScript フレームワークは必要ありません。 WebSocket接続がありません。 HTTP ストリーミングを賢く利用しただけです。使用する場合: レンダリング中にデータをフェッチする静的 SSR ページ。製品リスト ページ、ダッシュボード、レポートなど、初期データの読み込みに数百ミリ秒以上かかる可能性のある場所はどこでもです。

使用しない場合: データの読み込みが非常に速い場合 (100 ミリ秒未満)、ストリーミングのオーバーヘッドは価値がありません。また、ストリーミング SSR では継続的なインタラクティブ性が提供されません。そのためには、インタラクティブなレンダリング モードが必要です。

強化されたナビゲーションとフォーム処理

最新の Blazor における微妙だが強力な改良点の 1 つは、ナビゲーションの強化です。既定では、Blazor は内部リンクのクリックとフォームの送信をインターセプトし、完全なブラウザー ナビゲーションを実行するのではなく、fetch 経由で新しいページ コンテンツを取得し、DOM にパッチを適用します。

これは、静的 SSR ページ間の移動が SPA のように感じられることを意味します。ページ全体がフラッシュすることはなく、スクロール位置が保持され、エクスペリエンスは滑らかに滑らかになります。

仕組み

Blazor スクリプト (blazor.web.js) が読み込まれると、内部リンクのクリックが自動的にインターセプトされます。従来のブラウザ ナビゲーションの代わりに、次のことが可能です。

  1. ターゲット URL に対して fetch リクエストを実行します。
  2. HTML 応答を受信します。
  3. 新しいコンテンツを既存の DOM にマージします。
  4. ブラウザの URL と履歴を更新します。

これを有効にするために何もする必要はありません。デフォルトでオンになっています。しかし、それを制御することはできます。

<!-- Disable enhanced navigation for a specific link -->
<a href="/legacy-page" data-enhance-nav="false">Legacy Page</a>

<!-- Force a full page reload for external-like behavior -->
<a href="/downloads/report.pdf" data-enhance-nav="false">Download Report</a>

強化されたフォーム処理

フォームも同様に扱われます。 Blazor のフォーム処理で EditForm または標準の <form> 要素を使用すると、送信はインターセプトされ、fetch 経由で処理されます。

@page "/contact"

<EditForm Model="contactModel" OnValidSubmit="HandleSubmit" FormName="contact" Enhance>
    <DataAnnotationsValidator />

    <div class="mb-3">
        <label for="name">Name</label>
        <InputText id="name" @bind-Value="contactModel.Name" class="form-control" />
        <ValidationMessage For="() => contactModel.Name" />
    </div>

    <div class="mb-3">
        <label for="email">Email</label>
        <InputText id="email" @bind-Value="contactModel.Email" class="form-control" />
        <ValidationMessage For="() => contactModel.Email" />
    </div>

    <div class="mb-3">
        <label for="message">Message</label>
        <InputTextArea id="message" @bind-Value="contactModel.Message" class="form-control" />
        <ValidationMessage For="() => contactModel.Message" />
    </div>

    <button type="submit" class="btn btn-primary">Send</button>
</EditForm>

@code {
    [SupplyParameterFromForm]
    private ContactModel contactModel { get; set; } = new();

    private async Task HandleSubmit()
    {
        await ContactService.SubmitAsync(contactModel);
        contactModel = new ContactModel();
    }
}

EditFormEnhance 属性は、Blazor にフォームの送信をインターセプトするように指示します。フォームがサーバーに投稿され、サーバーがそれを処理してページを再レンダリングし、更新された HTML がストリーム バックされます。これらすべてが、完全なページ ナビゲーションを必要とせずに行われます。インタラクティブに感じられますが、完全にサーバーでレンダリングされます。

[SupplyParameterFromForm] 属性に注目してください。これは、Blazor が静的 SSR シナリオで投稿されたフォーム データをモデルにバインドする方法です。これは、従来のフォーム ポストと Blazor のコンポーネント モデルの間の橋渡しとなります。

ページごとおよびコンポーネントごとの対話性

新しいモデルの最も強力な側面の 1 つは、単一のアプリケーション内でレンダリング モードを混合できることです。製品リスト ページは静的 SSR、ショッピング カートは Interactive Server、製品コンフィギュレータは Interactive WebAssembly にすることができ、すべて同じアプリ内で行うことができます。

コンポーネント レベルでのレンダリング モードの設定

@rendermode ディレクティブを使用して、コンポーネントにレンダリング モードを直接設定できます。

@* This component is interactive via Server *@
@rendermode InteractiveServer

<h3>Live Chat</h3>
<!-- chat UI here -->

または、親のコンポーネントを使用するときに設定することもできます。

@page "/product/{Id:int}"

<h1>@product?.Name</h1>
<p>@product?.Description</p>

<!-- This child component gets its own interactive render mode -->
<ProductConfigurator Product="product" @rendermode="InteractiveWebAssembly" />

<!-- This stays static -->
<ProductReviews ProductId="Id" />

@code {
    [Parameter] public int Id { get; set; }
    private Product? product;

    protected override async Task OnInitializedAsync()
    {
        product = await ProductService.GetByIdAsync(Id);
    }
}

この例では、ページ自体が静的 SSR です。 ProductConfigurator コンポーネントは WebAssembly 上で実行され、クライアント側での豊富な対話性を実現します。 ProductReviews コンポーネントはデータを表示しているだけなので静的なままです。

レンダリング モードをグローバルに設定する

すべてのページをデフォルトでインタラクティブにしたい場合は、App.razor のルート レベルでレンダリング モードを設定できます。

<Routes @rendermode="InteractiveServer" />
```これにより、サーバー レンダリングを介してすべてのページがインタラクティブになります。必要に応じて、個々のコンポーネントがこれをオーバーライドできます。

### 覚えておくべき重要なルール

レンダリング モードを混合する場合は、留意すべき制約がいくつかあります。

- **子コンポーネントは、親よりも「よりインタラクティブな」レンダリング モードを持つことはできません。** 親が静的な場合、子は静的またはインタラクティブにすることができます。ただし、親が Interactive Server の場合、子を Interactive WebAssembly にすることはできません (Server または Auto である必要があります)
- **対話型コンポーネントは、静的な親からスコープ指定されたサービスを直接使用できません。** コンポーネントが WebAssembly で実行される場合、サーバー側の `DbContext` インスタンスに直接アクセスできません。API レイヤーが必要になります。
- **状態はレンダリング モード間で自動的に転送されません。** コンポーネントがサーバー上で事前レンダリングされてから WebAssembly に切り替わる場合は、状態の永続性を明示的に処理する必要があります。これについては、.NET 10 セクションで詳しく説明します。

## .NET 10 の新機能

.NET 10 では、信頼性と開発者のエクスペリエンスに重点を置き、段階的な改善アプローチを継続しています。 Blazor の対話性のハイライトは次のとおりです。

### 再接続エクスペリエンスの向上

運用環境で Interactive Server モードを使用したことがある場合は、おそらく再接続オーバーレイに遭遇したことがあるでしょう。SignalR 接続が切断され、ユーザーに「再接続中...」というメッセージが表示される瞬間です。 .NET 10 では、このエクスペリエンスが大幅に向上しています。

再接続ロジックの再試行がよりスマートになりました。固定の再試行間隔の代わりに、状況に適応するバックオフ戦略を使用します。再接続中の UI もさらに洗練され、エクスペリエンスをカスタマイズするためのフックが改善されました。

```razor
<!-- In your App.razor or layout -->
<div id="components-reconnect-modal">
    <div class="reconnect-visible">
        <p>Connection lost. Attempting to reconnect...</p>
        <div class="spinner"></div>
    </div>
    <div class="reconnect-failed">
        <p>Could not reconnect to the server.</p>
        <button onclick="location.reload()">Reload</button>
    </div>
    <div class="reconnect-rejected">
        <p>Your session has expired.</p>
        <a href="/">Return to Home</a>
    </div>
</div>

また、フレームワークは一時的な障害に対してより積極的に再接続を試行するようになり、回路状態をより確実に復元できるようになりました。これは、ユーザーにとって「ページをリロードしてください」という瞬間が少なくなることを意味します。

レンダリング モード間での永続的なコンポーネントの状態

これは大きなことです。 .NET 9 では、コンポーネントがサーバー上で事前レンダリングされてから WebAssembly に移行する (またはレンダリング モード間で切り替わる) と、コンポーネントの状態が失われます。コンポーネントは効果的に再初期化されるため、データの重複フェッチや UI のちらつきが発生する可能性があります。

.NET 10 では、PersistentComponentState サービスが改善され、レンダリング モードの移行間でよりシームレスに動作するようになりました。

@page "/weather"
@rendermode InteractiveWebAssembly
@inject PersistentComponentState ApplicationState

<h1>Weather Forecast</h1>

@if (forecasts is null)
{
    <p>Loading...</p>
}
else
{
    @foreach (var forecast in forecasts)
    {
        <div class="forecast-card">
            <h4>@forecast.Date.ToShortDateString()</h4>
            <p>@forecast.Summary  @forecast.TemperatureC°C</p>
        </div>
    }
}

@code {
    private List<WeatherForecast>? forecasts;
    private PersistingComponentStateSubscription persistingSubscription;

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

        if (!ApplicationState.TryTakeFromJson<List<WeatherForecast>>(
            "weather-forecasts", out var restored))
        {
            // Data wasn't persisted from prerendering — fetch it
            forecasts = await Http.GetFromJsonAsync<List<WeatherForecast>>("api/weather");
        }
        else
        {
            forecasts = restored;
        }
    }

    private Task PersistData()
    {
        ApplicationState.PersistAsJson("weather-forecasts", forecasts);
        return Task.CompletedTask;
    }

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

.NET 10 の改良により、このパターンはより確実に動作するようになりました。プリレンダリング中にシリアル化された状態は、コンポーネントがインタラクティブ モードで初期化されるときに適切に利用できるため、二重のデータ フェッチやユーザーが目にする可能性のある短いちらつきが回避されます。

静的 Web アセットとしての Blazor スクリプト

以前のバージョンでは、Blazor JavaScript ファイル (blazor.web.js) はフレームワークの内部エンドポイントから提供されていました。 .NET 10 では、静的な Web アセットとして配信されます。これは小さな変更のように聞こえるかもしれませんが、実際的な利点があります。- キャッシュの改善: 静的 Web アセットは適切なキャッシュ ヘッダーとフィンガープリントされた URL を取得するため、ブラウザーはそれらをより効果的にキャッシュします。

  • CDN フレンドリー: これは通常の静的ファイルであるため、CDN はそれをキャッシュし、エッジ ロケーションから提供できます。
  • 圧縮: 静的な Web アセット圧縮が自動的に適用され、ネットワーク上のスクリプト サイズが削減されます。

参照方法を変更する必要はありません。更新されたパスはフレームワークによって自動的に処理されます。

ベスト プラクティス: 適切なレンダリング モードの選択

いくつかのプロジェクトでこれらすべてのモードを使用した後、私の意思決定フレームワークは次のとおりです。

静的 SSR から始める

静的 SSR をデフォルトにします。ほとんどのアプリケーションのほとんどのページは、主にデータの表示に関するものです。製品ページ、ブログ投稿、ユーザー プロファイル、設定ページ - これらにはリアルタイムの対話性は必要ありません。静的 SSR は、最高のパフォーマンス、最小限のリソース使用量、そして最も単純なメンタル モデルを提供します。

必要な場合にのみインタラクティブ性を追加する

ユーザー操作にリアルタイムで応答する必要がある特定のコンポーネントを特定します。 「いいね!」ボタン、チャット ウィジェット、ドラッグ アンド ドロップ インターフェイス - これらには対話性が必要です。しかし、それらを囲むページはおそらくそうではありません。

インタラクティブ オートを頼りになるインタラクティブ モードとして使用する

インタラクティブ性が必要な場合は、多くの場合、Interactive Auto がデフォルトの最適な選択になります。これにより、高速な初期読み込み (サーバー レンダリング) と、最終的なクライアント側の実行 (WebAssembly) が実現します。ユーザーは両方の長所を活用でき、コードを一度記述するだけで済みます。

Interactive Server を特定のケースのために予約する

次の場合に Interactive Server を使用します。

  • コンポーネントはサーバー リソース (データベース、ファイル システム、内部 API) に直接アクセスする必要があります。
  • WebAssembly のダウンロード サイズが懸念されるため、自動は使用できません。
  • セキュリティ上の理由から (機密データの処理など)、コンポーネントをサーバー上で常に実行する必要があります。

ストリーミング SSR を積極的に使用する

静的 SSR ページがデータをフェッチする場合は、[StreamRendering] を追加します。コストは最小限であり、パフォーマンスの大幅な向上が感じられます。ユーザーは、空白のページを見つめるのではなく、コンテンツが徐々に表示されるのを確認します。

状態遷移を慎重に処理する

Interactive Auto を使用している場合、またはプリレンダリングと WebAssembly を組み合わせている場合は、重複したデータのフェッチを避けるために常に PersistentComponentState を使用してください。ユーザーは、ちらつくコンテンツがないことに感謝するでしょう。

コンポーネント ツリーを念頭に置く

レンダリング モードの階層ルールを覚えておいてください。インタラクティブな境界が意味をなすようにコンポーネント ツリーを計画します。一般的なパターンは、必要な場所にインタラクティブな「アイランド」が埋め込まれた静的レイアウトを使用することです。

@* Layout: Static SSR *@
<header>
    <NavMenu />
    <UserMenu @rendermode="InteractiveServer" /> @* Needs real-time auth state *@
</header>

<main>
    @Body
</main>

<footer>
    <ChatWidget @rendermode="InteractiveAuto" /> @* Rich interactivity *@
</footer>

結論

.NET 9 および .NET 10 の Blazor の対話性モデルは、Web アプリケーションを構築するための成熟した、よく考えられたアプローチを表しています。コンポーネントごとにレンダリング モードを選択できる機能、シームレスな強化されたナビゲーション、ストリーミング SSR、再接続と状態管理の継続的な改善により、幅広いアプリケーションにとって魅力的な選択肢となっています。重要な洞察は、インタラクティブ性はスペクトルであり、二者択一の選択ではないということです。アプリケーションのほとんどは静的にすることができます。一部の部分ではサーバー主導の対話性が必要です。ブラウザで実行するとメリットが得られるものもあります。 Blazor では、フレームワークと競合することなく、最も細かい粒度 (個々のコンポーネント) でその選択を行うことができるようになりました。

新しいプロジェクトを開始する場合、私のアドバイスはシンプルです。Blazor Web アプリを作成し、静的 SSR から開始し、データ量の多いページに [StreamRendering] を追加し、必要な場合にのみ個々のコンポーネントを対話モードに昇格させます。最終的には、高速かつ効率的で、拡張性に優れたアプリケーションが完成します。

コーディングを楽しんでください。いつものように、質問がある場合はお気軽にお問い合わせください。