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 9 di questo articolo.
Avviso
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 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 9 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:
- 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.
- Quando un componente viene rerendered, fornisce una nuova copia dei valori dei parametri a ognuno dei relativi componenti figlio.
- Dopo la ricezione di un nuovo set di valori di parametro, ogni componente decide se eseguire il rerender. 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
,int
bool
,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" />
, doveCustomerId
è unint
tipo, ilCustomer
componente non viene ririsolto a meno cheitem.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 shouldRender
true
. 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:
- ASP.NET ciclo di vita dei componenti di base Razor
- ShouldRender
- rendering dei componenti di Razor base ASP.NET
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.
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 Blazor sul lato 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 e RenderFragmentpublic
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.
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:
- Guida di riferimento della sintassi Razor per ASP.NET Core
- componenti di Razor base ASP.NET
- Componenti basati su modelli di ASP.NET Core Blazor
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 IsFixed
true
su su valori a catena. È possibile impostare su IsFixed
true
quando il valore specificato 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 altre informazioni, vedere Blazor a catena di base.
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 Blazor Core.
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 throttle
di 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.
Nota
L'approccio in questa sezione non comporta il flusso delle eccezioni ai limiti degli errori. 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.
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é:
- Le chiamate sono asincrone.
- I parametri e i valori restituiti sono serializzati in FORMATO JSON 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.
JS le chiamate di interoperabilità sono asincrone, indipendentemente dal fatto che il codice chiamato sia sincrono o asincrono. Le chiamate sono asincrone 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 IDisposable
...
@code {
...
private IJSInProcessObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var jsInProcess = (IJSInProcessRuntime)JS;
module = await jsInProcess.Invoke<IJSInProcessObjectReference>("import",
"./scripts.js");
var value = module.Invoke<string>("javascriptFunctionIdentifier");
}
}
...
void IDisposable.Dispose()
{
if (module is not null)
{
await module.Dispose();
}
}
}
Nell'esempio precedente, un JSDisconnectedException oggetto non viene intrappolato durante l'eliminazione del modulo perché non c'è alcun BlazorSignalR circuito in un'app Blazor WebAssembly da perdere. Per altre informazioni, vedere ASP.NET Core JavaScript interoperabilità (interoperabilità).For more information, see ASP.NET Core Blazor JavaScript interoperability (JS interop).
Chiamare .NET da JavaScript
Questa sezione si applica solo ai componenti lato client.
JS le chiamate di interoperabilità sono asincrone, indipendentemente dal fatto che il codice chiamato sia sincrono o asincrono. Le chiamate sono asincrone 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:
Questa sezione si applica solo ai componenti lato client.
JS le chiamate di interoperabilità sono asincrone, indipendentemente dal fatto che il codice chiamato sia sincrono o asincrono. Le chiamate sono asincrone 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 IDisposable
...
@code {
...
private IJSInProcessObjectReference? module;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
var jsInProcess = (IJSInProcessRuntime)JS;
module = await jsInProcess.Invoke<IJSInProcessObjectReference>("import",
"./scripts.js");
var value = module.Invoke<string>("javascriptFunctionIdentifier");
}
}
...
void IDisposable.Dispose()
{
if (module is not null)
{
await module.Dispose();
}
}
}
Nell'esempio precedente, un JSDisconnectedException oggetto non viene intrappolato durante l'eliminazione del modulo perché non c'è alcun BlazorSignalR circuito in un'app Blazor WebAssembly da perdere. Per altre informazioni, vedere ASP.NET Core JavaScript interoperabilità (interoperabilità).For more information, see ASP.NET Core Blazor JavaScript interoperability (JS interop).
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 la serializzazione JSON 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à JSImport/JSExport JavaScript con ASP.NET Core Blazor.
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, che è una libreria di serializzazione JSON a prestazioni elevate 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 JSON alternative.
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 agli scenari lato Blazor client.
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. Il linker del linguaggio 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 agli scenari lato Blazor client.
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 agli scenari lato Blazor client.
Blazor WebAssemblyIl runtime di include le funzionalità .NET seguenti che possono essere disabilitate per una dimensione del payload inferiore:
-
Blazor WebAssembly contiene 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.
L'adozione della globalizzazione invariante comporta solo l'uso di nomi di fuso orario non localizzati. Per ridurre il codice e i dati del fuso orario dall'app, applicare la
<InvariantTimezone>
proprietà MSBuild con un valore ditrue
nel file di progetto dell'app:<PropertyGroup> <InvariantTimezone>true</InvariantTimezone> </PropertyGroup>
Nota
<BlazorEnableTimeZoneSupport>
esegue l'override di un'impostazione precedente<InvariantTimezone>
. È consigliabile rimuovere l'impostazione<BlazorEnableTimeZoneSupport>
.
È 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 sufalse
nel file di progetto dell'app:<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 sufalse
:<PropertyGroup> <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData> </PropertyGroup>