次の方法で共有


ASP.NET Core Blazor レンダリングパフォーマンスのベスト プラクティス

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

警告

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

レンダリング速度を最適化して、レンダリング ワークロードを最小化し、UI の応答性を向上させます。これにより、UI のレンダリング速度を ''10 倍以上向上させる'' ことができます。

コンポーネントのサブツリーの不要なレンダリングを避ける

イベントが発生したときに子コンポーネントのサブツリーの再レンダリングをスキップすることで、親コンポーネントのレンダリング コストの大部分を取り除くことができる場合があります。 サブツリーのレンダリングに特にコストがかかり、UI の遅延の原因となっている場合にのみ、それらの再レンダリングをスキップすることを検討してください。

実行時に、コンポーネントは階層内に存在します。 ルート コンポーネント (最初に読み込まれるコンポーネント) には子コンポーネントがあります。 さらに、ルートの子にはそれぞれの子コンポーネントがあり、同様に続きます。 ユーザーがボタンを選択するなどのイベントが発生すると、以下のプロセスで再レンダリングされるコンポーネントが判断されます。

  1. イベントは、そのイベントのハンドラーをレンダリングしたコンポーネントにディスパッチされます。 イベント ハンドラーの実行後、そのコンポーネントは再レンダリングされます。
  2. コンポーネントが再レンダリングされると、その各子コンポーネントに対してパラメーター値の新しいコピーが提供されます。
  3. 新しいパラメーター値のセットを受け取った後、 Blazor はコンポーネントを再レンダリングするかどうかを決定します。 ShouldRendertrueを返す場合、コンポーネントは再レンダリングされます。これはオーバーライドされない限り既定の動作であり、パラメーター値が変更可能なオブジェクトである場合など、パラメーター値が変更されている可能性があります。

前のシーケンスの最後の 2 つの手順は、コンポーネント階層を下って再帰的に繰り返されます。 多くの場合、サブツリー全体が再レンダリングされます。 高レベル コンポーネントを対象とするイベントは、その高レベル コンポーネントの下にあるすべてのコンポーネントを再レンダリングする必要があるため、再レンダリングのコストが高くなる場合があります。

特定のサブツリーへの再帰的なレンダリングを防ぐには、次のいずれかの方法を使用します。

  • 子コンポーネント のパラメーターが、 stringint†、 boolDateTimeなど、特定の変更できない型であることを確認します。 変更を検出するための組み込みロジックでは、変更できないパラメーター値が変更されていない場合、再レンダリングは自動的にスキップされます。 <Customer CustomerId="item.CustomerId" /> (ここで、CustomerIdint 型です) を持つ子コンポーネントをレンダリングする場合、Customer が変更されない限り、item.CustomerId コンポーネントは再レンダリングされません。
  • ShouldRenderをオーバーライドし、falseを返します。
    • パラメーターが非特権型またはサポートされていない不変型である場合†複雑なカスタム モデル型や RenderFragment 値など、パラメーター値が変更されていない場合は、
    • パラメーター値の変更に関係なく、初期レンダリング後に変更されない UI 専用のコンポーネントを作成する場合。

†詳細については、Blazorの参照ソース (ChangeDetection.cs) の変更検出ロジックを参照してください。

通常、.NET 参照ソースへのドキュメント リンクを使用すると、リポジトリの既定のブランチが読み込まれます。このブランチは、.NET の次回リリースに向けて行われている現在の開発を表します。 特定のリリースのタグを選択するには、[Switch branches or tags](ブランチまたはタグの切り替え) ドロップダウン リストを使います。 詳細については、「ASP.NET Core ソース コードのバージョン タグを選択する方法」 (dotnet/AspNetCore.Docs #26205) を参照してください。

次の航空会社のフライトの検索ツール例では、プライベート フィールドを使用して、変更を検出するために必要な情報を追跡します。 前のインバウンド フライト識別子 (prevInboundFlightId) と前のアウトバウンド フライト識別子 (prevOutboundFlightId) では、次に考えられるコンポーネントの更新に関する情報を追跡します。 コンポーネントのパラメーターが OnParametersSet に設定されているときに、いずれかのフライト識別子が変更された場合、shouldRendertrue に設定されているため、コンポーネントが再レンダリングされます。 フライト識別子の確認後に shouldRenderfalse に評価された場合、負荷の高い再レンダリングは回避されます。

@code {
    private int prevInboundFlightId = 0;
    private int prevOutboundFlightId = 0;
    private bool shouldRender;

    [Parameter]
    public FlightInfo? InboundFlight { get; set; }

    [Parameter]
    public FlightInfo? OutboundFlight { get; set; }

    protected override void OnParametersSet()
    {
        shouldRender = InboundFlight?.FlightId != prevInboundFlightId
            || OutboundFlight?.FlightId != prevOutboundFlightId;

        prevInboundFlightId = InboundFlight?.FlightId ?? 0;
        prevOutboundFlightId = OutboundFlight?.FlightId ?? 0;
    }

    protected override bool ShouldRender() => shouldRender;
}

イベント ハンドラーで、shouldRendertrue に設定することもできます。 ほとんどのコンポーネントでは、通常、個々のイベント ハンドラーのレベルで再レンダリングを判断する必要はありません。

詳細については、次のリソースを参照してください。

仮想化

数千ものエントリが含まれたリストやグリッドなど、ループ内で大量の UI をレンダリングする場合、膨大な量のレンダリング操作によって UI のレンダリングに遅延が発生するおそれがあります。 ユーザーがスクロールなしで一度に少数の要素しか表示できない場合、現在表示されていない要素のレンダリングに時間を費やすのは無駄になることがよくあります。

Blazor には Virtualize<TItem> コンポーネントが用意されています。これを使用すると、任意のサイズのリストの外観とスクロール動作が作成されますが、現在のスクロール ビューポート内のリスト項目のみがレンダリングされます。 たとえば、コンポーネントでは 100,000 個のエントリを含むリストをレンダリングできますが、表示される 20 項目のレンダリング コストのみを負担することになります。

詳しくは、「ASP.NET Core Razor コンポーネントの仮想化」をご覧ください。

軽量で最適化されたコンポーネントを作成する

ほとんどの Razor コンポーネントで積極的に最適化を行う必要はありません。これは、UI ではほとんどのコンポーネントが繰り返されず、高頻度で再レンダリングされないためです。 たとえば、ダイアログやフォームなどの UI の高レベルの部分をレンダリングするために使用される @page ディレクティブとコンポーネントを含む、ルーティング可能なコンポーネントは、ほとんどの場合、一度に 1 つだけ表示され、ユーザー ジェスチャへの応答としてのみ再レンダリングされます。 通常はこれらのコンポーネントによってレンダリングの高ワークロードが作成されることはないため、レンダリングのパフォーマンスについてあまり心配することなく、フレームワーク機能を自由に組み合わせて使用できます。

しかし、コンポーネントが大規模に繰り返され、多くの場合、UI のパフォーマンスが低下する次のような一般的なシナリオがあります。

  • 入力やラベルなど、何百もの個別の要素を含む、大きな入れ子になったフォーム。
  • 数百の行または数千のセルを含むグリッド。
  • 何百万ものデータポイントを含む散布図。

各要素、セル、またはデータ ポイントを個別のコンポーネント インスタンスとしてモデリングする際は、多くの場合、その数が多くなり、レンダリングのパフォーマンスが重要になります。 このセクションでは、UI の速度と応答性を維持できるように、これらのコンポーネントを軽量にするためのアドバイスを提供します。

何千ものコンポーネント インスタンスを避ける

各コンポーネントは、その親と子とは無関係にレンダリングできる独立した島です。 UI をコンポーネント階層に分割する方法を選択することで、UI レンダリングの細分性を制御できます。 これにより、パフォーマンスが向上するか、低下するおそれがあります。

UI を別々のコンポーネントに分割することで、イベントの発生時に再レンダリングする UI の部分を小さくすることができます。 各行に 1 つのボタンがある多数の行を含むテーブルでは、ページまたはテーブル全体ではなく、子コンポーネントを使用することによって、単一行のみを再レンダリングできる場合があります。 しかし、各コンポーネントには、その独立した状態とレンダリング ライフサイクルを処理するための、追加のメモリと CPU オーバーヘッドが必要です。

ASP.NET Core 製品ユニット エンジニアによって実行されるテストでは、コンポーネント インスタンスあたり約 0.06 ms のレンダリング オーバーヘッドが Blazor WebAssembly アプリで検出されました。 テスト アプリでは、3 つのパラメーターを受け入れるシンプルなコンポーネントがレンダリングされています。 内部的には、オーバーヘッドの大部分が、ディクショナリからのコンポーネントごとの状態の取得と、パラメーターの受け渡しに起因します。 乗算により、2,000 個のコンポーネント インスタンスを新たに追加するとレンダリング時間が 0.12 秒長くなり、ユーザーが UI を低速に感じ始めることがわかります。

コンポーネントをより軽量にして、その数を増やすこともできます。 しかし、より強力な手法では多くの場合、非常に多くのコンポーネントがレンダリングされるのを回避します。 次のセクションでは、実行できる 2 つの方法について説明します。

メモリ管理の詳細については、「 デプロイされた ASP.NET Core サーバー側の Blazor アプリでメモリを管理する」を参照してください。

子コンポーネントを親にインライン化します。 ループ内で子コンポーネントをレンダリングする親コンポーネントの次の部分について考えてみましょう。

<div class="chat">
    @foreach (var message in messages)
    {
        <ChatMessageDisplay Message="message" />
    }
</div>

ChatMessageDisplay.razor:

<div class="chat-message">
    <span class="author">@Message.Author</span>
    <span class="text">@Message.Text</span>
</div>

@code {
    [Parameter]
    public ChatMessage? Message { get; set; }
}

前の例では、何千ものメッセージが一度に表示されない場合、パフォーマンスは良好です。 何千ものメッセージを一度に表示するために、個別のコンポーネントを切り離さないことを検討してください。 代わりに、子コンポーネントを親コンポーネント内にインライン化します。 次の方法では、各子コンポーネントのマークアップを個別にレンダリングできなくなる代わりに、非常に多くの子コンポーネントをレンダリングするコンポーネントごとのオーバーヘッドを回避します。

<div class="chat">
    @foreach (var message in messages)
    {
        <div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>
    }
</div>

再利用可能な RenderFragments をコードで定義する: レンダリング ロジックを再利用する方法として、子コンポーネントを純粋に除外している可能性があります。 その場合は、追加のコンポーネントを実装することなく、再利用可能なレンダリング ロジックを作成できます。 任意のコンポーネントの @code ブロックで、RenderFragment を定義します。 必要なだけフラグメントを任意の場所からレンダリングします。

@RenderWelcomeInfo

<p>Render the welcome content a second time:</p>

@RenderWelcomeInfo

@code {
    private RenderFragment RenderWelcomeInfo = @<p>Welcome to your new app!</p>;
}

複数のコンポーネント間で RenderTreeBuilder のコードを再利用できるようにするには、RenderFragmentpublicstatic を宣言します。

public static RenderFragment SayHello = @<h1>Hello!</h1>;

上の例の SayHello は、関連のないコンポーネントから呼び出すことができます。 この手法は、コンポーネントごとのオーバーヘッドなしでレンダリングされる、再利用可能なマークアップ スニペットのライブラリを構築する場合に便利です。

RenderFragment デリゲートはパラメーターを受け取ることができます。 次のコンポーネントでは、メッセージ (message) を RenderFragment デリゲートに渡します。

<div class="chat">
    @foreach (var message in messages)
    {
        @ChatMessageDisplay(message)
    }
</div>

@code {
    private RenderFragment<ChatMessage> ChatMessageDisplay = message =>
        @<div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>;
}

上記の方法では、コンポーネントごとのオーバーヘッドなしでレンダリング ロジックが再利用されます。 しかし、この方法では UI のサブツリーを個別に更新できません。また、コンポーネント境界がないため、親がレンダリングされるときに UI のサブツリーのレンダリングをスキップすることもできません。 RenderFragment デリゲートへの割り当ては、Razor コンポーネント ファイル (.razor) でのみサポートされます。

次の例の TitleTemplate など、フィールド初期化子で参照できない非静的フィールド、メソッド、またはプロパティについては、RenderFragment のフィールドの代わりにプロパティを使用します。

protected RenderFragment DisplayTitle =>
    @<div>
        @TitleTemplate
    </div>;

受け取るパラメーターの数が多すぎないようにする

1 つのコンポーネントが非常に頻繁に (たとえば、数百回または数千回) 繰り返される場合は、各パラメーターを受け渡しするオーバーヘッドが増大します。

多すぎるパラメーターによってパフォーマンスが著しく制限されることはまれですが、1 つの要因になる可能性があります。 グリッド内で 4,000 回レンダリングされる TableCell コンポーネントの場合、そのコンポーネントに渡される追加パラメーターごとに、レンダリングの総コストに約 15 ms が加えられます。 10 個のパラメーターを渡すと、約 150 ms が必要になり、UI レンダリングの遅延が発生します。

パラメーターの負荷を減らすには、カスタム クラスに複数のパラメーターをバンドルします。 たとえば、テーブル セル コンポーネントでは、共通のオブジェクトを受け入れる場合があります。 次の例では、Data はセルごとに異なりますが、Options はすべてのセルで共通です。

@typeparam TItem

...

@code {
    [Parameter]
    public TItem? Data { get; set; }
    
    [Parameter]
    public GridOptions? Options { get; set; }
}

ただし、プリミティブ パラメーターをクラスにバンドルすることは必ずしも利点ではないことに注意してください。 パラメーターの数を減らすことができますが、変更の検出とレンダリングの動作にも影響します。 プリミティブ以外のパラメーターを渡すと、常に再レンダリングがトリガーされます。Blazor は、任意のオブジェクトが内部的に変更可能な状態であるかどうかを認識できないためです。一方、プリミティブ パラメーターを渡すと、値が実際に変更された場合にのみ再レンダリングがトリガーされます。

テーブルセルコンポーネントを持たないことが改善と考えられるケースもあるので、前の例に示すように、そのロジックを親コンポーネントにインライン化することを検討してください

パフォーマンスを向上させるために複数の方法を使用できる場合は、通常、最適な結果が得られる方法を判断するために、方法のベンチマークが必要になります。

ジェネリック型パラメーター (@typeparam) の詳細については、次のリソースを参照してください。

カスケード型パラメーターが固定されていることを確認する

CascadingValue コンポーネントには、省略可能な IsFixed パラメーターがあります。

  • IsFixedfalse (既定値) の場合、カスケード値のすべての受信者は、変更通知を受け取るためのサブスクリプションを設定します。 サブスクリプションの追跡により、各[CascadingParameter]は通常のよりもかなり高価です。
  • IsFixedtrue (<CascadingValue Value="someValue" IsFixed="true"> など) の場合、受信者は初期値を受け取りますが、更新プログラムを受け取るためのサブスクリプションを設定しません。 各 [CascadingParameter] は軽量であり、通常の [Parameter] よりもコストが高くなることはありません。

IsFixedtrue に設定すると、カスケード値を受け取る他のコンポーネントが多数存在する場合にパフォーマンスが向上します。 可能な限り、カスケード値に対して IsFixedtrue に設定してください。 指定された値が時間の経過と共に変化しない場合は、IsFixedtrue に設定できます。

コンポーネントがカスケード値として this を渡す場合、IsFixed がコンポーネントのライフサイクルで変わらないため、truethisに設定することもできます。

<CascadingValue Value="this" IsFixed="true">
    <SomeOtherComponents>
</CascadingValue>

詳しくは、「ASP.NET Core Blazor の値とパラメーターのカスケード」をご覧ください。

CaptureUnmatchedValues で属性スプラッティングを避ける

コンポーネントでは、CaptureUnmatchedValues フラグを使用して、"一致しない" パラメーター値を受け取ることができます。

<div @attributes="OtherAttributes">...</div>

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? OtherAttributes { get; set; }
}

この方法では、任意の追加属性を要素に渡すことができます。 しかし、レンダラーで次のことを行う必要があるため、この方法にはコストがかかります。

  • 指定されたすべてのパラメーターを既知のパラメーターのセットと照合して、ディクショナリを構築する。
  • 同じ属性の複数のコピーが相互に上書きされているかどうかを追跡する。

頻繁に繰り返されないコンポーネントなど、コンポーネント レンダリングのパフォーマンスが重要でない場合は、CaptureUnmatchedValues を使用します。 大きなリスト内やグリッドのセル内の各項目など、大規模にレンダリングされるコンポーネントの場合は、属性のスプラッティングを避けるようにしてください。

詳細については、「ASP.NET Core Blazor の属性のスプラッティングと任意のパラメーター」を参照してください。

手動で SetParametersAsync を実装する

コンポーネントごとのレンダリング オーバーヘッドの重要な原因は、受信パラメーター値を [Parameter] プロパティに書き込んでいることです。 レンダラーではリフレクションを使用してパラメーター値を書き込みます。これにより、パフォーマンスが大きく低下するおそれがあります。

極端なケースでは、リフレクションを使用せずに、独自のパラメーター設定ロジックを手動で実装したい場合があります。 これは、次の場合に該当します。

  • UI に数百または数千のコンポーネントのコピーがある場合など、コンポーネントが非常に頻繁にレンダリングされる。
  • コンポーネントで多くのパラメーターが受け入れられる。
  • パラメーターを受け取るオーバーヘッドが UI の応答性に目に見える影響を与えていることがわかる。

極端なケースでは、コンポーネントの仮想 SetParametersAsync メソッドをオーバーライドし、独自のコンポーネント固有のロジックを実装できます。 次の例では、ディクショナリ参照を意図的に回避しています。

@code {
    [Parameter]
    public int MessageId { get; set; }

    [Parameter]
    public string? Text { get; set; }

    [Parameter]
    public EventCallback<string> TextChanged { get; set; }

    [Parameter]
    public Theme CurrentTheme { get; set; }

    public override Task SetParametersAsync(ParameterView parameters)
    {
        foreach (var parameter in parameters)
        {
            switch (parameter.Name)
            {
                case nameof(MessageId):
                    MessageId = (int)parameter.Value;
                    break;
                case nameof(Text):
                    Text = (string)parameter.Value;
                    break;
                case nameof(TextChanged):
                    TextChanged = (EventCallback<string>)parameter.Value;
                    break;
                case nameof(CurrentTheme):
                    CurrentTheme = (Theme)parameter.Value;
                    break;
                default:
                    throw new ArgumentException($"Unknown parameter: {parameter.Name}");
            }
        }

        return base.SetParametersAsync(ParameterView.Empty);
    }
}

上のコードでは、基底クラス SetParametersAsync を返すと、パラメーターを再度割り当てずに通常のライフサイクル メソッドが実行されます。

上のコードでわかるように、SetParametersAsync のオーバーライドとカスタム ロジックの提供は複雑で手間がかかるため、一般にこの方法を採用することはお勧めしません。 極端なケースでは、これによってレンダリング パフォーマンスを 20 から 25% 向上させることができますが、このセクションの前述の一覧にある極端なシナリオでのみ、この方法を検討してください。

イベントをすぐにトリガーしない

一部のブラウザー イベントは非常に頻繁に発生します。 たとえば、onmousemoveonscroll は、1 秒あたり数十回または数百回発生する場合があります。 ほとんどの場合、これほど頻繁に UI 更新を実行する必要はありません。 イベントがトリガーされるのが速すぎると、UI の応答性が損なわれたり、CPU 時間が過剰に消費されたりするおそれがあります。

すぐに発生するネイティブ イベントを使用するのではなく JS 相互運用機能を使って、発生頻度の低いコールバックを登録することを検討してください。 たとえば、次のコンポーネントにはマウスの位置が表示されますが、500 ms ごとに最大で 1 回しか更新されません。

@implements IDisposable
@inject IJSRuntime JS

<h1>@message</h1>

<div @ref="mouseMoveElement" style="border:1px dashed red;height:200px;">
    Move mouse here
</div>

@code {
    private ElementReference mouseMoveElement;
    private DotNetObjectReference<MyComponent>? selfReference;
    private string message = "Move the mouse in the box";

    [JSInvokable]
    public void HandleMouseMove(int x, int y)
    {
        message = $"Mouse move at {x}, {y}";
        StateHasChanged();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            selfReference = DotNetObjectReference.Create(this);
            var minInterval = 500;

            await JS.InvokeVoidAsync("onThrottledMouseMove", 
                mouseMoveElement, selfReference, minInterval);
        }
    }

    public void Dispose() => selfReference?.Dispose();
}

対応する JavaScript コードでは、マウス移動の DOM イベント リスナーを登録します。 この例の場合、イベント リスナーでは Lodash の throttle 関数を使用して、呼び出し率を制限しています。

<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.20/lodash.min.js"></script>
<script>
  function onThrottledMouseMove(elem, component, interval) {
    elem.addEventListener('mousemove', _.throttle(e => {
      component.invokeMethodAsync('HandleMouseMove', e.offsetX, e.offsetY);
    }, interval));
  }
</script>

イベントの処理後に状態を変更しないで再レンダリングを回避する

コンポーネントは ComponentBase から継承し、コンポーネントのイベント ハンドラーが呼び出された後で自動的に StateHasChanged が呼び出されます。 場合によっては、イベント ハンドラーが呼び出された後でレンダリングをトリガーすることが不要または望ましくないことがあります。 たとえば、イベント ハンドラーによってコンポーネントの状態が変更されていない場合があります。 このようなシナリオでは、アプリで IHandleEvent インターフェイスを利用して、Blazor のイベント処理の動作を制御できます。

このセクションのアプローチでは、例外をエラー境界にフローしません。 ComponentBase.DispatchExceptionAsync を呼び出すことによってエラー境界をサポートする詳細およびデモ コードについては、「AsNonRenderingEventHandler + ErrorBoundary = unexpected behavior (dotnet/aspnetcore #54543)」をご覧ください。

コンポーネントのすべてのイベント ハンドラーで再レンダリングを回避するには、IHandleEvent を実装し、IHandleEvent.HandleEventAsync を呼び出さずにイベント ハンドラーを呼び出す StateHasChanged タスクを提供します。

次の例では、コンポーネントに追加されたイベント ハンドラーによって再レンダリングがトリガーされないため、HandleSelect が呼び出されても再レンダリングは発生しません。

HandleSelect1.razor:

@page "/handle-select-1"
@using Microsoft.Extensions.Logging
@implements IHandleEvent
@inject ILogger<HandleSelect1> Logger

<p>
    Last render DateTime: @dt
</p>

<button @onclick="HandleSelect">
    Select me (Avoids Rerender)
</button>

@code {
    private DateTime dt = DateTime.Now;

    private void HandleSelect()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler doesn't trigger a rerender.");
    }

    Task IHandleEvent.HandleEventAsync(
        EventCallbackWorkItem callback, object? arg) => callback.InvokeAsync(arg);
}

グローバルな方法でコンポーネント内のイベント ハンドラーの後の再レンダリングを防ぐことに加えて、次のユーティリティ メソッドを使用することにより、単一のイベント ハンドラーの後の再レンダリングを防ぐことができます。

次の EventUtil クラスを Blazor アプリに追加します。 EventUtil クラス上の静的アクションと関数により、イベントを処理するときに Blazor によって使用される引数と戻り値の型の複数の組み合わせに対応するハンドラーが提供されます。

EventUtil.cs:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Components;

public static class EventUtil
{
    public static Action AsNonRenderingEventHandler(Action callback)
        => new SyncReceiver(callback).Invoke;
    public static Action<TValue> AsNonRenderingEventHandler<TValue>(
            Action<TValue> callback)
        => new SyncReceiver<TValue>(callback).Invoke;
    public static Func<Task> AsNonRenderingEventHandler(Func<Task> callback)
        => new AsyncReceiver(callback).Invoke;
    public static Func<TValue, Task> AsNonRenderingEventHandler<TValue>(
            Func<TValue, Task> callback)
        => new AsyncReceiver<TValue>(callback).Invoke;

    private record SyncReceiver(Action callback) 
        : ReceiverBase { public void Invoke() => callback(); }
    private record SyncReceiver<T>(Action<T> callback) 
        : ReceiverBase { public void Invoke(T arg) => callback(arg); }
    private record AsyncReceiver(Func<Task> callback) 
        : ReceiverBase { public Task Invoke() => callback(); }
    private record AsyncReceiver<T>(Func<T, Task> callback) 
        : ReceiverBase { public Task Invoke(T arg) => callback(arg); }

    private record ReceiverBase : IHandleEvent
    {
        public Task HandleEventAsync(EventCallbackWorkItem item, object arg) => 
            item.InvokeAsync(arg);
    }
}

EventUtil.AsNonRenderingEventHandler を呼び出して、呼び出されたときにレンダリングをトリガーしないイベント ハンドラーを呼び出します。

次に例を示します。

  • 最初のボタンを選択すると、HandleClick1 が呼び出され、再レンダリングがトリガーされます。
  • 2 番目のボタンを選択すると、HandleClick2 が呼び出され、再レンダリングはトリガーされません。
  • 3 番目のボタンを選択すると、HandleClick3 が呼び出され、再レンダリングはトリガーされずに、イベント引数 (MouseEventArgs) が使用されます。

HandleSelect2.razor:

@page "/handle-select-2"
@using Microsoft.Extensions.Logging
@inject ILogger<HandleSelect2> Logger

<p>
    Last render DateTime: @dt
</p>

<button @onclick="HandleClick1">
    Select me (Rerenders)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler(HandleClick2)">
    Select me (Avoids Rerender)
</button>

<button @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(HandleClick3)">
    Select me (Avoids Rerender and uses <code>MouseEventArgs</code>)
</button>

@code {
    private DateTime dt = DateTime.Now;

    private void HandleClick1()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler triggers a rerender.");
    }

    private void HandleClick2()
    {
        dt = DateTime.Now;

        Logger.LogInformation("This event handler doesn't trigger a rerender.");
    }
    
    private void HandleClick3(MouseEventArgs args)
    {
        dt = DateTime.Now;

        Logger.LogInformation(
            "This event handler doesn't trigger a rerender. " +
            "Mouse coordinates: {ScreenX}:{ScreenY}", 
            args.ScreenX, args.ScreenY);
    }
}

IHandleEvent インターフェイスを実装するだけでなく、この記事で説明されている他のベスト プラクティスを利用することで、イベントが処理された後の不要なレンダリングを減らすこともできます。 たとえば、ターゲット コンポーネントの子コンポーネントでの ShouldRender のオーバーライドは、再レンダリングを制御するために使用できます。

繰り返される多数の要素またはコンポーネント用のデリゲートの再作成を避ける

Blazorがループ内の要素またはコンポーネントのためにラムダ式のデリゲートを再作成することは、パフォーマンスの低下を招く可能性があります。

イベント処理に関する記事に示されている次のコンポーネントは、一連のボタンをレンダリングします。 それぞれのボタンは、デリゲートをその @onclick イベントに割り当てます。レンダリングするボタンの数が多くない場合は問題ありません。

EventHandlerExample5.razor:

@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}
@page "/event-handler-example-5"

<h1>@heading</h1>

@for (var i = 1; i < 4; i++)
{
    var buttonNumber = i;

    <p>
        <button @onclick="@(e => UpdateHeading(e, buttonNumber))">
            Button #@i
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private void UpdateHeading(MouseEventArgs e, int buttonNumber)
    {
        heading = $"Selected #{buttonNumber} at {e.ClientX}:{e.ClientY}";
    }
}

上記の方法を使用して多数のボタンがレンダリングされる場合は、レンダリング速度に悪影響を及ぼし、ユーザー エクスペリエンスの質が低下します。 クリック イベントのコールバックを使用して多数のボタンをレンダリングするために、次の例では、各ボタンの @onclick デリゲートを Action に割り当てるボタン オブジェクトのコレクションを使用します。 次の方法では、ボタンがレンダリングされるたびに、Blazor ですべてのボタン デリゲートをリビルドする必要がありません。

LambdaEventPerformance.razor:

@page "/lambda-event-performance"

<h1>@heading</h1>

@foreach (var button in Buttons)
{
    <p>
        <button @key="button.Id" @onclick="button.Action">
            Button #@button.Id
        </button>
    </p>
}

@code {
    private string heading = "Select a button to learn its position";

    private List<Button> Buttons { get; set; } = new();

    protected override void OnInitialized()
    {
        for (var i = 0; i < 100; i++)
        {
            var button = new Button();

            button.Id = Guid.NewGuid().ToString();

            button.Action = (e) =>
            {
                UpdateHeading(button, e);
            };

            Buttons.Add(button);
        }
    }

    private void UpdateHeading(Button button, MouseEventArgs e)
    {
        heading = $"Selected #{button.Id} at {e.ClientX}:{e.ClientY}";
    }

    private class Button
    {
        public string? Id { get; set; }
        public Action<MouseEventArgs> Action { get; set; } = e => { };
    }
}