ASP.NET Core Blazor 效能最佳做法

注意

這不是這篇文章的最新版本。 如需目前版本,請參閱本文的 .NET 8 版本

重要

這些發行前產品的相關資訊在產品正式發行前可能會有大幅修改。 Microsoft 對此處提供的資訊,不做任何明確或隱含的瑕疵擔保。

如需目前版本,請參閱本文的 .NET 8 版本

大多數的現實應用程式 UI 案例會將 Blazor 最佳化以獲得高效能。 不過,要獲得最佳效能,就要依靠開發人員採用正確的模式和功能。

注意

本文中的程式碼範例採用 可為 Null 的參考型別 (NRT) 和 .NET 編譯器 Null 狀態靜態分析,這在 .NET 6 或更新版本的 ASP.NET Core 中受到支援。

將轉譯速度最佳化

將轉譯速度最佳化以將轉譯工作負載降到最低並提升 UI 回應能力,從而將 UI 轉譯速度提升十倍以上

避免不必要的元件子樹轉譯

您可以在有事件發生時略過子元件子樹的重新轉譯,以去除大部分父元件的轉譯成本。 請只考慮略過轉譯成本特別高且會造成 UI 延遲的子樹重新轉譯。

在執行階段時,元件會存在於某個階層。 根元件 (載入的第一個元件) 具有子元件。 根元件的子元件會再有自己的子元件,依此類推。 發生使用者選取按鈕之類的事件時,下列程序會決定要重新轉譯的元件:

  1. 事件分派給轉譯事件處理常式的元件。 在執行事件處理常式後,元件便會重新轉譯。
  2. 元件重新轉譯時,會為其每個子元件提供參數值的新複本。
  3. 每個元件在收到新的一組參數值後會決定是否要重新轉譯。 根據預設,如果參數值可能已變更 (例如,如果這些值是可變動的物件),元件便會重新轉譯。

上述序列的最後兩個步驟會在元件階層中一直往下遞迴進行。 在許多情況中,整個子樹會重新轉譯。 以高階元件為目標的事件可能會造成成本很高的重新轉譯,因為高階元件下方的每個元件都必須重新轉譯。

若要防止轉譯遞迴進行到特定子樹,請使用下列任一方法:

  • 確定子元件參數屬於基本不可變型別,例如 stringintboolDateTime 和其他類似的型別。 如果基本不可變的參數值尚未變更,用於偵測變更的內建邏輯會自動略過重新轉譯。 如果您使用 <Customer CustomerId="@item.CustomerId" /> 轉譯子元件 (其中 CustomerIdint 型別),則除非 item.CustomerId 有所變更,否則 Customer 元件不會重新轉譯。
  • 覆寫 ShouldRender
    • 以接受非基本參數值,例如複雜的自訂模型型別、事件回呼或 RenderFragment 值。
    • 如果撰寫不會在初始轉譯後發生變更的僅限 UI 元件的話 (不論參數值是否變更)。

下列航班搜尋工具範例會使用私人欄位來追蹤必要資訊以偵測變更。 先前的入境航班識別碼 (prevInboundFlightId) 和先前的出境航班識別碼 (prevOutboundFlightId) 會追蹤下一個潛在元件更新的資訊。 當元件的參數有設定到 OnParametersSet 時,如果任一航班識別碼發生變更,元件便會重新轉譯,因為 shouldRender 設定為 true。 在檢查航班識別碼後,如果 shouldRender 評估為 false,則會避免進行成本高昂的重新轉譯:

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

事件處理常式也會將 shouldRender 設定為 true。 大部分的元件通常不需要在個別事件處理常式的層級判斷是否要進行重新轉譯。

如需詳細資訊,請參閱以下資源:

虛擬化

在迴圈內轉譯大量 UI 時 (例如,具有數千個項目的清單或格線),大量的轉譯作業可能會延遲 UI 轉譯。 由於使用者在不捲動的情況下一次只能看到一小部分的元素,因此花時間轉譯目前看不到的項目並不符合成本效益。

Blazor 提供了 Virtualize<TItem> 元件來建立任意大型清單的外觀和捲動行為,同時只轉譯目前捲動檢視區內的清單項目。 例如,某個元件可轉譯具有 100,000 個項目的清單,但只支付 20 個可見項目的轉譯成本。

如需詳細資訊,請參閱 ASP.NET Core Razor 元件虛擬化

建立已最佳化的輕量型元件

大部分 Razor 元件不需要進行積極的最佳化工作,因為大部分元件在 UI 中不會重複,因此不會極為頻率地重新轉譯。 例如,具有 @page 指示詞的可路由元件以及用來轉譯 UI 高階片段的元件 (例如對話方塊或表單) 很可能一次只會出現一個,而且只會在回應使用者的手勢時才重新轉譯。 這些元件通常不會產生很高的轉譯工作負載,因此您可以自由地使用任何架構功能組合,而不必擔心轉譯效能。

不過,在以下常見情況中,元件則會大規模重複,因此往往會導致 UI 效能不佳:

  • 具有數百個個別元素 (例如,輸入或標籤) 的大型巢狀表單。
  • 具有數百列或數千個儲存格的格線。
  • 具有數百萬個資料點的散佈圖。

如果將每個元素、儲存格或資料點模型化為個別的元件執行個體,其中許多項目的轉譯效能往往會變得十分重要。 本節會提供如何讓這類元件輕量化的建議,以便 UI 能夠保持快速且有所回應。

避免數千個元件執行個體

每個元件都是一座孤島,可獨立於父系和子系之外進行轉譯。 藉由選擇如何將 UI 分割成元件階層,您可以控制 UI 轉譯的細微性。 這可能會讓效能變好,也可能會讓效能變差。

藉由將 UI 分割成個別元件,您可以在事件發生時重新轉譯較小的 UI 部分。 如果資料表中在許多資料列,且這些資料列各自有一個按鈕,您或許可以使用子元件只重新轉譯該單一資料列,而不是重新轉譯整個頁面或資料表。 不過,每個元件都需要額外的記憶體和 CPU 額外負荷,才能處理其獨立狀態和轉譯生命週期。

在 ASP.NET Core 產品單位工程師所執行的測試中可以看到,Blazor WebAssembly 應用程式中每個元件執行個體大約會有 0.06 毫秒的轉譯額外負荷。 進行測試的應用程式轉譯了採用三個參數的簡單元件。 在內部,額外負荷主要來自從字典擷取每一元件狀態,以及傳遞和接收參數。 透過乘法計算可以看到,另外新增 2,000 個元件執行個體會讓轉譯時間增加 0.12 秒,因此使用者會開始感到 UI 的速度變慢。

您可以將元件輕量化,以便擁有更多元件。 不過,威力更加強大的技術往往會避免擁有過多需要轉譯的元件。 下列各節會描述您可以採用的兩種方法。

如需記憶體管理的詳細資訊,請參閱裝載和部署 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 程式碼可在多個元件之間重複使用,請宣告 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 子樹的轉譯。 只有 Razor 元件檔案 (.razor) 可支援指派給 RenderFragment 委派的操作,事件回呼則不受支援。

對於欄位初始設定式無法參考的非靜態欄位、方法或屬性 (例如下列範例中的 TitleTemplate),請針對 RenderFragment 使用屬性,而非使用欄位:

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

不要接收太多參數

如果元件會非常頻繁地重複 (例如數百次或數千次),傳遞和接收每個參數的額外負荷就會累積起來。

參數過多很少會嚴重限制效能,但這會是其中一個因素。 對於在格線內轉譯 4,000 次的 TableCell 元件,傳遞至該元件的每個參數可能會讓總轉譯成本增加約 15 毫秒。 傳遞十個參數大約需要 150 毫秒,並導致 UI 轉譯延遲。

若要減少參數負載,請將多個參數組合到自訂類別中。 例如,資料表儲存格元件可能會接受通用物件。 在下列範例中,每個儲存格的 Data 各不相同,但 Options 在所有儲存格執行個體中是通用的:

@typeparam TItem

...

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

不過,您要考慮到,如上述範例所示,不使用資料表儲存格元件,而是將其邏輯內嵌到父元件可能是一項改善。

注意

當有多個方法可用來改善效能時,通常需要對方法進行基準測試,以判斷哪一種方法會產生最佳結果。

如需泛型型別參數 (@typeparam) 的詳細資訊,請參閱下列資源:

確定串連參數已固定

CascadingValue 元件 具有選擇性的 IsFixed 參數:

  • 如果 IsFixedfalse (預設值),則串聯值的每個收件者都會設定訂用帳戶以接收變更通知。 由於訂用帳戶追蹤的關係,每個 [CascadingParameter]成本都遠高於一般的 [Parameter]
  • 如果 IsFixedtrue (例如,<CascadingValue Value="someValue" IsFixed="true">),則收件者會收到初始值,但不會設定訂用帳戶以接收更新。 每個 [CascadingParameter] 都是輕量型,且成本不會再高於一般的 [Parameter]

如果有大量其他元件會接收串聯值,則將 IsFixed 設定為 true 可改善效能。 請盡可能在串聯值上將 IsFixed 設定為 true。 當所提供的值不會隨時間變更時,便可以將 IsFixed 設定為 true

當元件以串聯值的形式傳遞 this 時,也可以將 IsFixed 設定為 true

<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 可以每秒引發數十次或數百次。 在大部分情況下,您不需要如此頻繁地執行 UI 更新。 如果事件觸發速度太快,則可能會傷害 UI 的回應能力,或耗用過多 CPU 時間。

請考慮使用 JS Interop 來註冊較不會頻繁引發的回呼,而不是使用會快速引發的原生事件。 例如,下列元件會顯示滑鼠的位置,但最多只會每 500 毫秒更新一次:

@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 並提供 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) 會觸發重新轉譯。
  • 選取第二個按鈕 (會呼叫 HandleClick2) 不會觸發重新轉譯。
  • 選取第三個按鈕 (會呼叫 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 在迴圈中重新建立元素或元件的 Lambda 運算式委派會導致效能不佳。

事件處理文章中顯示的下列元件會轉譯一組按鈕。 每個按鈕都會向其 @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 Interop 速度最佳化

.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 Interop 呼叫。 相反地,下列方法會將 JS Interop 減少為單一呼叫:

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 Interop 呼叫整合為單一呼叫通常只會在元件發出大量 JS Interop 呼叫時才會大幅改善效能。

考慮使用同步呼叫

從 .NET 呼叫 JavaScript

本節僅適用於用戶端元件。

不論所呼叫的程式碼是同步還是非同步,JS Interop 呼叫預設都是非同步。 呼叫會預設為以非同步方式進行,以確保元件在伺服器端和用戶端轉譯模式可以相容。 在伺服器上,所有 JS Interop 呼叫都必須以非同步方式進行,因為這些呼叫會透過網路連線來傳送。

如果您確定元件只會在 WebAssembly 上執行,則可以選擇進行同步的 JS Interop 呼叫。 進行同步呼叫的額外負荷會略低於進行非同步呼叫,而且可減少轉譯週期,因為在等候結果時不會有中繼狀態。

若要在用戶端元件中進行從 .NET 到 JavaScript 的同步呼叫,請將 IJSRuntime 轉換成 IJSInProcessRuntime 以進行 JS Interop 呼叫:

@inject IJSRuntime JS

...

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

在 ASP.NET Core 5.0 或更新版本的用戶端元件中使用 IJSObjectReference 時,您可以改為以同步方式使用 IJSInProcessObjectReferenceIJSInProcessObjectReference 會實作 IAsyncDisposable/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 Interop 呼叫預設都是非同步。 呼叫會預設為以非同步方式進行,以確保元件在伺服器端和用戶端轉譯模式可以相容。 在伺服器上,所有 JS Interop 呼叫都必須以非同步方式進行,因為這些呼叫會透過網路連線來傳送。

如果您確定元件只會在 WebAssembly 上執行,則可以選擇進行同步的 JS Interop 呼叫。 進行同步呼叫的額外負荷會略低於進行非同步呼叫,而且可減少轉譯週期,因為在等候結果時不會有中繼狀態。

若要在用戶端元件中從 JavaScript 同步呼叫至 .NET,請使用 DotNet.invokeMethod 而非 DotNet.invokeMethodAsync

如果是以下情形,則同步呼叫可運作:

  • 元件只會在 WebAssembly 上轉譯執行。
  • 呼叫的函式會以同步方式傳回值。 函式不是 async 方法,也不會傳回 .NET Task 或 JavaScript Promise

本節僅適用於用戶端元件。

不論所呼叫的程式碼是同步還是非同步,JS Interop 呼叫預設都是非同步。 呼叫會預設為以非同步方式進行,以確保元件在伺服器端和用戶端轉譯模式可以相容。 在伺服器上,所有 JS Interop 呼叫都必須以非同步方式進行,因為這些呼叫會透過網路連線來傳送。

如果您確定元件只會在 WebAssembly 上執行,則可以選擇進行同步的 JS Interop 呼叫。 進行同步呼叫的額外負荷會略低於進行非同步呼叫,而且可減少轉譯週期,因為在等候結果時不會有中繼狀態。

若要在用戶端元件中進行從 .NET 到 JavaScript 的同步呼叫,請將 IJSRuntime 轉換成 IJSInProcessRuntime 以進行 JS Interop 呼叫:

@inject IJSRuntime JS

...

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

在 ASP.NET Core 5.0 或更新版本的用戶端元件中使用 IJSObjectReference 時,您可以改為以同步方式使用 IJSInProcessObjectReferenceIJSInProcessObjectReference 會實作 IAsyncDisposable/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 Interop 方法額外負荷,但目前並無文件記載要與這些 API 互動所需的 JavaScript API,而且所需的 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] Interop

相較於 .NET 7 中 ASP.NET Core 前的架構版本中的 JS Interop API,Blazor WebAssembly 應用程式的 JavaScript [JSImport]/[JSExport] Interop 所提供的效能和穩定性會有所改善。

如需詳細資訊,請參閱 JavaScript JSImport/JSExport Interop 與 ASP.NET Core Blazor

預先 (AOT) 編譯

預先 (AOT) 編譯會直接將 Blazor 應用程式的 .NET 程式碼編譯成原生 WebAssembly,以供瀏覽器直接執行。 AOT 編譯的應用程式會導致應用程式變得較大,而需要較長的下載時間,但 AOT 編譯的應用程式通常會提供更好的執行階段效能,對於會執行 CPU 密集工作的應用程式來說更是如此。 如需詳細資訊,請參閱 ASP.NET Core Blazor WebAssembly 建置工具和預先 (AOT) 編譯

將應用程式下載大小降到最低

執行階段重新連結

如需有關執行階段重新連結如何將應用程式的下載大小降到最低的資訊,請參閱 ASP.NET Core Blazor WebAssembly 建置工具和預先 (AOT) 編譯

使用System.Text.Json

Blazor 的 JS Interop 實作會依賴 System.Text.Json,這是高效能的 JSON 序列化程式庫,所配置的記憶體很少。 相較於新增一或多個替代 JSON 程式庫,使用 System.Text.Json 應該不會導致額外的應用程式承載大小。

如需移轉指導,請參閱如何從 Newtonsoft.Json 移轉至 System.Text.Json

中繼語言 (IL) 修剪

本節僅適用於 Blazor WebAssembly 應用程式。

從 Blazor WebAssembly 應用程式修剪未使用的組件會藉由移除應用程式二進位檔中未使用的程式碼來減少應用程式的大小。 如需詳細資訊,請參閱設定 ASP.NET Core Blazor 的修剪器

連結 Blazor WebAssembly 應用程式會藉由修剪應用程式二進位檔中未使用的程式碼來減少應用程式的大小。 根據預設,中繼語言 (IL) 連結器只會在建置於 Release 組態時啟用。 若要從中受益,請使用 dotnet publish 命令並將 -c|--configuration 選項設定為 Release 來發佈應用程式以進行部署:

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>
    
  • 根據預設,Blazor WebAssembly 會攜帶要在使用者的文化特性中顯示日期和貨幣等值所需的全球化資源。 如果應用程式不需要當地語系化,您可以將應用程式設定為支援不因文化特性而異,其會以 en-US 文化特性作為基礎。