ASP.NET Core Blazor の高度なシナリオ (レンダー ツリーの構築)

注意

これは、この記事の最新バージョンではありません。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

警告

このバージョンの ASP.NET Core はサポート対象から除外されました。 詳細については、「.NET および .NET Core サポート ポリシー」を参照してください。 現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

重要

この情報はリリース前の製品に関する事項であり、正式版がリリースされるまでに大幅に変更される可能性があります。 Microsoft はここに示されている情報について、明示か黙示かを問わず、一切保証しません。

現在のリリースについては、この記事の .NET 8 バージョンを参照してください。

この記事では、Blazor レンダー ツリーを RenderTreeBuilder を利用して手動で構築するための高度なシナリオについて説明します。

警告

コンポーネントの作成に RenderTreeBuilder を使用することは、"高度なシナリオ" です。 形式が正しくないコンポーネント (閉じられていないマークアップ タグなど) により、定義されていない動作が発生する可能性があります。 未定義の動作には、コンテンツのレンダリングの中断、アプリの機能の損失、セキュリティ侵害などが含まれます。

レンダー ツリー (RenderTreeBuilder) を手動で構築する

RenderTreeBuilder には、コンポーネントと要素を操作するためのメソッドが用意されています。これには、C# コードでコンポーネントを手動で作成することも含まれます。

次の PetDetails コンポーネントを考えてみましょう。これは手動で別のコンポーネントにレンダリングすることができます。

PetDetails.razor:

<h2>Pet Details</h2>

<p>@PetDetailsQuote</p>

@code
{
    [Parameter]
    public string? PetDetailsQuote { get; set; }
}

次の BuiltContent コンポーネントでは、CreateComponent メソッド内のループによって、3 つの PetDetails コンポーネントが生成されます。

シーケンス番号のある RenderTreeBuilder メソッドでは、シーケンス番号はソース コードの行番号です。 Blazor の差分アルゴリズムは、個別の呼び出しではなく、個別のコード行に対応するシーケンス番号に依存しています。 RenderTreeBuilder メソッドを使用してコンポーネントを作成する場合は、シーケンス番号の引数をハードコードします。 計算またはカウンターを使用してシーケンス番号を生成すると、パフォーマンスが低下する可能性があります。 詳細については、「シーケンス番号は実行順序ではなくコード行番号に関係する」セクションを参照してください。

BuiltContent.razor:

@page "/built-content"

<PageTitle>Built Content</PageTitle>

<h1>Built Content Example</h1>

<div>
    @CustomRender
</div>

<button @onclick="RenderComponent">
    Create three Pet Details components
</button>

@code {
    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (var i = 0; i < 3; i++) 
        {
            builder.OpenComponent(0, typeof(PetDetails));
            builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
            builder.CloseComponent();
        }
    };

    private void RenderComponent() => CustomRender = CreateComponent();
}
@page "/built-content"

<PageTitle>Built Content</PageTitle>

<h1>Built Content Example</h1>

<div>
    @CustomRender
</div>

<button @onclick="RenderComponent">
    Create three Pet Details components
</button>

@code {
    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (var i = 0; i < 3; i++) 
        {
            builder.OpenComponent(0, typeof(PetDetails));
            builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
            builder.CloseComponent();
        }
    };

    private void RenderComponent() => CustomRender = CreateComponent();
}
@page "/built-content"

<h1>Build a component</h1>

<div>
    @CustomRender
</div>

<button @onclick="RenderComponent">
    Create three Pet Details components
</button>

@code {
    private RenderFragment? CustomRender { get; set; }

    private RenderFragment CreateComponent() => builder =>
    {
        for (var i = 0; i < 3; i++) 
        {
            builder.OpenComponent(0, typeof(PetDetails));
            builder.AddAttribute(1, "PetDetailsQuote", "Someone's best friend!");
            builder.CloseComponent();
        }
    };

    private void RenderComponent()
    {
        CustomRender = CreateComponent();
    }
}

警告

Microsoft.AspNetCore.Components.RenderTree の型により、レンダリング操作の "結果" の処理が許可されます。 これらは、Blazor フレームワーク実装の内部的な詳細です。 これらの型は "不安定" と考えるべきで、今後のリリースで変更される可能性があります。

シーケンス番号は実行順序ではなくコード行番号に関係する

Razor コンポーネント ファイル (.razor) は常にコンパイルされます。 コンパイル済みコードを実行する方が、コードを解釈するよりも潜在的な利点があります。コンパイル済みコードを生成するコンパイル ステップを使用して、実行時のアプリのパフォーマンスを向上させる情報を挿入できるからです。

これらの機能強化の主な例として、"シーケンス番号" があります。 シーケンス番号は、出力がコードの個別の順序付けられたどの行からのものかをランタイムに示します。 ランタイムでは、この情報を使用して、効率的なツリーの差分を線形時間で生成します。これは、一般的なツリーの差分アルゴリズムで通常にできるよりもかなり高速です。

次の Razor コンポーネント ファイル (.razor) について考えてみましょう。

@if (someFlag)
{
    <text>First</text>
}

Second

上記の Razor マークアップとテキスト コンテンツは、次のような C# コードにコンパイルされます。

if (someFlag)
{
    builder.AddContent(0, "First");
}

builder.AddContent(1, "Second");

コードの初めての実行時に、someFlagtrue である場合、ビルダーは次の表で示すシーケンスを受け取ります。

シーケンス 種類 データ
0 テキスト ノード First
1 テキスト ノード Second

someFlagfalse になり、マークアップが再びレンダリングされるとします。 この時点で、ビルダーは次の表に示すシーケンスを受け取ります。

シーケンス 種類 データ
1 テキスト ノード Second

ランタイムで差分を実行すると、シーケンス 0 の項目が削除されたことが認識されるため、1 つのステップで次のような単純な "編集スクリプト" が生成されます。

  • Remove the first text node. (最初のテキスト ノードを削除します。)

プログラムによってシーケンス番号を生成する場合の問題

代わりに、次のレンダリング ツリー ビルダー ロジックを記述したとします。

var seq = 0;

if (someFlag)
{
    builder.AddContent(seq++, "First");
}

builder.AddContent(seq++, "Second");

最初の出力は次の表のようになります。

シーケンス 種類 データ
0 テキスト ノード First
1 テキスト ノード Second

この結果は前のケースと同じであるため、否定的な問題は存在しません。 someFlag は 2 番目のレンダリングでは false であり、出力は次の表のようになります。

シーケンス 種類 データ
0 テキスト ノード Second

今度は、差分アルゴリズムによって、"2 つ" の変更が発生したことが認識されます。 アルゴリズムによって次の編集スクリプトが生成されます。

  • 最初のテキスト ノードの値を Second に変更します。
  • 2 番目のテキスト ノードを削除します。

シーケンス番号を生成すると、if/else 分岐とループが元のコードのどこにあったかに関する有用な情報がすべて失われます。 これにより、差分が以前の 2 倍の長さなります。

これは簡単な例です。 複雑で深く入れ子になった構造体で、特にループがある、より現実的なケースでは、通常、パフォーマンス コストが高くなります。 どのループ ブロックまたは分岐が挿入または削除されたかを即座に特定する代わりに、差分アルゴリズムではレンダリング ツリー内を深く再帰処理する必要があります。 これにより、古い構造と新しい構造体が相互にどのように関連しているかについて差分アルゴリズムが誤って通知されるため、通常、より長い編集スクリプトを作成することになります。

ガイダンスと結論

  • シーケンス番号が動的に生成される場合、アプリのパフォーマンスが低下します。
  • コンパイル時に情報がキャプチャされない限り、必要な情報が存在しないため、実行時にフレームワークでシーケンス番号を自動的に作成することはできません。
  • 手動で実装された RenderTreeBuilder ロジックの長いブロックは記述しないでください。 .razor ファイルを優先し、コンパイラがシーケンス番号を処理できるようにします。 手動の RenderTreeBuilder ロジックを回避できない場合は、長いブロックのコードを OpenRegion/CloseRegion 呼び出しでラップされたより小さな部分に分割します。 各リージョンには独自のシーケンス番号の個別のスペースがあるため、各リージョン内でゼロ (または他の任意の数) から再開できます。
  • シーケンス番号がハードコードされている場合、差分アルゴリズムでは、シーケンス番号の値が増えることだけが要求されます。 初期値とギャップは関係ありません。 合理的な選択肢の 1 つは、コード行番号をシーケンス番号として使用するか、ゼロから開始し、1 つずつまたは 100 ずつ (または任意の間隔で) 増やすことです。
  • ループの場合、シーケンス番号はソース コード内で増加し、実行時の動作にはよりません。 実際、実行時に数値が繰り返されることで、差分システムは、ユーザーがループ内にあることを認識できます。
  • Blazor ではシーケンス番号が使用されていますが、他のツリー差分 UI フレームワークでは使用されていません。 シーケンス番号を使用すると、差分がはるかに高速になります。また、Blazor には、.razor ファイルを作成する開発者に対して、シーケンス番号を自動的に処理するコンパイル ステップの利点があります。