Condividi tramite


Migliori pratiche per le prestazioni di rendering di ASP.NET Core Blazor

Annotazioni

Questa non è la versione più recente di questo articolo. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Avvertimento

Questa versione di ASP.NET Core non è più supportata. Per altre informazioni, vedere i criteri di supporto di .NET e .NET Core. Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Importante

Queste informazioni si riferiscono a un prodotto in fase di anteprima che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non fornisce alcuna garanzia, espressa o implicita, in relazione alle informazioni fornite qui.

Per la versione corrente, vedere la versione .NET 9 di questo articolo.

Ottimizzare la velocità di rendering per ridurre al minimo il carico di lavoro di rendering e migliorare la reattività dell'interfaccia utente, il che può produrre un miglioramento decuplo o superiore nella velocità di rendering dell'interfaccia utente.

Evitare il rendering non necessario dei sottoalberi dei componenti

È possibile rimuovere la maggior parte dei costi di rendering di un componente padre ignorando il rerendering dei sottoalberi dei componenti figli quando si verifica un evento. È consigliabile concentrarsi solo sull'ignorare i sottoalberi di rendering che sono particolarmente costosi da renderizzare e che causano ritardi nell'interfaccia utente.

In fase di esecuzione, i componenti esistono in una gerarchia. Un componente radice (il primo componente caricato) include componenti figlio. A loro volta, i figli della radice hanno i propri componenti figli e così via. Quando si verifica un evento, ad esempio la selezione di un pulsante da parte dell'utente, il processo seguente determina quali componenti ridisegnare:

  1. L'evento viene inviato al componente che ha eseguito il rendering e gestisce l'evento. Dopo aver eseguito il gestore eventi, il componente viene riindirizzato.
  2. Quando un componente viene rerendered, fornisce una nuova copia dei valori dei parametri a ognuno dei relativi componenti figlio.
  3. Dopo la ricezione di un nuovo set di valori di parametro, Blazor decide se eseguire il rerendere il componente. I componenti vengono nuovamente renderizzati se ShouldRender restituisce true, che è il comportamento predefinito a meno che non sia sovrascritto, e i valori dei parametri potrebbero essere stati modificati, ad esempio se sono oggetti modificabili.

Gli ultimi due passaggi della sequenza precedente continuano in modo ricorsivo verso il basso nella gerarchia dei componenti. In molti casi, viene eseguito il rendering dell'intero sottoalbero. Gli eventi destinati a componenti di alto livello possono causare un rendering oneroso poiché ogni componente sotto il componente di alto livello deve essere nuovamente renderizzato.

Per impedire la ricorsione nel rendering di un particolare sottoalbero, usare uno degli approcci seguenti:

  • Assicurarsi che i parametri del componente figlio siano di tipi non modificabili specifici†, ad esempio string, int, boole DateTime. La logica predefinita per rilevare le modifiche ignora automaticamente il rerendering se i valori dei parametri non modificabili non sono stati modificati. Se si esegue il rendering di un componente figlio con <Customer CustomerId="item.CustomerId" />, dove CustomerId è un tipo int, il componente Customer non viene nuovamente renderizzato a meno che item.CustomerId non cambi.
  • Eseguire l'override di ShouldRender, restituendo false:
    • Quando i parametri sono tipi nonprimitivi o tipi non modificabili non supportati†, ad esempio tipi o RenderFragment valori di modelli personalizzati complessi e i valori dei parametri non sono stati modificati,
    • Quando si crea un componente dell'interfaccia utente che non cambia dopo il rendering iniziale, indipendentemente dalle modifiche ai valori dei parametri.

†Per altre informazioni, vedere la logica di rilevamento delle modifiche nell'origine Blazordi riferimento (ChangeDetection.cs).

Annotazioni

In genere, i collegamenti della documentazione alla sorgente di riferimento .NET caricano il ramo predefinito del repository, che rappresenta lo sviluppo corrente per la versione successiva di .NET. Per selezionare un tag per una versione specifica, utilizzare il menu a tendina Seleziona rami o tag. Per ulteriori informazioni, vedere Come selezionare un tag di versione del codice sorgente di ASP.NET Core (dotnet/AspNetCore.Docs #26205).

L'esempio seguente dello strumento di ricerca dei voli aerei usa campi privati per tenere traccia delle informazioni necessarie per rilevare le modifiche. L'identificatore del volo precedente in arrivo (prevInboundFlightId) e l'identificatore del volo precedente in partenza (prevOutboundFlightId) tracciano le informazioni per il successivo aggiornamento potenziale del componente. Se uno degli identificatori di volo cambia quando i parametri del componente vengono impostati in OnParametersSet, il componente viene renderizzato di nuovo perché shouldRender è impostato su true. Se il valore di shouldRender è uguale a false dopo il controllo degli identificatori di volo, viene evitato un costoso rerender:

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

Un gestore eventi può anche impostare shouldRender a true. Per la maggior parte dei componenti, determinare il ridisegno a livello di singoli gestori di eventi in genere non è necessario.

Per altre informazioni, vedere le risorse seguenti:

Virtualizzazione

Quando si esegue il rendering di grandi quantità di interfaccia utente all'interno di un ciclo, ad esempio un elenco o una griglia con migliaia di voci, il numero di operazioni di rendering può provocare un rallentamento del rendering dell'interfaccia utente. Dato che l'utente può visualizzare solo un numero ridotto di elementi contemporaneamente senza scorrere, spesso è sprecato dedicare tempo a rendere gli elementi che non sono attualmente visibili.

Blazor fornisce il componente Virtualize<TItem> per creare l'aspetto e i comportamenti di scorrimento di un elenco di dimensioni arbitrariamente grandi, eseguendo il rendering solo degli elementi di elenco che si trovano all'interno del viewport di scorrimento corrente. Ad esempio, un componente può eseguire il rendering di un elenco con 100.000 voci, ma pagare solo il costo di rendering di 20 elementi visibili.

Per ulteriori informazioni, vedere ASP.NET Core Razor virtualizzazione dei componenti.

Creare componenti leggeri e ottimizzati

La maggior parte dei Razor componenti non richiede sforzi di ottimizzazione aggressivi perché la maggior parte dei componenti non si ripete nell'interfaccia utente e non esegue il rerender ad alta frequenza. Ad esempio, i componenti instradabili con una direttiva @page e i componenti usati per il rendering di parti di alto livello dell'interfaccia utente, come dialoghi o moduli, appaiono probabilmente solo uno alla volta e vengono ri-renderizzati solo in risposta a un gesto dell'utente. Questi componenti in genere non creano un carico di lavoro di rendering elevato, quindi è possibile usare liberamente qualsiasi combinazione di funzionalità del framework senza preoccuparsi delle prestazioni di rendering.

Esistono tuttavia scenari comuni in cui i componenti vengono ripetuti su larga scala e spesso comportano prestazioni dell'interfaccia utente scarse:

  • Moduli annidati di grandi dimensioni con centinaia di singoli elementi, ad esempio input o etichette.
  • Griglie con centinaia di righe o migliaia di celle.
  • Grafici a dispersione con milioni di punti dati.

Se si modella ogni elemento, cella o punto dati come istanza di componente separata, spesso il numero di queste istanze è così elevato che le loro prestazioni di rendering diventano critiche. Questa sezione fornisce consigli su come rendere leggeri tali componenti in modo che l'interfaccia utente rimanga veloce e reattiva.

Evitare di creare migliaia di istanze del componente

Ogni componente è un'isola separata che può eseguire il rendering indipendentemente dai genitori e dai figli. Scegliendo come suddividere l'interfaccia utente in una gerarchia di componenti, si assume il controllo sulla granularità del rendering dell'interfaccia utente. Ciò può comportare prestazioni buone o scarse.

Suddividendo l'interfaccia utente in componenti separati, è possibile avere parti più piccole del rerender dell'interfaccia utente quando si verificano eventi. In una tabella con molte righe che dispongono di un pulsante in ogni riga, potrebbe essere possibile disporre solo del nuovo riavvio di una singola riga usando un componente figlio anziché l'intera pagina o la tabella. Tuttavia, ogni componente richiede un sovraccarico di memoria e CPU aggiuntivo per gestire lo stato indipendente e il ciclo di vita del rendering.

In un test eseguito dagli ingegneri dell'unità di prodotto ASP.NET Core, è stato rilevato un sovraccarico di rendering di circa 0,06 ms per ogni istanza del componente in un'app Blazor WebAssembly . L'app di test ha eseguito il rendering di un componente semplice che accetta tre parametri. Internamente, l'overhead è dovuto in gran parte al recupero dello stato per componente dai dizionari e al passaggio e alla ricezione di parametri. Moltiplicando, è possibile notare che l'aggiunta di 2.000 istanze aggiuntive di componenti aggiunge 0,12 secondi al tempo di rendering e l'interfaccia utente inizierebbe a sentirsi lenta agli utenti.

È possibile rendere i componenti più leggeri in modo da poterli avere di più. Tuttavia, una tecnica più potente è spesso evitare di avere così tanti componenti per il rendering. Le sezioni seguenti descrivono due approcci che è possibile adottare.

Per ulteriori informazioni sulla gestione della memoria, vedere Gestire la memoria nelle app lato server distribuite ASP.NET CoreBlazor.

Componenti figlio in linea nei rispettivi componenti genitori: Si consideri la seguente parte di un componente padre che rende i componenti figlio in un ciclo:

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

L'esempio precedente funziona correttamente se migliaia di messaggi non vengono visualizzati contemporaneamente. Per visualizzare migliaia di messaggi contemporaneamente, è consigliabile non eseguire il factoring del componente separato ChatMessageDisplay . Invece, incorpora il componente figlio nel componente padre. L'approccio seguente evita il sovraccarico per componente del rendering di molti componenti figli al costo di perdere la possibilità di rilavorare il markup di ciascun componente figlio in modo indipendente:

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

Definire componenti riutilizzabili RenderFragments nel codice: si potrebbe separare i componenti figlio come un modo per riutilizzare la logica di rendering. In questo caso, è possibile creare una logica di rendering riutilizzabile senza implementare componenti aggiuntivi. In un blocco di qualsiasi componente @code, definire un RenderFragment. Eseguire il rendering del frammento da qualsiasi posizione quante volte necessario:

@RenderWelcomeInfo

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

@RenderWelcomeInfo

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

Per rendere il codice RenderTreeBuilder riutilizzabile tra più componenti, dichiarare RenderFragment, public e static.

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

SayHello nell'esempio precedente può essere richiamato da un componente non correlato. Questa tecnica è utile per la creazione di librerie di frammenti di markup riutilizzabili che eseguono il rendering senza sovraccarico per componente.

RenderFragment i delegati possono accettare parametri. Il componente seguente passa il messaggio (message) al RenderFragment delegato:

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

L'approccio precedente riutilizza la logica di rendering senza sovraccarico per componente. Tuttavia, l'approccio non consente di aggiornare il sottoalbero dell'interfaccia utente in modo indipendente, né ha la possibilità di ignorare il rendering del sottoalbero dell'interfaccia utente quando viene eseguito il rendering del relativo albero padre perché non esiste alcun limite del componente. L'assegnazione a un delegato RenderFragment è supportata solo nei file del componente Razor (.razor).

Per un campo, un metodo o una proprietà non statica a cui non è possibile fare riferimento da un inizializzatore di campo, ad esempio TitleTemplate nell'esempio seguente, usare una proprietà anziché un campo per :RenderFragment

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

Non ricevere troppi parametri

Se un componente si ripete molto spesso, ad esempio centinaia o migliaia di volte, il sovraccarico del passaggio e della ricezione di ogni parametro si accumula.

È raro che troppi parametri limitino gravemente le prestazioni, ma può essere un fattore. Per un TableCell componente che esegue il rendering di 4.000 volte all'interno di una griglia, ogni parametro passato al componente aggiunge circa 15 ms al costo di rendering totale. Il passaggio di dieci parametri richiede circa 150 ms e causa un ritardo di rendering dell'interfaccia utente.

Per ridurre il carico dei parametri, aggregare più parametri in una classe personalizzata. Ad esempio, un componente di cella di tabella potrebbe accettare un oggetto comune. Nell'esempio seguente, Data è diverso per ogni cella, ma Options è comune in tutte le istanze di cella:

@typeparam TItem

...

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

Tieni presente, tuttavia, che raggruppare i parametri primitivi in una classe non è sempre un vantaggio. Anche se può ridurre il numero di parametri, influisce anche sul comportamento del rilevamento e del rendering delle modifiche. Il passaggio di parametri non primitivi attiva sempre un nuovo rendering, perché Blazor non è in grado di sapere se gli oggetti arbitrari hanno uno stato modificabile internamente, mentre il passaggio di parametri primitivi attiva solo un nuovo rendering se i valori sono stati effettivamente modificati.

Si consideri inoltre che potrebbe essere un miglioramento non avere un componente della cella di tabella, come illustrato nell'esempio precedente, e inline la logica nel componente padre.

Annotazioni

Quando sono disponibili più approcci per migliorare le prestazioni, il benchmarking degli approcci è in genere necessario per determinare quale approccio produce i risultati migliori.

Per altre informazioni sui parametri di tipo generico (@typeparam), vedere le risorse seguenti:

Assicurarsi che i parametri a catena siano corretti

Il CascadingValue componente ha un parametro facoltativo IsFixed :

  • Se IsFixed è false (impostazione predefinita), ogni destinatario del valore a catena configura una sottoscrizione per ricevere notifiche di modifica. Ognuno [CascadingParameter] è sostanzialmente più costoso di un normale [Parameter] a causa del rilevamento delle sottoscrizioni.
  • Se IsFixed è true (ad esempio, <CascadingValue Value="someValue" IsFixed="true">), i destinatari ricevono il valore iniziale ma non configurano una sottoscrizione per ricevere gli aggiornamenti. Ogni [CascadingParameter] è leggero e non è più costoso di un normale [Parameter].

Impostare IsFixed su true migliora le prestazioni se è presente un numero elevato di altri componenti che ricevono il valore in cascata. Se possibile, impostare IsFixed su true sui valori concatenati. È possibile impostare IsFixed su true quando il valore fornito non cambia nel tempo.

Quando un componente passa this come valore a catena, IsFixed può anche essere impostato su true, perché this non cambia mai durante il ciclo di vita del componente:

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

Per ulteriori informazioni, vedere ASP.NET Core Blazor valori e parametri a cascata.

Evitare lo splatting degli attributi con CaptureUnmatchedValues

I componenti possono scegliere di ricevere i valori dei parametri "non corrispondenti" usando il CaptureUnmatchedValues flag :

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

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

Questo approccio consente di passare attributi aggiuntivi arbitrari all'elemento . Tuttavia, questo approccio è costoso perché il renderer deve:

  • Confrontare tutti i parametri forniti con il set di parametri noti per costruire un dizionario.
  • Tenere traccia di come più copie dello stesso attributo si sovrascrivono a vicenda.

Usare CaptureUnmatchedValues dove le prestazioni di rendering dei componenti non sono critiche, ad esempio i componenti che non vengono ripetuti frequentemente. Per i componenti di cui viene eseguito il rendering su larga scala, ad esempio ogni elemento in un elenco di grandi dimensioni o nelle celle di una griglia, provare a evitare l'uso estensivo di attributi.

Per ulteriori informazioni, vedere ASP.NET Core Blazor attribute splatting e parametri arbitrari.

Implementare SetParametersAsync manualmente

Un'origine significativa del sovraccarico di rendering per componente consiste nella scrittura dei valori dei parametri in ingresso nelle [Parameter] proprietà. Il renderer utilizza reflection per registrare i valori dei parametri, il che può portare a prestazioni ridotte su larga scala.

In alcuni casi estremi, è possibile evitare la reflection e implementare manualmente la logica di settaggio dei parametri. Questo può essere applicabile quando:

  • Un componente esegue il rendering estremamente spesso, ad esempio quando sono presenti centinaia o migliaia di copie del componente nell'interfaccia utente.
  • Un componente accetta molti parametri.
  • Si nota che il sovraccarico della ricezione dei parametri ha un impatto osservabile sulla velocità di risposta dell'interfaccia utente.

In casi estremi, è possibile eseguire l'override del metodo virtuale SetParametersAsync del componente e implementare una logica specifica del componente. L'esempio seguente evita deliberatamente le ricerche nei dizionari:

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

Nel codice precedente, la restituzione della classe SetParametersAsync base esegue il metodo normale del ciclo di vita senza assegnare di nuovo i parametri.

Come si può notare nel codice precedente, l'override SetParametersAsync e la fornitura di logica personalizzata è complicata e laboriosa, quindi in genere non è consigliabile adottare questo approccio. In casi estremi, può migliorare le prestazioni di rendering entro 20-25%, ma è consigliabile considerare questo approccio solo negli scenari estremi elencati in precedenza in questa sezione.

Non attivare gli eventi troppo rapidamente

Alcuni eventi del browser vengono attivati molto frequentemente. Ad esempio, onmousemove e onscroll può sparare decine o centinaia di volte al secondo. Nella maggior parte dei casi, non è necessario eseguire frequentemente gli aggiornamenti dell'interfaccia utente. Se gli eventi vengono attivati troppo rapidamente, è possibile danneggiare la velocità di risposta dell'interfaccia utente o consumare un tempo di CPU eccessivo.

Invece di usare eventi nativi che si attivano rapidamente, considera l'uso di JS interop per registrare un callback che si attiva meno frequentemente. Ad esempio, il componente seguente visualizza la posizione del mouse, ma aggiorna solo una volta ogni 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();
}

Il codice JavaScript corrispondente registra il listener di eventi DOM per lo spostamento del mouse. In questo esempio, il listener di eventi usa la funzione di throttle Lodash per limitare la frequenza delle chiamate:

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

Evitare il rerendering dopo la gestione degli eventi senza modifiche dello stato

I componenti ereditano da ComponentBase, che richiama StateHasChanged automaticamente dopo che i gestori eventi del componente vengono richiamati. In alcuni casi, potrebbe non essere necessario o indesiderato attivare un rerender dopo che viene richiamato un gestore eventi. Ad esempio, un gestore eventi potrebbe non modificare lo stato del componente. In questi scenari, l'app può sfruttare l'interfaccia IHandleEvent per controllare il comportamento della gestione degli Blazoreventi.

Annotazioni

L'approccio in questa sezione non propaga le eccezioni ai confini di errore. Per altre informazioni e codice dimostrativo che supporta i limiti di errore chiamando ComponentBase.DispatchExceptionAsync, vedere AsNonRenderingEventHandler + ErrorBoundary = comportamento imprevisto (dotnet/aspnetcore #54543).

Per impedire i rerender per tutti i gestori eventi di un componente, implementare IHandleEvent e fornire un'attività IHandleEvent.HandleEventAsync che richiama il gestore eventi senza chiamare StateHasChanged.

Nell'esempio seguente, nessun gestore eventi aggiunto al componente attiva un rerender, quindi HandleSelect non genera un rerender quando viene richiamato.

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

Oltre a impedire i rerender dopo che i gestori eventi vengono attivati in un componente in modo globale, è possibile impedire i rerender dopo un singolo gestore eventi usando il metodo di utilità seguente.

Aggiungi la seguente classe EventUtil a un'applicazione Blazor. Le azioni e le funzioni statiche all'inizio della EventUtil classe forniscono gestori che coprono diverse combinazioni di argomenti e tipi restituiti che Blazor usano per la gestione degli eventi.

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

Chiamare EventUtil.AsNonRenderingEventHandler per invocare un gestore eventi che non attiva una renderizzazione quando viene invocato.

Nell'esempio seguente:

  • Selezionando il primo pulsante, che richiama HandleClick1, si attiva un nuovo rendering.
  • La selezione del secondo pulsante, che chiama HandleClick2, non attiva un rerender.
  • Selezionando il terzo pulsante, che chiama HandleClick3, non attiva un rerender e usa gli argomenti dell'evento (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);
    }
}

Oltre a implementare l'interfaccia IHandleEvent , sfruttando le altre procedure consigliate descritte in questo articolo può anche contribuire a ridurre i rendering indesiderati dopo la gestione degli eventi. Ad esempio, la sovrascrittura ShouldRender nei componenti figlio del componente di destinazione può essere usata per controllare il nuovo rendering.

Evitare di ricreare delegati per molti elementi o componenti ripetuti

BlazorLa ricreazione dei delegati di espressioni lambda per elementi o componenti in un ciclo può causare prestazioni scarse.

Il componente seguente illustrato nell'articolo sulla gestione degli eventi esegue il rendering di un set di pulsanti. Ogni pulsante assegna un delegato al relativo @onclick evento, che è corretto se non sono presenti molti pulsanti di cui eseguire il rendering.

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

Se viene eseguito il rendering di un numero elevato di pulsanti usando l'approccio precedente, la velocità di rendering viene compromessa con un'esperienza utente scarsa. Per eseguire il rendering di un numero elevato di pulsanti con un callback per gli eventi click, nell'esempio seguente viene utilizzata una raccolta di oggetti pulsante che assegnano il delegato di @onclick ogni pulsante a un oggetto Action. L'approccio seguente non richiede di ricompilare Blazor tutti i delegati del pulsante ogni volta che viene effettuato il rendering dei pulsanti:

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