najlepsze rozwiązania dotyczące wydajności ASP.NET Core Blazor

Uwaga

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Ważne

Te informacje odnoszą się do produktu w wersji wstępnej, który może zostać znacząco zmodyfikowany, zanim zostanie wydany komercyjnie. Firma Microsoft nie udziela żadnych gwarancji, jawnych lub domniemanych, w odniesieniu do informacji podanych w tym miejscu.

Aby zapoznać się z bieżącą wersją, zapoznaj się z wersją tego artykułu platformy .NET 8.

Blazor jest zoptymalizowany pod kątem wysokiej wydajności w najbardziej realistycznych scenariuszach interfejsu użytkownika aplikacji. Jednak najlepsza wydajność zależy od deweloperów przyjmujących prawidłowe wzorce i funkcje.

Uwaga

Przykłady kodu w tym artykule przyjmują typy odwołań dopuszczających wartość null (NRTs) i statyczną analizę stanu null kompilatora platformy .NET, które są obsługiwane w programie ASP.NET Core na platformie .NET 6 lub nowszym.

Optymalizowanie szybkości renderowania

Zoptymalizuj szybkość renderowania, aby zminimalizować obciążenie renderowania i poprawić czas odpowiedzi interfejsu użytkownika, co może spowodować dziesięciokrotnie większą poprawę szybkości renderowania interfejsu użytkownika.

Unikaj niepotrzebnego renderowania poddrzew składników

W przypadku wystąpienia zdarzenia może być możliwe usunięcie większości kosztów renderowania składnika nadrzędnego, pomijając rerendering poddrzew składników podrzędnych. Należy martwić się tylko o pomijanie poddrzew rerendering, które są szczególnie kosztowne do renderowania i powodują opóźnienie interfejsu użytkownika.

W czasie wykonywania składniki istnieją w hierarchii. Składnik główny (pierwszy załadowany składnik) zawiera składniki podrzędne. Z kolei elementy podrzędne katalogu głównego mają własne składniki podrzędne i tak dalej. W przypadku wystąpienia zdarzenia, takiego jak wybranie przycisku przez użytkownika, następujący proces określa, które składniki mają być rerender:

  1. Zdarzenie jest wysyłane do składnika, który renderował program obsługi zdarzenia. Po wykonaniu procedury obsługi zdarzeń składnik jest rerendered.
  2. Gdy składnik jest rerendered, dostarcza nową kopię wartości parametrów do każdego z jego składników podrzędnych.
  3. Po odebraniu nowego zestawu wartości parametrów każdy składnik decyduje, czy rerender. Domyślnie składniki rerender, jeśli wartości parametrów mogły ulec zmianie, na przykład jeśli są modyfikowalne obiekty.

Ostatnie dwa kroki poprzedniej sekwencji są kontynuowane rekursywnie w dół hierarchii składników. W wielu przypadkach całe poddrzewo jest rerendered. Zdarzenia ukierunkowane na składniki wysokiego poziomu mogą powodować kosztowne rerendering, ponieważ każdy składnik poniżej składnika wysokiego poziomu musi rerender.

Aby zapobiec rekursji renderowania do określonego poddrzewa, użyj jednej z następujących metod:

  • Upewnij się, że parametry składnika podrzędnego są typami pierwotnymi niezmiennymi, takimi jak string, int, bool, , DateTimei inne podobne typy. Wbudowana logika wykrywania zmian automatycznie pomija rerendering, jeśli pierwotne niezmienne wartości parametrów nie uległy zmianie. Jeśli renderujesz składnik podrzędny za pomocą <Customer CustomerId="@item.CustomerId" />polecenia , gdzie CustomerId jest typem int , Customer składnik nie jest rerendered, chyba że item.CustomerId ulegnie zmianie.
  • Zastąpij ShouldRender:
    • Aby zaakceptować wartości parametrów innych niżprimitive, takie jak złożone niestandardowe typy modeli, wywołania zwrotne zdarzeń lub RenderFragment wartości.
    • Jeśli tworzenie składnika tylko interfejsu użytkownika, który nie zmienia się po początkowym renderowaniu, niezależnie od zmian wartości parametru.

Poniższy przykład narzędzia wyszukiwania lotów linii lotniczych używa pól prywatnych do śledzenia niezbędnych informacji w celu wykrywania zmian. Poprzedni identyfikator lotu przychodzącego () i poprzedni identyfikator lotu wychodzącego (prevInboundFlightIdprevOutboundFlightId) śledzą informacje dotyczące następnej potencjalnej aktualizacji składnika. Jeśli jeden z identyfikatorów lotu zmieni się, gdy parametry składnika są ustawione w OnParametersSetelemencie , składnik jest rerendered, ponieważ shouldRender jest ustawiony na truewartość . Jeśli shouldRender ocena zostanie false obliczona po sprawdzeniu identyfikatorów lotu, unika się kosztownego rerendera:

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

Program obsługi zdarzeń może również ustawić wartość shouldRendertrue. W przypadku większości składników określenie rerenderingu na poziomie poszczególnych procedur obsługi zdarzeń zwykle nie jest konieczne.

Aby uzyskać więcej informacji, zobacz następujące zasoby:

Wirtualizacja

Podczas renderowania dużych ilości interfejsu użytkownika w pętli, na przykład listy lub siatki z tysiącami wpisów, sama ilość operacji renderowania może prowadzić do opóźnienia renderowania interfejsu użytkownika. Biorąc pod uwagę, że użytkownik może zobaczyć tylko niewielką liczbę elementów jednocześnie bez przewijania, często marnujące jest poświęcanie czasu na renderowanie elementów, które nie są obecnie widoczne.

BlazorVirtualize<TItem> Udostępnia składnik umożliwiający tworzenie zachowań wyglądu i przewijania dowolnej dużej listy podczas renderowania tylko elementów listy znajdujących się w bieżącym okienku widoku przewijania. Na przykład składnik może renderować listę z 100 000 wpisów, ale płaci tylko koszt renderowania 20 elementów, które są widoczne.

Aby uzyskać więcej informacji, zobacz ASP.NET Core component virtualization (Wirtualizacja składników podstawowego systemu ASP.NET CoreRazor).

Tworzenie lekkich, zoptymalizowanych składników

Większość składników nie wymaga agresywnych działań optymalizacji, ponieważ większość Razor składników nie powtarza się w interfejsie użytkownika i nie rerender z wysoką częstotliwością. Na przykład składniki routingu z dyrektywą @page i składnikami używanymi do renderowania elementów wysokiego poziomu interfejsu użytkownika, takich jak okna dialogowe lub formularze, najprawdopodobniej pojawiają się tylko jeden naraz i tylko rerender w odpowiedzi na gest użytkownika. Te składniki zwykle nie tworzą dużego obciążenia renderowania, więc można swobodnie używać dowolnej kombinacji funkcji platformy bez obaw o wydajność renderowania.

Istnieją jednak typowe scenariusze, w których składniki są powtarzane na dużą skalę i często powodują niską wydajność interfejsu użytkownika:

  • Duże zagnieżdżone formularze z setkami poszczególnych elementów, takich jak dane wejściowe lub etykiety.
  • Siatki z setkami wierszy lub tysiącami komórek.
  • Wykresy punktowe z milionami punktów danych.

Jeśli modelowanie każdego elementu, komórki lub punktu danych jako oddzielne wystąpienie składnika, często jest tak wiele z nich, że ich wydajność renderowania staje się krytyczna. Ta sekcja zawiera porady dotyczące tworzenia takich składników lekkich, aby interfejs użytkownika pozostał szybki i dynamiczny.

Unikaj tysięcy wystąpień składników

Każdy składnik jest oddzielną wyspą, która może renderować niezależnie od rodziców i dzieci. Wybierając sposób dzielenia interfejsu użytkownika na hierarchię składników, przejmujesz kontrolę nad szczegółowością renderowania interfejsu użytkownika. Może to spowodować dobrą lub słabą wydajność.

Dzieląc interfejs użytkownika na oddzielne składniki, możesz mieć mniejsze części rerender interfejsu użytkownika w przypadku wystąpienia zdarzeń. W tabeli z wieloma wierszami, które mają przycisk w każdym wierszu, można mieć tylko ten pojedynczy rerender wierszy przy użyciu składnika podrzędnego zamiast całej strony lub tabeli. Jednak każdy składnik wymaga dodatkowego obciążenia pamięci i procesora CPU, aby poradzić sobie z niezależnym stanem i cyklem życia renderowania.

W teście przeprowadzonym przez inżynierów jednostek produktu ASP.NET Core w aplikacji było widoczne obciążenie związane z renderowaniem Blazor WebAssembly wynoszącym około 0,06 ms na wystąpienie składnika. Aplikacja testowa renderowała prosty składnik, który akceptuje trzy parametry. Wewnętrznie obciążenie jest w dużej mierze spowodowane pobieraniem stanu poszczególnych składników z słowników i przekazywaniem i odbieraniem parametrów. Przez mnożenie widać, że dodanie 2000 dodatkowych wystąpień składników spowoduje dodanie 0,12 sekund do czasu renderowania, a interfejs użytkownika zacznie odczuwać powolne działanie dla użytkowników.

Możliwe jest, aby składniki były bardziej lekkie, dzięki czemu można mieć więcej z nich. Jednak bardziej zaawansowaną techniką jest często unikanie renderowania tak wielu składników. W poniższych sekcjach opisano dwa podejścia, które można wykonać.

Aby uzyskać więcej informacji na temat zarządzania pamięcią, zobacz Hostowanie i wdrażanie aplikacji po stronie Blazor serwera ASP.NET Core.

Wbudowane składniki podrzędne do ich elementów nadrzędnych

Rozważmy następującą część składnika nadrzędnego, który renderuje składniki podrzędne w pętli:

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

Powyższy przykład sprawdza się dobrze, jeśli tysiące komunikatów nie jest wyświetlanych jednocześnie. Aby pokazać tysiące komunikatów jednocześnie, rozważ nie uwzględnianie oddzielnego ChatMessageDisplay składnika. Zamiast tego wstaw składnik podrzędny do elementu nadrzędnego. Poniższe podejście pozwala uniknąć narzutu na składnik renderowania tak wielu składników podrzędnych kosztem utraty możliwości samodzielnego adiustacji każdego składnika podrzędnego:

<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>
Definiowanie wielokrotnego użytku RenderFragments w kodzie

Składniki podrzędne mogą być uwzględniane wyłącznie jako sposób ponownego korzystania z logiki renderowania. Jeśli tak jest, możesz utworzyć logikę renderowania wielokrotnego użytku bez implementowania dodatkowych składników. W bloku dowolnego składnika @code zdefiniuj element RenderFragment. Renderuj fragment z dowolnej lokalizacji dowolną liczbę razy w razie potrzeby:

@RenderWelcomeInfo

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

@RenderWelcomeInfo

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

Aby kod RenderTreeBuilder był wielokrotnego użytku w wielu składnikach, zadeklaruj element RenderFragmentpublic i static:

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

SayHello w poprzednim przykładzie można wywołać z niepowiązanego składnika. Ta technika jest przydatna w przypadku tworzenia bibliotek fragmentów znaczników wielokrotnego użytku, które są renderowane bez narzutu na składnik.

RenderFragment delegaci mogą akceptować parametry. Następujący składnik przekazuje komunikat (message) do delegata 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>;
}

Powyższe podejście ponownie używa logiki renderowania bez obciążenia poszczególnych składników. Jednak podejście nie zezwala na odświeżanie poddrzewa interfejsu użytkownika niezależnie, ani nie ma możliwości pomijania renderowania poddrzewa interfejsu użytkownika, gdy element nadrzędny renderuje, ponieważ nie ma granicy składników. Przypisanie do delegata RenderFragment jest obsługiwane tylko w Razor plikach składników (.razor), a wywołania zwrotne zdarzeń nie są obsługiwane.

W przypadku pola, metody lub właściwości, do którego nie można odwoływać się przez inicjatora pola, takiego jak TitleTemplate w poniższym przykładzie, należy użyć właściwości zamiast pola dla RenderFragmentelementu :

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

Nie odbieraj zbyt wielu parametrów

Jeśli składnik powtarza się bardzo często, na przykład setki lub tysiące razy, obciążenie związane z przekazywaniem i odbieraniem każdego parametru jest kompilowanie.

Rzadko zdarza się, że zbyt wiele parametrów poważnie ogranicza wydajność, ale może być czynnikiem. TableCell W przypadku składnika renderowanego 4000 razy w siatce każdy parametr przekazany do składnika dodaje około 15 ms do całkowitego kosztu renderowania. Przekazywanie dziesięciu parametrów wymaga około 150 ms i powoduje opóźnienie renderowania interfejsu użytkownika.

Aby zmniejszyć obciążenie parametrów, należy powiązać wiele parametrów w klasie niestandardowej. Na przykład składnik komórki tabeli może zaakceptować wspólny obiekt. W poniższym przykładzie Data jest inna dla każdej komórki, ale Options jest powszechna we wszystkich wystąpieniach komórek:

@typeparam TItem

...

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

Należy jednak wziąć pod uwagę, że może to być poprawa, aby nie mieć składnika komórki tabeli, jak pokazano w poprzednim przykładzie, i zamiast tego w tekście jej logiki do składnika nadrzędnego.

Uwaga

Jeśli dostępnych jest wiele podejść do poprawy wydajności, testowanie porównawcze podejścia jest zwykle wymagane do określenia, które podejście daje najlepsze wyniki.

Aby uzyskać więcej informacji na temat ogólnych parametrów typu (@typeparam), zobacz następujące zasoby:

Upewnij się, że parametry kaskadowe są stałe

Składnik CascadingValue ma opcjonalny IsFixed parametr:

  • Jeśli IsFixed wartość to false (wartość domyślna), każdy odbiorca wartości kaskadowej konfiguruje subskrypcję w celu otrzymywania powiadomień o zmianie. Każdy [CascadingParameter] z nich jest znacznie droższy niż zwykły [Parameter] ze względu na śledzenie subskrypcji.
  • Jeśli IsFixed wartość to true (na przykład ), adresaci otrzymują wartość początkową, <CascadingValue Value="someValue" IsFixed="true">ale nie konfigurują subskrypcji w celu otrzymywania aktualizacji. Każdy z nich [CascadingParameter] jest lekki i nie droższy niż zwykły [Parameter].

Ustawienie IsFixed w celu true zwiększenia wydajności, jeśli istnieje duża liczba innych składników, które otrzymują wartość kaskadową. Wszędzie tam, gdzie to możliwe, ustaw wartość IsFixedtrue na wartości kaskadowe. Można ustawić IsFixed wartość na true , gdy podana wartość nie zmienia się wraz z upływem czasu.

Jeśli składnik przekazuje this jako wartość kaskadową, IsFixed można również ustawić wartość :true

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

Aby uzyskać więcej informacji, zobacz ASP.NET Core Blazor kaskadowych wartości i parametrów.

Unikaj rozplatania atrybutów za pomocą polecenia CaptureUnmatchedValues

Składniki mogą wybierać odbieranie wartości parametrów "niedopasowanych" przy użyciu flagi CaptureUnmatchedValues :

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

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

Takie podejście umożliwia przekazywanie dowolnych dodatkowych atrybutów do elementu. Jednak takie podejście jest kosztowne, ponieważ moduł renderowany musi:

  • Dopasuj wszystkie podane parametry do zestawu znanych parametrów, aby utworzyć słownik.
  • Śledź, jak wiele kopii tego samego atrybutu zastępuje się nawzajem.

Użyj CaptureUnmatchedValues miejsca, w którym wydajność renderowania składników nie jest krytyczna, na przykład składniki, które nie są często powtarzane. W przypadku składników renderowanych na dużą skalę, takich jak każdy element na dużej liście lub w komórkach siatki, spróbuj uniknąć rozplatania atrybutów.

Aby uzyskać więcej informacji, zobacz ASP.NET Core Blazor atrybutówplatting i dowolnych parametrów.

Implementowanie SetParametersAsync ręcznie

Znaczącym źródłem obciążenia związanego z renderowaniem poszczególnych składników jest zapisywanie przychodzących wartości parametrów do [Parameter] właściwości. Moduł renderowania używa odbicia w celu zapisania wartości parametrów, co może prowadzić do niskiej wydajności na dużą skalę.

W niektórych skrajnych przypadkach możesz uniknąć odbicia i ręcznie zaimplementować własną logikę ustawiania parametrów. Może to mieć zastosowanie w przypadku:

  • Składnik jest renderowany bardzo często, na przykład w przypadku setek lub tysięcy kopii składnika w interfejsie użytkownika.
  • Składnik akceptuje wiele parametrów.
  • Okaże się, że obciążenie związane z odbieraniem parametrów ma zauważalny wpływ na czas odpowiedzi interfejsu użytkownika.

W skrajnych przypadkach można zastąpić metodę wirtualną SetParametersAsync składnika i zaimplementować własną logikę specyficzną dla składnika. Poniższy przykład celowo unika wyszukiwania słowników:

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

W poprzednim kodzie zwracanie klasy SetParametersAsync bazowej uruchamia normalną metodę cyklu życia bez ponownego przypisywania parametrów.

Jak widać w poprzednim kodzie, zastępowanie SetParametersAsync i dostarczanie logiki niestandardowej jest skomplikowane i pracochłonne, więc ogólnie nie zalecamy przyjęcia tego podejścia. W skrajnych przypadkach może zwiększyć wydajność renderowania o 20–25%, ale należy rozważyć to podejście tylko w skrajnych scenariuszach wymienionych wcześniej w tej sekcji.

Nie wyzwalaj zbyt szybko zdarzeń

Niektóre zdarzenia przeglądarki są uruchamiane bardzo często. Na przykład onmousemove i onscroll może uruchamiać dziesiątki lub setki razy na sekundę. W większości przypadków nie trzeba często wykonywać aktualizacji interfejsu użytkownika. Jeśli zdarzenia są wyzwalane zbyt szybko, możesz zaszkodzić czasowi reakcji interfejsu użytkownika lub zużyć nadmierny czas procesora CPU.

Zamiast używać natywnych zdarzeń, które szybko uruchamiają się, rozważ użycie międzyoperacyjności JS w celu zarejestrowania wywołania zwrotnego, które jest uruchamiane rzadziej. Na przykład następujący składnik wyświetla położenie myszy, ale aktualizuje tylko raz co 500 ms:

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

Odpowiedni kod JavaScript rejestruje odbiornik zdarzeń DOM na potrzeby przenoszenia myszy. W tym przykładzie odbiornik zdarzeń używa funkcji Lodash throttle w celu ograniczenia szybkości wywołań:

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

Unikaj rerenderingu po obsłudze zdarzeń bez zmian stanu

Domyślnie składniki dziedziczą z ComponentBaseprogramu , który automatycznie wywołuje się po wywołaniu StateHasChanged programów obsługi zdarzeń składnika. W niektórych przypadkach może to być niepotrzebne lub niepożądane, aby wyzwolić rerender po wywołaniu programu obsługi zdarzeń. Na przykład program obsługi zdarzeń może nie modyfikować stanu składnika. W tych scenariuszach aplikacja może korzystać z interfejsu IHandleEvent w celu kontrolowania zachowania obsługi zdarzeń Blazor.

Aby zapobiec rerenders dla wszystkich programów obsługi zdarzeń składnika, zaimplementuj IHandleEvent i podaj zadanie, które wywołuje procedurę IHandleEvent.HandleEventAsync obsługi zdarzeń bez wywoływania metody StateHasChanged.

W poniższym przykładzie żadna procedura obsługi zdarzeń nie jest dodawana do składnika wyzwala rerender, więc HandleSelect nie powoduje wywołania elementu rerender.

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

Oprócz zapobiegania rerenders po uruchomieniu procedur obsługi zdarzeń w składniku w sposób globalny można zapobiec rerenders po jednym procedurze obsługi zdarzeń, stosując następującą metodę narzędzia.

Dodaj następującą EventUtil klasę Blazor do aplikacji. Akcje statyczne i funkcje w górnej części EventUtil klasy udostępniają programy obsługi obejmujące kilka kombinacji argumentów i zwracanych typów używanych Blazor podczas obsługi zdarzeń.

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

Wywołaj wywołanie EventUtil.AsNonRenderingEventHandler procedury obsługi zdarzeń, która nie wyzwala renderowania podczas wywoływania.

W poniższym przykładzie:

  • Wybranie pierwszego przycisku, który wywołuje HandleClick1metodę , wyzwala rerender.
  • Wybranie drugiego przycisku, który wywołuje HandleClick2metodę , nie powoduje wyzwolenia elementu rerender.
  • Wybranie trzeciego przycisku, który wywołuje HandleClick3metodę , nie wyzwala elementu rerender i używa argumentów zdarzeń (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);
    }
}

Oprócz implementacji interfejsu IHandleEvent wykorzystanie innych najlepszych rozwiązań opisanych w tym artykule może również pomóc zmniejszyć niepożądane renderowanie po obsłużeniu zdarzeń. Na przykład przesłonięcia ShouldRender w składnikach podrzędnych składnika docelowego można użyć do kontrolowania rerenderingu.

Unikaj ponownego tworzenia delegatów dla wielu powtarzających się elementów lub składników

BlazorOdtwarzanie delegatów wyrażeń lambda dla elementów lub składników w pętli może prowadzić do niskiej wydajności.

Poniższy składnik pokazany w artykule dotyczącym obsługi zdarzeń renderuje zestaw przycisków. Każdy przycisk przypisuje delegata do zdarzenia @onclick , co jest w porządku, jeśli nie ma wielu przycisków do renderowania.

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

W przypadku renderowania dużej liczby przycisków przy użyciu powyższego podejścia szybkość renderowania ma negatywny wpływ, co prowadzi do złego środowiska użytkownika. Aby renderować dużą liczbę przycisków z wywołaniem zwrotnym dla zdarzeń kliknięcia, w poniższym przykładzie użyto kolekcji obiektów przycisków, które przypisują delegata @onclick każdego przycisku do elementu Action. Następujące podejście nie wymaga Blazor ponownego kompilowania wszystkich delegatów przycisku za każdym razem, gdy przyciski są renderowane:

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

Optymalizowanie szybkości interopcji języka JavaScript

Wywołania między platformami .NET i JavaScript wymagają dodatkowych obciążeń, ponieważ:

  • Domyślnie wywołania są asynchroniczne.
  • Domyślnie parametry i wartości zwracane są JSserializowane w środowisku ON, aby zapewnić łatwy do zrozumienia mechanizm konwersji między typami .NET i JavaScript.

Ponadto w przypadku aplikacji po stronie Blazor serwera te wywołania są przekazywane przez sieć.

Unikaj nadmiernie precyzyjnych wywołań

Ponieważ każde wywołanie wiąże się z pewnym obciążeniem, warto zmniejszyć liczbę wywołań. Rozważmy następujący kod, który przechowuje kolekcję elementów w przeglądarce localStorage:

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

W poprzednim przykładzie jest oddzielne JS wywołanie międzyoperajności dla każdego elementu. Zamiast tego następujące podejście zmniejsza JS międzyoperajności do jednego wywołania:

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

Odpowiadająca mu funkcja JavaScript przechowuje całą kolekcję elementów na kliencie:

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

W przypadku Blazor WebAssembly aplikacji stopniowe pojedyncze JS wywołania międzyoperacyjne w jednym wywołaniu zwykle znacznie poprawia wydajność tylko wtedy, gdy składnik wykonuje dużą liczbę wywołań międzyoperacyjnych JS .

Rozważ użycie wywołań synchronicznych

Wywoływanie kodu JavaScript z platformy .NET

Ta sekcja dotyczy tylko składników po stronie klienta.

Wywołania współdziałania języka JS są domyślnie asynchroniczne niezależnie od tego, czy wywoływany kod jest synchroniczny, czy asynchroniczny. Wywołania są domyślnie asynchroniczne, aby upewnić się, że składniki są zgodne z trybami renderowania po stronie serwera i po stronie klienta. Na serwerze wszystkie JS wywołania międzyoperacyjne muszą być asynchroniczne, ponieważ są wysyłane za pośrednictwem połączenia sieciowego.

Jeśli wiesz, że składnik działa tylko w zestawie WebAssembly, możesz wykonać synchroniczne wywołania międzyoperacyjne JS . Ma to nieco mniejsze obciążenie niż wykonywanie wywołań asynchronicznych i może spowodować zmniejszenie liczby cykli renderowania, ponieważ nie ma stanu pośredniego podczas oczekiwania na wyniki.

Aby wykonać synchroniczne wywołanie z platformy .NET do języka JavaScript w składniku po stronie klienta, rzutuj IJSRuntime w celu IJSInProcessRuntime wywołania międzyoperacyjności JS :

@inject IJSRuntime JS

...

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

Podczas pracy ze składnikami IJSObjectReference ASP.NET Core 5.0 lub nowszymi po stronie klienta można użyć IJSInProcessObjectReference synchronicznie. IJSInProcessObjectReference implementuje IAsyncDisposable/IDisposable i należy usunąć odzyskiwanie pamięci, aby zapobiec wyciekowi pamięci, jak pokazano w poniższym przykładzie:

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

Wywoływanie platformy .NET ze środowiska JavaScript

Ta sekcja dotyczy tylko składników po stronie klienta.

Wywołania współdziałania języka JS są domyślnie asynchroniczne niezależnie od tego, czy wywoływany kod jest synchroniczny, czy asynchroniczny. Wywołania są domyślnie asynchroniczne, aby upewnić się, że składniki są zgodne z trybami renderowania po stronie serwera i po stronie klienta. Na serwerze wszystkie JS wywołania międzyoperacyjne muszą być asynchroniczne, ponieważ są wysyłane za pośrednictwem połączenia sieciowego.

Jeśli wiesz, że składnik działa tylko w zestawie WebAssembly, możesz wykonać synchroniczne wywołania międzyoperacyjne JS . Ma to nieco mniejsze obciążenie niż wykonywanie wywołań asynchronicznych i może spowodować zmniejszenie liczby cykli renderowania, ponieważ nie ma stanu pośredniego podczas oczekiwania na wyniki.

Aby wykonać wywołanie synchroniczne z języka JavaScript do platformy .NET w składniku po stronie klienta, użyj polecenia DotNet.invokeMethod zamiast DotNet.invokeMethodAsync.

Wywołania synchroniczne działają, jeśli:

  • Składnik jest renderowany tylko do wykonania w zestawie WebAssembly.
  • Wywołana funkcja zwraca wartość synchronicznie. Funkcja nie async jest metodą i nie zwraca platformy .NET Task ani języka JavaScript Promise.

Ta sekcja dotyczy tylko składników po stronie klienta.

Wywołania współdziałania języka JS są domyślnie asynchroniczne niezależnie od tego, czy wywoływany kod jest synchroniczny, czy asynchroniczny. Wywołania są domyślnie asynchroniczne, aby upewnić się, że składniki są zgodne z trybami renderowania po stronie serwera i po stronie klienta. Na serwerze wszystkie JS wywołania międzyoperacyjne muszą być asynchroniczne, ponieważ są wysyłane za pośrednictwem połączenia sieciowego.

Jeśli wiesz, że składnik działa tylko w zestawie WebAssembly, możesz wykonać synchroniczne wywołania międzyoperacyjne JS . Ma to nieco mniejsze obciążenie niż wykonywanie wywołań asynchronicznych i może spowodować zmniejszenie liczby cykli renderowania, ponieważ nie ma stanu pośredniego podczas oczekiwania na wyniki.

Aby wykonać synchroniczne wywołanie z platformy .NET do języka JavaScript w składniku po stronie klienta, rzutuj IJSRuntime w celu IJSInProcessRuntime wywołania międzyoperacyjności JS :

@inject IJSRuntime JS

...

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

Podczas pracy ze składnikami IJSObjectReference ASP.NET Core 5.0 lub nowszymi po stronie klienta można użyć IJSInProcessObjectReference synchronicznie. IJSInProcessObjectReference implementuje IAsyncDisposable/IDisposable i należy usunąć odzyskiwanie pamięci, aby zapobiec wyciekowi pamięci, jak pokazano w poniższym przykładzie:

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

Rozważ użycie niezamężnych połączeń

Ta sekcja dotyczy Blazor WebAssembly tylko aplikacji.

W przypadku uruchamiania w systemie Blazor WebAssemblymożna wykonać niezamężne wywołania z platformy .NET do języka JavaScript. Są to synchroniczne wywołania, które nie wykonują JSserializacji argumentów ani zwracanych wartości. Wszystkie aspekty zarządzania pamięcią i tłumaczenia między reprezentacjami platformy .NET i języka JavaScript są pozostawione deweloperowi.

Ostrzeżenie

Chociaż użycie IJSUnmarshalledRuntime ma najmniejsze obciążenie związane z JS podejściami międzyoperacyjnymi, interfejsy API języka JavaScript wymagane do interakcji z tymi interfejsami API są obecnie nieudokumentowane i podlegają zmianom powodującym niezgodność w przyszłych wersjach.

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

Korzystanie z międzyoperacji języka JavaScript [JSImport]/[JSExport]

Współdziałanie języka JavaScript [JSImport]/[JSExport] dla Blazor WebAssembly aplikacji zapewnia lepszą wydajność i stabilność interfejsu API międzyoperacyjnego JS w wersjach platformy przed ASP.NET Core na platformie .NET 7.

Aby uzyskać więcej informacji, zobacz JavaScript Import/Export interop with ASP.NET Core (Importowanie/JSeksportowanie międzyoperacji języka JavaScript JSza pomocą programu ASP.NET CoreBlazor).

Kompilacja Z wyprzedzeniem (AOT)

Kompilacja Z wyprzedzeniem (AOT) kompiluje Blazor kod platformy .NET aplikacji bezpośrednio do natywnego zestawu WebAssembly na potrzeby bezpośredniego wykonywania przez przeglądarkę. Aplikacje skompilowane za pomocą funkcji AOT powodują, że pobieranie większych aplikacji trwa dłużej, ale aplikacje skompilowane za pomocą funkcji AOT zwykle zapewniają lepszą wydajność środowiska uruchomieniowego, zwłaszcza w przypadku aplikacji wykonujących zadania intensywnie korzystające z procesora CPU. Aby uzyskać więcej informacji, zobacz ASP.NET Core build tools and ahead-of-time (AOT) build tools and ahead-of-time compilation (AOT) (Narzędzia kompilacji ASP.NET Core Blazor WebAssembly i kompilacja przed czasem (AOT).

Minimalizowanie rozmiaru pobierania aplikacji

Ponowne łączenie środowiska uruchomieniowego

Aby uzyskać informacje o tym, jak ponowne łączenie środowiska uruchomieniowego minimalizuje rozmiar pobierania aplikacji, zobacz ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) build tools and ahead-of-time (AOT) build tools (Przed czasem kompilacji).

Korzystanie z polecenia System.Text.Json

BlazorImplementacja międzyoperamentowa JS opiera się na systemie System.Text.Json, który jest wysoce wydajnym JSbiblioteką serializacji ON z małą alokacją pamięci. Użycie System.Text.Json metody nie powinno spowodować dodatkowego rozmiaru ładunku aplikacji przez dodanie co najmniej jednej alternatywnej JSbiblioteki ON.

Aby uzyskać wskazówki dotyczące migracji, zobacz How to migrate from Newtonsoft.Json to System.Text.Json.

Przycinanie języka pośredniego (IL)

Ta sekcja dotyczy Blazor WebAssembly tylko aplikacji.

Przycinanie nieużywanych zestawów z Blazor WebAssembly aplikacji zmniejsza rozmiar aplikacji przez usunięcie nieużywanego kodu w plikach binarnych aplikacji. Aby uzyskać więcej informacji, zobacz Configure the Trimmer for ASP.NET Core (Konfigurowanie programu Trimmer dla platformy ASP.NET Core Blazor).

Blazor WebAssembly Łączenie aplikacji zmniejsza rozmiar aplikacji przez przycinanie nieużywanego kodu w plikach binarnych aplikacji. Domyślnie konsolidator języka pośredniego (IL) jest włączony tylko podczas kompilowania w Release konfiguracji. Aby skorzystać z tego, opublikuj aplikację do wdrożenia przy użyciu dotnet publish polecenia z opcją -c|--configuration ustawioną na :Release

dotnet publish -c Release

Zestawy ładowania z opóźnieniem

Ta sekcja dotyczy Blazor WebAssembly tylko aplikacji.

Ładowanie zestawów w czasie wykonywania, gdy zestawy są wymagane przez trasę. Aby uzyskać więcej informacji, zobacz Ładowanie zestawów z opóźnieniem w programie ASP.NET Core Blazor WebAssembly.

Kompresja

Ta sekcja dotyczy Blazor WebAssembly tylko aplikacji.

Po opublikowaniu Blazor WebAssembly aplikacji dane wyjściowe są statycznie kompresowane podczas publikowania, aby zmniejszyć rozmiar aplikacji i usunąć obciążenie związane z kompresją środowiska uruchomieniowego. Blazor korzysta z serwera w celu negocjowania zawartości i obsługi statycznie skompresowanych plików.

Po wdrożeniu aplikacji sprawdź, czy aplikacja obsługuje skompresowane pliki. Sprawdź kartę Sieć w narzędziach deweloperskich przeglądarki i sprawdź, czy pliki są obsługiwane Content-Encoding: br (kompresja Brotli) lub Content-Encoding: gz (kompresja Gzip). Jeśli host nie obsługuje skompresowanych plików, postępuj zgodnie z instrukcjami w temacie Host i wdróż ASP.NET Core Blazor WebAssembly.

Wyłączanie nieużywanych funkcji

Ta sekcja dotyczy Blazor WebAssembly tylko aplikacji.

Blazor WebAssemblyŚrodowisko uruchomieniowe platformy .NET obejmuje następujące funkcje platformy .NET, które można wyłączyć dla mniejszego rozmiaru ładunku:

  • Plik danych jest dołączany w celu poprawienia informacji o strefie czasowej. Jeśli aplikacja nie wymaga tej funkcji, rozważ jej wyłączenie, ustawiając BlazorEnableTimeZoneSupport właściwość MSBuild w pliku projektu aplikacji na :false

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • Informacje dotyczące sortowania są uwzględniane w celu poprawnego działania interfejsów StringComparison.InvariantCultureIgnoreCase API. Jeśli masz pewność, że aplikacja nie wymaga danych sortowania, rozważ jej wyłączenie, ustawiając BlazorWebAssemblyPreserveCollationData właściwość MSBuild w pliku projektu aplikacji na :false

    <PropertyGroup>
      <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
    </PropertyGroup>
    
  • Domyślnie Blazor WebAssembly w kulturze użytkownika są przenoszone zasoby globalizacji wymagane do wyświetlania wartości, takich jak daty i waluta. Jeśli aplikacja nie wymaga lokalizacji, możesz skonfigurować aplikację tak, aby obsługiwała niezmienną kulturę opartą en-US na kulturze.