ASP.NET Core Blazor パフォーマンスに関するベスト プラクティス

注意

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

重要

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

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

Blazor は、最も現実的なアプリケーション UI シナリオでハイ パフォーマンスを実現するために最適化されています。 しかし、最適なパフォーマンスが得られるかどうかは、開発者が適切なパターンと機能を採用するかどうかによります。

Note

この記事のコード例では、null 許容参照型 (NRT) と .NET コンパイラの null 状態スタティック分析を採用しています。これは、.NET 6 以降の ASP.NET Core でサポートされています。

レンダリング速度を最適化する

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

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

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

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

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

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

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

  • 子コンポーネントのパラメーターが確実に、stringintboolDateTime などのプリミティブ不変型となるようにします。 変更を検出するための組み込みロジックでは、プリミティブ不変パラメーター値が変更されていなかった場合、再レンダリングが自動的にスキップされます。 <Customer CustomerId="@item.CustomerId" /> (ここで、CustomerIdint 型です) を持つ子コンポーネントをレンダリングする場合、item.CustomerId が変更されない限り、Customer コンポーネントは再レンダリングされません。
  • オーバーライド ShouldRender:
    • 複合カスタム モデル型、イベント コールバック、RenderFragment 値などの非プリミティブ パラメーター値を受け入れるため。
    • パラメーター値の変更に関係なく、初期レンダリング後に変更されない UI 専用のコンポーネントを作成する場合。

次の航空会社のフライトの検索ツール例では、プライベート フィールドを使用して、変更を検出するために必要な情報を追跡します。 前のインバウンド フライト識別子 (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; }
}

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

<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 コードを再利用できるようにするには、RenderFragmentpublic および static を宣言します。

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; }
}

ただし、前の例で示したように、テーブル セル コンポーネントを使用せず、代わりにそのロジックを親コンポーネントにインライン化した方が改善される場合があることを考慮してください。

Note

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

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

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

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

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

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

コンポーネントによって this がカスケード値として渡される場合、IsFixedtrue に設定することもできます。

<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 のイベント処理の動作を制御できます。

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

次の例では、コンポーネントに追加されたイベント ハンドラーによって再レンダリングがトリガーされないため、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 => { };
    }
}

JavaScript 相互運用の速度を最適化する

.NET と JavaScript の間の呼び出しには、次の理由から追加のオーバーヘッドが必要です。

  • 既定では、呼び出しは非同期です。
  • 既定では、パラメーターと戻り値は JSON でシリアル化され、.NET および JavaScript 型の間でわかりやすい変換メカニズムが提供されます。

さらにサーバー側の Blazor アプリの場合、これらの呼び出しはネットワーク経由で渡されます。

過度に細かい呼び出しを避ける

各呼び出しにはある程度のオーバーヘッドが伴うため、呼び出しの回数を減らすことが有益な場合があります。 ブラウザーの localStorage に項目のコレクションを格納する次のコードについて考えてみます。

private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
    foreach (var item in items)
    {
        await JS.InvokeVoidAsync("localStorage.setItem", item.Id, 
            JsonSerializer.Serialize(item));
    }
}

上の例では、各項目に対して個別の JS 相互運用呼び出しを行います。 代わりに、次の方法では JS 相互運用を 1 回の呼び出しに減らしています。

private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
    await JS.InvokeVoidAsync("storeAllInLocalStorage", items);
}

対応する JavaScript 関数では、クライアントの項目のコレクション全体を格納します。

function storeAllInLocalStorage(items) {
  items.forEach(item => {
    localStorage.setItem(item.id, JSON.stringify(item));
  });
}

Blazor WebAssembly アプリの場合、個々の JS 相互運用呼び出しを 1 回の呼び出しにローリングすると、通常、コンポーネントで多数の JS 相互運用呼び出しを行う場合にのみ、パフォーマンスが大幅に向上します。

同期呼び出しの使用を検討する

.NET から JavaScript を呼び出す

このセクションはクライアント側コンポーネントにのみ適用されます。

JS 相互運用呼び出しは、呼び出されたコードが同期であるか非同期であるかに関係なく、既定で非同期となります。 呼び出しが既定で非同期であるのは、サーバー側とクライアント側のレンダリング モードでコンポーネントの互換性を確保するためです。 サーバーでは、すべての JS 相互運用呼び出しはネットワーク接続を介して送信されるため、非同期である必要があります。

コンポーネントが WebAssembly でのみ実行されることが確実にわかっている場合は、同期 JS 相互運用呼び出しを行うように選択できます。 これにより、非同期呼び出しを行う場合よりもオーバーヘッドがわずかに減少し、レンダリング サイクルが少なくなる可能性があります。これは、結果を待機する間の中間状態が存在しないためです。

クライアント側コンポーネントで .NET から JavaScript への同期呼び出しを行うには、IJSRuntimeIJSInProcessRuntime にキャストして JS 相互運用呼び出しを行います。

@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}

ASP.NET Core 5.0 以降のクライアント側コンポーネントの IJSObjectReference で作業する場合は、代わりに IJSInProcessObjectReference を同期的に使用できます。 IJSInProcessObjectReferenceIAsyncDisposable/IDisposable を実装しており、次の例に示すように、メモリ リークを防ぐためにガベージ コレクションに破棄する必要があります。

@inject IJSRuntime JS
@implements IAsyncDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", 
            "./scripts.js");
        }
    }

    ...

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

JavaScript から .NET を呼び出す

このセクションはクライアント側コンポーネントにのみ適用されます。

JS 相互運用呼び出しは、呼び出されたコードが同期であるか非同期であるかに関係なく、既定で非同期となります。 呼び出しが既定で非同期であるのは、サーバー側とクライアント側のレンダリング モードでコンポーネントの互換性を確保するためです。 サーバーでは、すべての JS 相互運用呼び出しはネットワーク接続を介して送信されるため、非同期である必要があります。

コンポーネントが WebAssembly でのみ実行されることが確実にわかっている場合は、同期 JS 相互運用呼び出しを行うように選択できます。 これにより、非同期呼び出しを行う場合よりもオーバーヘッドがわずかに減少し、レンダリング サイクルが少なくなる可能性があります。これは、結果を待機する間の中間状態が存在しないためです。

クライアント側コンポーネントで JavaScript から .NET への同期呼び出しを行うには、DotNet.invokeMethodAsync ではなく DotNet.invokeMethod を使用します。

同期呼び出しは、次の場合に機能します。

  • コンポーネントは、WebAssembly で実行するためにのみレンダリングされます。
  • 呼び出された関数から同期的に値が返される。 この関数は async メソッドではなく、.NET Task や JavaScript Promise は返されません。

このセクションはクライアント側コンポーネントにのみ適用されます。

JS 相互運用呼び出しは、呼び出されたコードが同期であるか非同期であるかに関係なく、既定で非同期となります。 呼び出しが既定で非同期であるのは、サーバー側とクライアント側のレンダリング モードでコンポーネントの互換性を確保するためです。 サーバーでは、すべての JS 相互運用呼び出しはネットワーク接続を介して送信されるため、非同期である必要があります。

コンポーネントが WebAssembly でのみ実行されることが確実にわかっている場合は、同期 JS 相互運用呼び出しを行うように選択できます。 これにより、非同期呼び出しを行う場合よりもオーバーヘッドがわずかに減少し、レンダリング サイクルが少なくなる可能性があります。これは、結果を待機する間の中間状態が存在しないためです。

クライアント側コンポーネントで .NET から JavaScript への同期呼び出しを行うには、IJSRuntimeIJSInProcessRuntime にキャストして JS 相互運用呼び出しを行います。

@inject IJSRuntime JS

...

@code {
    protected override void HandleSomeEvent()
    {
        var jsInProcess = (IJSInProcessRuntime)JS;
        var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
    }
}

ASP.NET Core 5.0 以降のクライアント側コンポーネントの IJSObjectReference で作業する場合は、代わりに IJSInProcessObjectReference を同期的に使用できます。 IJSInProcessObjectReferenceIAsyncDisposable/IDisposable を実装しており、次の例に示すように、メモリ リークを防ぐためにガベージ コレクションに破棄する必要があります。

@inject IJSRuntime JS
@implements IAsyncDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", 
            "./scripts.js");
        }
    }

    ...

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

マーシャリング解除された呼び出しの使用を検討する

"このセクションは Blazor WebAssembly アプリにのみ適用されます。"

Blazor WebAssembly で実行する場合、.NET から JavaScript へのマーシャリング解除された呼び出しを行うことができます。 これらは、引数または戻り値の JSON シリアル化を実行しない同期呼び出しです。 .NET と JavaScript の表現の間のメモリ管理と翻訳のすべての側面は、開発者に任されます。

警告

IJSUnmarshalledRuntime を使用すると JS 相互運用アプローチのオーバーヘッドが最小限になりますが、これらの API とのやりとりに必要な JavaScript API は現在ドキュメントに記載されていません。また、今後のリリースでの破壊的変更の対象となることがあります。

function jsInteropCall() {
  return BINDING.js_to_mono_obj("Hello world");
}
@inject IJSRuntime JS

@code {
    protected override void OnInitialized()
    {
        var unmarshalledJs = (IJSUnmarshalledRuntime)JS;
        var value = unmarshalledJs.InvokeUnmarshalled<string>("jsInteropCall");
    }
}

JavaScript [JSImport]/[JSExport] 相互運用の使用

Blazor WebAssembly アプリの JavaScript [JSImport]/[JSExport] 相互運用により、.NET 7 の ASP.NET Core より前のフレームワーク リリースの JS 相互運用 API よりもパフォーマンスと安定性が向上します。

詳細については、「ASP.NET Core Blazor を使用した JavaScript JSImport/JSExport 相互運用」を参照してください。

Ahead-Of-Time (AOT) コンパイル

Ahead-of-time (AOT) コンパイルでは、Blazor アプリの .NET コードをネイティブ WebAssembly に直接コンパイルし、ブラウザーによる直接実行ができるようにします。 AOT でコンパイルされたアプリは、サイズの大きいアプリになり、ダウンロードに時間がかかります。しかし、一般に、AOT でコンパイルされたアプリは実行時のパフォーマンスが向上します。CPU を集中的に使用するタスクを実行するアプリの場合は、特にそうです。 詳細については、「ASP.NET Core Blazor WebAssembly ビルド ツールと Ahead-Of-Time (AOT) コンパイル」を参照してください。

アプリのダウンロード サイズを最小限にする

ランタイムの再リンク

ランタイムの再リンクによってアプリのダウンロード サイズを最小化する方法については、「ASP.NET Core Blazor WebAssembly ビルド ツールと Ahead-Of-Time (AOT) コンパイル」をご覧ください。

System.Text.Json を使用します

Blazor の JS 相互運用の実装は System.Text.Json に依存します。これは、メモリ割り当てが少ない高パフォーマンスの JSON シリアル化ライブラリです。 System.Text.Json を使用しても、1 つ以上の代替 JSON ライブラリを追加するよりも、アプリ ペイロードのサイズが大きくなることはありません。

移行のガイダンスについては、「Newtonsoft.Json から System.Text.Json に移行する方法」を参照してください。

中間言語 (IL) のトリミング

"このセクションは Blazor WebAssembly アプリにのみ適用されます。"

使用されていないアセンブリを Blazor WebAssembly アプリからトリミングすると、アプリのバイナリで使用されていないコードを削除して、アプリのサイズを縮小することができます。 詳しくは、「ASP.NET Core Blazor 用のトリマーを構成する」をご覧ください。

Blazor WebAssembly アプリをリンクすると、アプリのバイナリで使用されていないコードをトリミングすることで、アプリのサイズが縮小されます。 既定では、中間言語 (IL) リンカーは、Release 構成でビルドする場合にのみ有効になります。 これを活用するには、-c|--configuration オプションを Release に設定した状態で dotnet publish コマンドを使用して、展開用にアプリを発行します。

dotnet publish -c Release

アセンブリの遅延読み込み

"このセクションは Blazor WebAssembly アプリにのみ適用されます。"

アセンブリがルートによって要求されたときに、実行時にアセンブリを読み込みます。 詳しくは、「ASP.NET Core Blazor WebAssembly でのアセンブリの遅延読み込み」をご覧ください。

圧縮

"このセクションは Blazor WebAssembly アプリにのみ適用されます。"

Blazor WebAssembly アプリが公開されると、公開中に出力が静的に圧縮されてアプリのサイズが縮小され、実行時の圧縮に必要なオーバーヘッドがなくなります。 Blazor は、コンテンツ ネゴシエーションの実行および静的に圧縮されたファイルの提供に関して、サーバーに依存します。

アプリが展開されたら、アプリが圧縮ファイルを提供していることを確認します。 ブラウザーの [開発者ツール][ネットワーク] タブを調べ、ファイルが Content-Encoding: br (Brotli 圧縮) または Content-Encoding: gz (Gzip 圧縮) で提供されていることを確認します。 ホストが圧縮ファイルを提供していない場合は、「ASP.NET Core Blazor WebAssembly のホストと展開」の手順に従ってください。

未使用の機能を無効にする

"このセクションは Blazor WebAssembly アプリにのみ適用されます。"

Blazor WebAssembly のランタイムには、次の .NET 機能が含まれており、ペイロード サイズを小さくするために無効にすることができます。

  • データ ファイルは、タイムゾーン情報を正しく保つために含まれています。 アプリでこの機能を必要としない場合は、アプリのプロジェクト ファイル内の BlazorEnableTimeZoneSupport MSBuild プロパティを false に設定して無効にすることを検討してください。

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • StringComparison.InvariantCultureIgnoreCase などの API を正常に動作させるために、照合順序情報が含まれています。 アプリで照合順序情報を必要としないことが確実な場合は、アプリのプロジェクト ファイル内の BlazorWebAssemblyPreserveCollationData MSBuild プロパティを false に設定して無効にすることを検討してください。

    <PropertyGroup>
      <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
    </PropertyGroup>