Procedure consigliate per le prestazioni principali Blazor di ASP.NET

Nota

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

Importante

Queste informazioni si riferiscono a un prodotto non definitive che può essere modificato in modo sostanziale prima che venga rilasciato commercialmente. Microsoft non riconosce alcuna garanzia, espressa o implicita, in merito alle informazioni qui fornite.

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

Blazor è ottimizzato per prestazioni elevate negli scenari più realistici dell'interfaccia utente dell'applicazione. Tuttavia, le prestazioni migliori dipendono dagli sviluppatori che adottano i modelli e le funzionalità corretti.

Nota

Gli esempi di codice in questo articolo adottano tipi di riferimento nullable (NRT) e l'analisi statica dello stato null del compilatore .NET, supportati in ASP.NET Core in .NET 6 o versione successiva.

Ottimizzare la velocità di rendering

Ottimizzare la velocità di rendering per ridurre al minimo il carico di lavoro di rendering e migliorare la velocità di risposta dell'interfaccia utente, che può produrre un miglioramento della 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 rindering dei sottoalberi dei componenti figlio quando si verifica un evento. È consigliabile ignorare solo i sottoalberi di rindering particolarmente costosi per il rendering e causare un ritardo dell'interfaccia utente.

In fase di esecuzione, i componenti esistono in una gerarchia. Un componente radice (il primo componente caricato) include componenti figlio. A sua volta, gli elementi figlio della radice hanno i propri componenti figlio e così via. Quando si verifica un evento, ad esempio un utente che seleziona un pulsante, il processo seguente determina quali componenti eseguire il rerender:

  1. L'evento viene inviato al componente che ha eseguito il rendering del gestore dell'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, ogni componente decide se eseguire il rerender. Per impostazione predefinita, i componenti rerender se i valori dei parametri possono 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 costoso perché ogni componente al di sotto del componente di alto livello deve eseguire il rerendering.

Per impedire il rendering della ricorsione in un particolare sottoalbero, usare uno degli approcci seguenti:

  • Assicurarsi che i parametri dei componenti figlio siano di tipi primitivi non modificabili, ad esempio string, intbool, DateTime, e altri tipi simili. La logica predefinita per rilevare le modifiche ignora automaticamente il rerendering se i valori dei parametri non modificabili primitivi non sono stati modificati. Se si esegue il rendering di un componente figlio con <Customer CustomerId="@item.CustomerId" />, dove CustomerId è un int tipo, il Customer componente non viene ririsolto a meno che item.CustomerId non vengano apportate modifiche.
  • Eseguire l'override ShouldRenderdi :
    • Per accettare valori di parametro nonprimitive, ad esempio tipi di modello personalizzati complessi, callback di eventi o RenderFragment valori.
    • Se la creazione di un componente solo dell'interfaccia utente che non cambia dopo il rendering iniziale, indipendentemente dal valore del parametro cambia.

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 di anteprima in ingresso precedente (prevInboundFlightId) e le informazioni precedenti sull'identificatore di anteprima in uscita (prevOutboundFlightId) per il successivo aggiornamento potenziale del componente. Se uno degli identificatori di anteprima cambia quando i parametri del componente vengono impostati in OnParametersSet, il componente viene riabilita perché shouldRender è impostato su true. Se shouldRender restituisce false dopo il controllo degli identificatori di anteprima, viene evitato un render costoso:

@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 su shouldRendertrue. Per la maggior parte dei componenti, determinare il rerendering a livello di singoli gestori eventi in genere non è necessario.

Per ulteriori informazioni, vedi le seguenti risorse:

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, la quantità di operazioni di rendering può causare un ritardo nel 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 Virtualize<TItem> componente per creare l'aspetto e i comportamenti di scorrimento di un elenco arbitrariamente di grandi dimensioni, eseguendo il rendering solo degli elementi di elenco 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 altre informazioni, vedere ASP.NET Virtualizzazione dei componenti coreRazor.

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 @page direttiva e i componenti usati per eseguire il rendering di parti di alto livello dell'interfaccia utente, ad esempio dialoghi o moduli, appaiono probabilmente solo uno alla volta e rerendere solo in risposta a un movimento 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 la modellazione di ogni elemento, cella o punto dati come istanza di componente separata, spesso le prestazioni di rendering diventano fondamentali. Questa sezione fornisce consigli su come rendere leggeri tali componenti in modo che l'interfaccia utente rimanga veloce e reattiva.

Evitare 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 altre informazioni sulla gestione della memoria, vedere Ospitare e distribuire ASP.NET app sul lato Blazor server core.

Componenti figlio inline nei propri genitori

Si consideri la parte seguente di un componente padre che esegue il rendering dei 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, inline il componente figlio nell'elemento padre. L'approccio seguente evita il sovraccarico per componente del rendering di tanti componenti figlio al costo di perdere la possibilità di eseguire il rerendere il markup di ogni 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 il riutilizzo RenderFragments nel codice

È possibile eseguire il factoring dei componenti figlio esclusivamente come metodo per riutilizzare la logica di rendering. In questo caso, è possibile creare una logica di rendering riutilizzabile senza implementare componenti aggiuntivi. In un blocco di @code qualsiasi componente definire un oggetto 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 RenderTreeBuilder riutilizzabile il codice tra più componenti, dichiarare epublicRenderFragmentstatic:

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 RenderFragment delegato è supportata solo nei Razor file dei componenti (.razor) e i callback degli eventi non sono supportati.

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

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

Nota

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

L'impostazione IsFixed di per true migliorare le prestazioni se sono presenti un numero elevato di altri componenti che ricevono il valore a catena. Se possibile, impostare su IsFixedtrue su su valori a catena. È possibile impostare su IsFixedtrue quando il valore specificato non cambia nel tempo.

Quando un componente passa this come valore a catena, IsFixed può anche essere impostato su true:

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

Per altre informazioni, vedere ASP.NET Valori e parametri a catena di baseBlazor.

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:

  • Trovare la corrispondenza con tutti i parametri forniti rispetto al set di parametri noti per compilare un dizionario.
  • Tenere traccia della sovrascrittura tra più copie dello stesso attributo.

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 lo splatting degli attributi.

Per altre informazioni, vedere ASP.NET parametri arbitrari e splatting dell'attributo CoreBlazor.

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 usa la reflection per scrivere i valori dei parametri, che possono causare prestazioni scarse su larga scala.

In alcuni casi estremi, è possibile evitare la reflection e implementare manualmente la logica di impostazione 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 del 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 generati 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, prendere in considerazione l'uso dell'interoperabilità JS per registrare un callback che genera 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

Per impostazione predefinita, 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.

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.

Aggiungere la classe seguente EventUtil a un'app 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 chiamare un gestore eventi che non attiva un rendering quando viene richiamato.

Nell'esempio seguente :

  • Selezionando il primo pulsante, che chiama HandleClick1, viene attivato un rerender.
  • 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, l'override ShouldRender nei componenti figlio del componente di destinazione può essere usato per controllare il rerendering.

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 Blazor la ricompilazione di tutti i delegati del pulsante ogni volta che viene eseguito 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 => { };
    }
}

Ottimizzare la velocità di interoperabilità JavaScript

Le chiamate tra .NET e JavaScript richiedono un sovraccarico aggiuntivo perché:

  • Per impostazione predefinita, le chiamate sono asincrone.
  • Per impostazione predefinita, i parametri e i valori restituiti vengono JSserializzati per fornire un meccanismo di conversione facile da comprendere tra i tipi .NET e JavaScript.

Inoltre, per le app sul lato Blazor server, queste chiamate vengono passate attraverso la rete.

Evitare chiamate con granularità eccessiva

Poiché ogni chiamata comporta un sovraccarico, può essere utile ridurre il numero di chiamate. Si consideri il codice seguente, che archivia una raccolta di elementi nel browser localStorage:

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

Nell'esempio precedente viene eseguita una chiamata di interoperabilità separata JS per ogni elemento. L'approccio seguente riduce invece l'interoperabilità JS a una singola chiamata:

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

La funzione JavaScript corrispondente archivia l'intera raccolta di elementi nel client:

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

Per Blazor WebAssembly le app, il rollover di singole JS chiamate di interoperabilità in una singola chiamata in genere migliora significativamente le prestazioni solo se il componente effettua un numero elevato di JS chiamate di interoperabilità.

Prendere in considerazione l'uso di chiamate sincrone

Chiamare JavaScript da .NET

Questa sezione si applica solo ai componenti lato client.

Le chiamate all'interoperabilità JS sono asincrone per impostazione predefinita, indipendentemente dal fatto che il codice chiamato sia sincrono o asincrono. Le chiamate sono asincrone per impostazione predefinita per garantire che i componenti siano compatibili tra le modalità di rendering lato server e lato client. Nel server tutte le JS chiamate di interoperabilità devono essere asincrone perché vengono inviate tramite una connessione di rete.

Se si è certi che il componente viene eseguito solo in WebAssembly, è possibile scegliere di effettuare chiamate di interoperabilità sincrone JS . Questo comporta un sovraccarico leggermente inferiore rispetto all'esecuzione di chiamate asincrone e può comportare un minor numero di cicli di rendering perché non esiste uno stato intermedio in attesa dei risultati.

Per effettuare una chiamata sincrona da .NET a JavaScript in un componente lato client, eseguire il cast IJSRuntime per IJSInProcessRuntime effettuare la JS chiamata di interoperabilità:

@inject IJSRuntime JS

...

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

Quando si usa IJSObjectReference in ASP.NET Componenti lato client core 5.0 o versioni successive, è possibile usare IJSInProcessObjectReference invece in modo sincrono. IJSInProcessObjectReference implementa IAsyncDisposable/IDisposable e deve essere eliminato per l'operazione di Garbage Collection per evitare una perdita di memoria, come illustrato nell'esempio seguente:

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

Chiamare .NET da JavaScript

Questa sezione si applica solo ai componenti lato client.

Le chiamate all'interoperabilità JS sono asincrone per impostazione predefinita, indipendentemente dal fatto che il codice chiamato sia sincrono o asincrono. Le chiamate sono asincrone per impostazione predefinita per garantire che i componenti siano compatibili tra le modalità di rendering lato server e lato client. Nel server tutte le JS chiamate di interoperabilità devono essere asincrone perché vengono inviate tramite una connessione di rete.

Se si è certi che il componente viene eseguito solo in WebAssembly, è possibile scegliere di effettuare chiamate di interoperabilità sincrone JS . Questo comporta un sovraccarico leggermente inferiore rispetto all'esecuzione di chiamate asincrone e può comportare un minor numero di cicli di rendering perché non esiste uno stato intermedio in attesa dei risultati.

Per eseguire una chiamata sincrona da JavaScript a .NET in un componente lato client, usare DotNet.invokeMethod anziché DotNet.invokeMethodAsync.

Le chiamate sincrone funzionano se:

  • Il rendering del componente viene eseguito solo per l'esecuzione in WebAssembly.
  • La funzione chiamata restituisce un valore in modo sincrono. La funzione non è un async metodo e non restituisce .NET Task o JavaScript Promise.

Questa sezione si applica solo ai componenti lato client.

Le chiamate all'interoperabilità JS sono asincrone per impostazione predefinita, indipendentemente dal fatto che il codice chiamato sia sincrono o asincrono. Le chiamate sono asincrone per impostazione predefinita per garantire che i componenti siano compatibili tra le modalità di rendering lato server e lato client. Nel server tutte le JS chiamate di interoperabilità devono essere asincrone perché vengono inviate tramite una connessione di rete.

Se si è certi che il componente viene eseguito solo in WebAssembly, è possibile scegliere di effettuare chiamate di interoperabilità sincrone JS . Questo comporta un sovraccarico leggermente inferiore rispetto all'esecuzione di chiamate asincrone e può comportare un minor numero di cicli di rendering perché non esiste uno stato intermedio in attesa dei risultati.

Per effettuare una chiamata sincrona da .NET a JavaScript in un componente lato client, eseguire il cast IJSRuntime per IJSInProcessRuntime effettuare la JS chiamata di interoperabilità:

@inject IJSRuntime JS

...

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

Quando si usa IJSObjectReference in ASP.NET Componenti lato client core 5.0 o versioni successive, è possibile usare IJSInProcessObjectReference invece in modo sincrono. IJSInProcessObjectReference implementa IAsyncDisposable/IDisposable e deve essere eliminato per l'operazione di Garbage Collection per evitare una perdita di memoria, come illustrato nell'esempio seguente:

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

Prendere in considerazione l'uso di chiamate non sposate

Questa sezione si applica solo alle Blazor WebAssembly app.

Quando è in esecuzione in Blazor WebAssembly, è possibile effettuare chiamate senza falle da .NET a JavaScript. Si tratta di chiamate sincrone che non eseguono JSla serializzazione ON di argomenti o valori restituiti. Tutti gli aspetti della gestione della memoria e delle traduzioni tra le rappresentazioni .NET e JavaScript vengono lasciati allo sviluppatore.

Avviso

Anche se l'uso IJSUnmarshalledRuntime ha il minor sovraccarico degli JS approcci di interoperabilità, le API JavaScript necessarie per interagire con queste API non sono attualmente documentate e soggette a modifiche di rilievo nelle versioni future.

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

Usare l'interoperabilità JavaScript [JSImport]/[JSExport]

L'interoperabilità JavaScript [JSImport]/[JSExport] per Blazor WebAssembly le app offre prestazioni e stabilità migliorate rispetto all'API JS di interoperabilità nelle versioni del framework prima di ASP.NET Core in .NET 7.

Per altre informazioni, vedere Interoperabilità di importazione/JSesportazione JavaScript JScon ASP.NET CoreBlazor.

Compilazione anticipata (AOT)

La compilazione AOT (Ahead-of-Time) compila il codice .NET di un'app Blazor direttamente in WebAssembly nativo per l'esecuzione diretta dal browser. Le app compilate con AOT comportano app di dimensioni maggiori che richiedono più tempo per il download, ma le app compilate con AOT in genere offrono prestazioni di runtime migliori, soprattutto per le app che eseguono attività a elevato utilizzo di CPU. Per altre informazioni, vedere ASP.NET Compilazione di core Blazor WebAssembly e compilazione anticipata (AOT).

Ridurre al minimo le dimensioni di download dell'app

Ricollegamento del runtime

Per informazioni su come il ripristino del runtime riduce al minimo le dimensioni di download di un'app, vedere ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation (AOT).

Utilizzare System.Text.Json.

BlazorL'implementazione dell'interoperabilità JS si basa su System.Text.Json, ovvero una libreria di serializzazione ON a prestazioni JSelevate con allocazione di memoria insufficiente. L'uso System.Text.Json non dovrebbe comportare dimensioni aggiuntive del payload dell'app rispetto all'aggiunta di una o più librerie ON alternative JS.

Per indicazioni sulla migrazione, vedere Come eseguire la migrazione da Newtonsoft.Json a System.Text.Json.

Taglio del linguaggio intermedio (IL)

Questa sezione si applica solo alle Blazor WebAssembly app.

La rimozione di assembly inutilizzati da un'app Blazor WebAssembly riduce le dimensioni dell'app rimuovendo il codice inutilizzato nei file binari dell'app. Per altre informazioni, vedere Configurare Trimmer per ASP.NET Core Blazor.

Il collegamento di un'app Blazor WebAssembly riduce le dimensioni dell'app tagliando il codice inutilizzato nei file binari dell'app. Per impostazione predefinita, il linker intermedio (IL) è abilitato solo durante la compilazione nella Release configurazione. A tale scopo, pubblicare l'app per la distribuzione usando il dotnet publish comando con l'opzione -c|--configuration impostata su Release:

dotnet publish -c Release

Assembly di caricamento differita

Questa sezione si applica solo alle Blazor WebAssembly app.

Caricare gli assembly in fase di esecuzione quando gli assembly sono richiesti da una route. Per altre informazioni, vedere Assembly di caricamento differita in ASP.NET Core Blazor WebAssembly.

Compressione

Questa sezione si applica solo alle Blazor WebAssembly app.

Quando un'app Blazor WebAssembly viene pubblicata, l'output viene compresso in modo statico durante la pubblicazione per ridurre le dimensioni dell'app e rimuovere il sovraccarico per la compressione in fase di esecuzione. Blazor si basa sul server per eseguire la negoziazione del contenuto e gestire file compressi in modo statico.

Dopo la distribuzione di un'app, verificare che l'app gestisca i file compressi. Esaminare la scheda Rete negli strumenti di sviluppo di un browser e verificare che i file siano serviti con Content-Encoding: br (compressione Brotli) o Content-Encoding: gz (compressione Gzip). Se l'host non gestisce file compressi, seguire le istruzioni in Host e distribuire ASP.NET Core Blazor WebAssembly.

Disabilitare le funzionalità inutilizzate

Questa sezione si applica solo alle Blazor WebAssembly app.

Blazor WebAssemblyIl runtime di include le funzionalità .NET seguenti che possono essere disabilitate per una dimensione del payload inferiore:

  • È incluso un file di dati per correggere le informazioni sul fuso orario. Se l'app non richiede questa funzionalità, è consigliabile disabilitarla impostando la BlazorEnableTimeZoneSupport proprietà MSBuild nel file di progetto dell'app su false:

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • Le informazioni sulle regole di confronto sono incluse per fare in modo che le API funzionino StringComparison.InvariantCultureIgnoreCase correttamente. Se si è certi che l'app non richiede i dati delle regole di confronto, è consigliabile disabilitarla impostando la BlazorWebAssemblyPreserveCollationData proprietà MSBuild nel file di progetto dell'app su false:

    <PropertyGroup>
      <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
    </PropertyGroup>
    
  • Per impostazione predefinita, Blazor WebAssembly le risorse di globalizzazione necessarie per visualizzare valori, ad esempio date e valuta, nelle impostazioni cultura dell'utente. Se l'app non richiede la localizzazione, puoi configurare l'app per supportare le impostazioni cultura invarianti, basate sulle en-US impostazioni cultura.