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

Uwaga / Notatka

Nie jest to najnowsza wersja tego artykułu. Aby zapoznać się z aktualną wersją, zobacz artykuł w wersji .NET 10.

Ostrzeżenie

Ta wersja ASP.NET Core nie jest już obsługiwana. Aby uzyskać więcej informacji, zobacz zasady pomocy technicznej platformy .NET i platformy .NET Core. Aby zapoznać się z aktualną wersją, zobacz artykuł w wersji .NET 10.

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 komponentów

Można usunąć większość kosztów renderowania składnika nadrzędnego, pomijając ponowne renderowanie poddrzew składników podrzędnych w przypadku wystąpienia zdarzenia. 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 pracy składniki istnieją w hierarchii. Składnik główny (pierwszy załadowany składnik) zawiera składniki podrzędne. Z kolei elementy podrzędne korzenia mają własne komponenty 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 należy ponownie przerysować.

  1. Zdarzenie jest wysyłane do składnika, który renderował program obsługi zdarzenia. Po wykonaniu obsługi zdarzenia składnik jest ponownie renderowany.
  2. Gdy komponent jest ponownie renderowany, dostarcza nową kopię wartości parametrów do każdego z jego komponentów podrzędnych.
  3. Po otrzymaniu nowego zestawu wartości parametrów, Blazor decyduje, czy ponownie renderować komponent. Składniki przerysowują się, jeśli ShouldRender zwraca true, co jest zachowaniem domyślnym, chyba że zostanie ono nadpisane, a wartości parametrów mogły ulec zmianie, na przykład, jeśli są zmiennymi obiektami.

Ostatnie dwa kroki poprzedniej sekwencji są kontynuowane rekursywnie w dół hierarchii składników. Całe poddrzewo jest często przekreślone w wielu przypadkach. Zdarzenia ukierunkowane na składniki wysokiego poziomu mogą powodować kosztowne ponowne renderowanie, ponieważ każdy składnik poniżej składnika wysokiego poziomu musi być ponownie renderowany.

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

  • Upewnij się, że parametry składnika podrzędnego mają określone typy niezmienne†, takie jak string, int, booli DateTime. Wbudowana logika wykrywania zmian automatycznie pomija rerendering, jeśli niezmienne wartości parametrów nie uległy zmianie. Jeśli renderujesz składnik podrzędny za pomocą <Customer CustomerId="item.CustomerId" />, gdzie CustomerId jest typem int, składnik Customer nie jest ponownie renderowany, chyba że item.CustomerId ulegnie zmianie.
  • Zastąp ShouldRender, zwracając false:
    • Gdy parametry są typami nieprymitywnymi lub nieobsługiwanymi typami niezmiennymi†, takimi jak złożone niestandardowe typy modelu lub wartości RenderFragment, a wartości parametrów nie zostały zmienione,
    • Jeśli tworzysz składnik przeznaczony wyłącznie dla interfejsu użytkownika, który nie zmienia się po początkowym renderowaniu, niezależnie od zmian wartości parametrów.

† Aby uzyskać więcej informacji, zobacz logikę wykrywania zmian w Blazorźródle odwołania (ChangeDetection.cs).

Uwaga / Notatka

Linki dokumentacyjne do źródła referencyjnego .NET zwykle ładują domyślną gałąź repozytorium, która odzwierciedla obecne prace rozwojowe nad nadchodzącą wersją .NET. Aby wybrać tag dla określonej wersji, użyj listy rozwijanej Przełącz gałęzie lub tagi. Aby uzyskać więcej informacji, zobacz Jak wybrać tag wersji kodu źródłowego ASP.NET Core (dotnet/AspNetCore.Docs #26205).

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 którykolwiek z identyfikatorów lotu zmieni się, gdy parametry składnika są ustawione w OnParametersSet, składnik jest ponownie renderowany, ponieważ shouldRender jest ustawiony na true. Jeśli shouldRender zostanie ocenione jako false po sprawdzeniu identyfikatorów lotu, unika się kosztownego ponownego renderowania:

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

Procedura obsługi zdarzeń może również ustawić shouldRender na true. 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.

Blazor Virtualize<TItem> udostępnia składnik, który umożliwia tworzenie zachowań związanych z wyglądem i przewijaniem dowolnej dużej listy, renderując przy tym tylko elementy listy znajdujące się w bieżącym 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 wirtualizację komponentów w ASP.NET Core.

Tworzenie lekkich, zoptymalizowanych składników

Większość komponentów nie wymaga agresywnych działań optymalizacji, ponieważ większość Razor komponentów nie powtarza się w interfejsie użytkownika i nie renderują się ponownie z dużą częstotliwością. Na przykład komponenty z dyrektywą @page, które są używane do renderowania elementów wysokiego poziomu interfejsu użytkownika, takich jak okna dialogowe lub formularze, najprawdopodobniej pojawiają się tylko pojedynczo i są ponownie renderowane tylko 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 każdy element, komórka lub punkt danych jest modelowany jako oddzielna instancja składnika, często jest ich tak wiele, ż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 komponenty, możesz mieć mniejsze części interfejsu użytkownika renderowane ponownie w przypadku wystąpienia zdarzeń. W tabeli z wieloma wierszami, które mają przycisk w każdym wierszu, można zaktualizować tylko ten jeden wiersz przy użyciu komponentu podrzędnego zamiast całej strony lub tabeli. Jednak każdy składnik wymaga dodatkowego obciążenia pamięci i procesora, aby zmierzyć się z niezależnym stanem i cyklem życia renderowania.

W teście przeprowadzonym przez inżynierów zespołu produktu ASP.NET Core zaobserwowano obciążenie renderowania w aplikacji wynoszące około 0,06 ms na wystąpienie komponentu. 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. Przy użyciu mnożenia można zauważyć, że dodanie 2000 dodatkowych wystąpień składników spowoduje dodanie 0,12 sekundy do czasu renderowania, a interfejs użytkownika zacznie działać wolniej 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 Zarządzanie pamięcią we wdrożonych aplikacjach po stronie Blazor serwera ASP.NET Core.

Osadź składniki podrzędne w ich rodzicach: Rozważmy następującą część składnika nadrzędnego, w której renderowane są 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 zagnieźdź składnik podrzędny w elemencie nadrzędnym. Poniższe podejście pozwala uniknąć narzutu związanego z renderowaniem tak wielu komponentów podrzędnych, ale kosztem utraty możliwości samodzielnego przerysowania znaczników każdego z tych komponentów.

<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: możesz uwzględniać składniki podrzędne wyłącznie jako sposób ponownego użycia 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ł wielokrotnie używany w wielu składnikach, zadeklaruj 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 związanego z każdym składnikiem.

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 to podejście nie pozwala na niezależne odświeżanie poddrzewa interfejsu użytkownika, ani nie ma możliwości pomijania renderowania poddrzewa interfejsu użytkownika, gdy jego nadrzędny element jest renderowany, ponieważ nie ma granicy komponentów. Przypisanie do delegata RenderFragment jest obsługiwane tylko w plikach komponentów Razor (.razor).

W przypadku niestatycznego pola, metody lub właściwości, do których nie można odwoływać się za pomocą inicjalizatora pola, jak jest to pokazane w poniższym przykładzie dla TitleTemplate, należy użyć właściwości zamiast pola dla RenderFragment.

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

Nie odbieraj zbyt wielu parametrów

Jeśli komponent powtarza się bardzo często, na przykład setki lub tysiące razy, obciążenie związane z przesyłaniem i odbieraniem każdego parametru narasta.

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 inny dla każdej komórki, ale Options jest wspólny w przypadku wszystkich wystąpień komórek.

@typeparam TItem

...

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

Należy jednak pamiętać, że łączenie parametrów pierwotnych w klasie nie zawsze jest zaletą. Chociaż może zmniejszyć liczbę parametrów, ma również wpływ na zachowanie wykrywania zmian i renderowania. Przekazywanie parametrów innych niż pierwotne zawsze wyzwala ponowne renderowanie, ponieważ Blazor nie może wiedzieć, czy dowolne obiekty mają stan wewnętrznie modyfikowalny, podczas gdy przekazywanie parametrów pierwotnych wyzwala ponowne renderowanie tylko wtedy, gdy ich wartości rzeczywiście uległy zmianie.

Należy również wziąć pod uwagę, że może to być ulepszenie, aby nie mieć komponentu komórki tabeli, jak pokazano w poprzednim przykładzie, a zamiast tego logika była bezpośrednio wbudowana w komponent nadrzędny.

Uwaga / Notatka

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 jest ustawione na false (wartość domyślna), każdy odbiorca wartości w kaskadowym przepływie 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 wartość IsFixed to true (na przykład <CascadingValue Value="someValue" IsFixed="true">), adresaci otrzymują wartość początkową, ale nie konfigurują subskrypcji, aby otrzymywać aktualizacje. Każdy z nich [CascadingParameter] jest lekki i nie droższy niż zwykły [Parameter].

Ustawienie IsFixed na true poprawia wydajność, jeśli istnieje duża liczba innych składników, które otrzymują wartość kaskadową. W miarę możliwości ustaw IsFixed na true w odniesieniu do wartości kaskadowanych. Można ustawić IsFixed wartość na true , gdy podana wartość nie zmienia się wraz z upływem czasu.

Gdy składnik przekazuje this jako wartość kaskadową, można również ustawić IsFixed na true, ponieważ this nigdy nie zmienia się podczas cyklu życia składnika:

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

Aby uzyskać więcej informacji, zobacz Blazor.

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 splattingu atrybutów i dowolnych parametrów.

Implementuj 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 podejście może przynieść niewielką poprawę renderowania, zazwyczaj poniżej 10% nawet przy 10 000+ instancji składników podczas targetowania .NET 10. Potencjalne zyski są mniejsze w kolejnych wersjach, ponieważ przypisanie parametrów opartych na odbiciu jest zoptymalizowane. Rozważ to podejście wyłącznie w ekstremalnych scenariuszach, które wymieniono wcześniej w tej sekcji, i najpierw wykonaj test porównawczy — oszczędności są zwykle przyćmione przez inne koszty, na przykład zmiany (edycja DOM) dla Interaktywnego Serwera.

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 podejście może poprawić wydajność renderowania przez 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 mogą uruchamiać się dziesiątki lub setki razy w 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 działają szybko, rozważ użycie JS interop w celu zarejestrowania wywołania zwrotnego, które działa 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 dla ruchu myszy. W tym przykładzie nasłuchiwacz zdarzeń używa funkcji throttle Lodash 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 ponownego renderowania po obsłudze zdarzeń bez zmian stanu

Składniki dziedziczą z ComponentBase, który automatycznie wywołuje StateHasChanged po wywołaniu procedur obsługi zdarzeń składnika. W niektórych przypadkach może być niepotrzebne lub niepożądane, aby wywołać ponowne renderowanie po uruchomieniu procedury 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.

Uwaga / Notatka

Podejście w tej sekcji nie propaguje wyjątków do granice błędów. Aby uzyskać więcej informacji oraz zobaczyć kod demonstracyjny, który obsługuje ograniczenia błędów poprzez wywołanie metody ComponentBase.DispatchExceptionAsync, zobacz AsNonRenderingEventHandler + ErrorBoundary = nieoczekiwane zachowanie (dotnet/aspnetcore #54543).

Aby uniknąć ponownego renderowania wszystkich programów obsługi zdarzeń składnika, zaimplementuj IHandleEvent i podaj zadanie IHandleEvent.HandleEventAsync, które wywołuje obsługę zdarzeń bez sięgania po metodę StateHasChanged.

W poniższym przykładzie do składnika nie jest dodawana żadna obsługa zdarzeń, która powoduje ponowne renderowanie, dlatego HandleSelect nie powoduje ponownego renderowania po wywołaniu.

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 ponownemu renderowaniu po uruchomieniu procedur obsługi zdarzeń w składniku na poziomie globalnym, można zapobiec ponownemu renderowaniu po pojedynczej procedurze obsługi zdarzeń, stosując następującą metodę pomocniczą.

Dodaj następującą EventUtil klasę Blazor do aplikacji. Akcje statyczne i funkcje na szczycie klasy EventUtil zapewniają procedury obsługi obejmujące kilka kombinacji argumentów i typów zwracanych, które Blazor wykorzystuje 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 EventUtil.AsNonRenderingEventHandler procedurę obsługi zdarzeń, która nie wywołuje renderowania po wywołaniu.

W poniższym przykładzie:

  • Wybranie pierwszego przycisku, który wywołuje metodę HandleClick1, inicjuje ponowne renderowanie.
  • Wybranie drugiego przycisku, który wywołuje HandleClick2, nie powoduje przerenderowania.
  • Wybranie trzeciego przycisku, które wywołuje , nie powoduje ponownego renderowania i używa argumentów zdarzeń ().

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 nadpisanie ShouldRender w komponentach podrzędnych komponentu docelowego można użyć do kontrolowania procesu ponownego renderowania.

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

BlazorTworzenie na nowo delegatów wyrażeń lambda dla elementów lub komponentó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 jest niekorzystnie wpłynięta, co prowadzi do złego doświadczenia 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 => { };
    }
}