best practices voor ASP.NET Core Blazor-prestaties
Notitie
Dit is niet de nieuwste versie van dit artikel. Zie de .NET 9-versie van dit artikelvoor de huidige release.
Waarschuwing
Deze versie van ASP.NET Core wordt niet meer ondersteund. Zie de .NET- en .NET Core-ondersteuningsbeleidvoor meer informatie. Zie de .NET 9-versie van dit artikelvoor de huidige release.
Belangrijk
Deze informatie heeft betrekking op een pre-releaseproduct dat aanzienlijk kan worden gewijzigd voordat het commercieel wordt uitgebracht. Microsoft geeft geen garanties, uitdrukkelijk of impliciet, met betrekking tot de informatie die hier wordt verstrekt.
Zie de .NET 9-versie van dit artikelvoor de huidige release.
Blazor is geoptimaliseerd voor hoge prestaties in de meeste realistische gebruikersinterfacescenario's voor toepassingen. De beste prestaties zijn echter afhankelijk van ontwikkelaars die de juiste patronen en functies gebruiken.
Notitie
De codevoorbeelden in dit artikel gebruiken nullable reference types (NRT's) en .NET compiler null-state statische analyse, die worden ondersteund in ASP.NET Core in .NET 6 of hoger.
Renderingsnelheid optimaliseren
Optimaliseer de renderingsnelheid om de renderingworkload te minimaliseren en de reactiesnelheid van de gebruikersinterface te verbeteren. Dit kan leiden tot een tienvoudige of hogere verbetering in de weergavesnelheid van de gebruikersinterface.
Vermijd onnodige weergave van onderdeelsubstructuren
Mogelijk kunt u het merendeel van de weergavekosten van een ouderonderdeel verminderen door de herweergave van onderdelen van kindcomponenten over te slaan bij een gebeurtenis. Je hoeft je alleen zorgen te maken over het overslaan van de hertekeningsafdelingen die bijzonder veel rekenkracht vereisen en die UI-vertraging veroorzaken.
Tijdens runtime bestaan onderdelen in een hiërarchie. Een hoofdonderdeel (het eerste geladen onderdeel) bevat onderliggende onderdelen. De kinderen van de wortel hebben hun eigen kindcomponenten, en zo verder. Wanneer een gebeurtenis plaatsvindt, zoals een gebruiker die een knop selecteert, bepaalt het volgende proces welke onderdelen opnieuw moeten worden gebruikt:
- De gebeurtenis wordt gestuurd naar de component die de gebeurtenishandler heeft gerenderd. Nadat de evenementhandler is uitgevoerd, wordt het onderdeel hergerenderd.
- Wanneer een onderdeel opnieuw wordt gebruikt, wordt er een nieuwe kopie van parameterwaarden aan elk van de onderliggende onderdelen geleverd.
- Nadat een nieuwe set parameterwaarden is ontvangen, bepaalt elk onderdeel of het opnieuw moet worden uitgevoerd. Onderdelen worden opnieuw uitgevoerd als de parameterwaarden kunnen zijn gewijzigd, bijvoorbeeld als ze veranderlijke objecten zijn.
De laatste twee stappen van de voorgaande reeks worden recursief voortgezet binnen de componenthiërarchie. In veel gevallen wordt de hele subboom hergerenderd. Gebeurtenissen die gericht zijn op onderdelen op hoog niveau kunnen dure rerendering veroorzaken, omdat elk onderdeel onder het onderdeel op hoog niveau opnieuw moet worden uitgevoerd.
Gebruik een van de volgende benaderingen om recursie in een bepaalde substructuur te voorkomen:
- Zorg ervoor dat de parameters van onderliggende componenten primitieve onveranderbare typen zijn, zoals
string
,int
,bool
,DateTime
en andere vergelijkbare typen. De ingebouwde logica voor het detecteren van wijzigingen slaat automatisch rerendering over als de primitieve onveranderbare parameterwaarden niet zijn gewijzigd. Als u een onderliggend onderdeel met<Customer CustomerId="item.CustomerId" />
weergeeft, waarbijCustomerId
eenint
type is, wordt hetCustomer
onderdeel niet gewijzigd, tenzijitem.CustomerId
wijzigingen aanbrengt. - Overschrijven ShouldRender:
- Als u niet-primieve parameterwaarden wilt accepteren, zoals complexe aangepaste modeltypen, gebeurtenis-callbacks of RenderFragment waarden.
- Als u een UI-alleen onderdeel maakt dat niet verandert na de eerste weergave, ongeacht of de parameterwaarde verandert.
In het volgende voorbeeld van een vluchtzoekprogramma voor luchtvaartmaatschappijen worden privévelden gebruikt om de benodigde informatie bij te houden om wijzigingen te detecteren. De vorige binnenkomende vlucht-id (prevInboundFlightId
) en de vorige uitgaande vlucht-id (prevOutboundFlightId
) houden informatie bij voor de volgende mogelijke componentupdate. Als een van de flight-id's verandert wanneer de parameters van het onderdeel zijn ingesteld in OnParametersSet
, wordt het onderdeel opnieuw gebruikt omdat shouldRender
is ingesteld op true
. Als shouldRender
na de controle van de vlucht-id's overeenkomt met false
, wordt een dure rerender vermeden.
@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;
}
Een gebeurtenis-handler kan ook shouldRender
instellen op true
. Voor de meeste onderdelen is het bepalen van rerendering op het niveau van afzonderlijke gebeurtenis-handlers meestal niet nodig.
Zie de volgende bronnen voor meer informatie:
Virtualisatie
Bij het weergeven van grote hoeveelheden gebruikersinterface binnen een lus, bijvoorbeeld een lijst of raster met duizenden vermeldingen, kan het grote aantal renderingbewerkingen leiden tot vertraging in de rendering van de gebruikersinterface. Aangezien de gebruiker slechts een klein aantal elementen tegelijk kan zien zonder te schuiven, is het vaak verspilling om tijd te besteden aan het weergeven van elementen die momenteel niet zichtbaar zijn.
Blazor biedt het Virtualize<TItem> onderdeel om het uiterlijk en schuifgedrag van een willekeurig grote lijst te maken, terwijl alleen de lijstitems worden weergegeven die zich in de huidige schuifweergavepoort bevinden. Een onderdeel kan bijvoorbeeld een lijst met 100.000 vermeldingen weergeven, maar alleen de renderingkosten betalen van 20 items die zichtbaar zijn.
Zie ASP.NET Core Razor componentvirtualisatievoor meer informatie.
Lichtgewicht, geoptimaliseerde onderdelen maken
De meeste Razor onderdelen vereisen geen agressieve optimalisatie-inspanningen omdat de meeste onderdelen niet worden herhaald in de gebruikersinterface en niet met hoge frequentie opnieuw worden uitgevoerd. Routeerbare onderdelen met een @page
-instructie en onderdelen die worden gebruikt om onderdelen van de gebruikersinterface op hoog niveau weer te geven, zoals dialoogvensters of formulieren, worden waarschijnlijk slechts één voor één weergegeven en worden alleen opnieuw uitgevoerd als reactie op een gebruikersbeweging. Deze onderdelen maken meestal geen hoge renderingworkload, dus u kunt vrijelijk elke combinatie van frameworkfuncties gebruiken zonder dat u veel zorgen hoeft te maken over renderingprestaties.
Er zijn echter veelvoorkomende scenario's waarbij onderdelen op schaal worden herhaald en vaak leiden tot slechte ui-prestaties:
- Grote geneste formulieren met honderden afzonderlijke elementen, zoals invoervelden of labelteksten.
- Rasters met honderden rijen of duizenden cellen.
- Spreidingsdiagrammen met miljoenen gegevenspunten.
Als u elk element, elke cel of elk gegevenspunt modelleert als een afzonderlijk onderdeelexemplaar, zijn er vaak zoveel dat de renderingprestaties cruciaal zijn. In deze sectie vindt u advies over het lichtgewicht maken van dergelijke onderdelen, zodat de gebruikersinterface snel en responsief blijft.
Vermijd duizenden instantiaties van onderdelen
Elk onderdeel is een afzonderlijk eiland dat onafhankelijk van zijn ouders en kinderen kan worden weergegeven. Door te kiezen hoe u de gebruikersinterface splitst in een hiërarchie van onderdelen, neemt u de controle over de granulariteit van ui-rendering. Dit kan leiden tot goede of slechte prestaties.
Door de gebruikersinterface te splitsen in afzonderlijke onderdelen, kunt u kleinere delen van de gebruikersinterface opnieuw genereren wanneer er gebeurtenissen plaatsvinden. In een tabel met veel rijen met een knop in elke rij kunt u mogelijk slechts één rij opnieuw maken met behulp van een onderliggend onderdeel in plaats van de hele pagina of tabel. Elk onderdeel vereist echter extra geheugen- en CPU-overhead om te kunnen omgaan met de onafhankelijke status en renderinglevenscyclus.
In een test die is uitgevoerd door de technici van de ASP.NET Core-producteenheid, werd een rendering-overhead van ongeveer 0,06 ms per componentinstantie waargenomen bij een Blazor WebAssembly-app. De test-app heeft een eenvoudig onderdeel weergegeven dat drie parameters accepteert. Intern is de overhead grotendeels te wijten aan het ophalen van de status per onderdeel uit woordenlijsten en het doorgeven en ontvangen van parameters. Door te vermenigvuldigen kunt u zien dat het toevoegen van 2000 extra onderdeelexemplaren 0,12 seconden zou toevoegen aan de renderingtijd en dat de gebruikersinterface langzaam zou beginnen te voelen voor gebruikers.
Het is mogelijk om onderdelen lichter te maken, zodat u er meer van kunt hebben. Een krachtigere techniek is echter vaak om te voorkomen dat zoveel onderdelen worden weergegeven. In de volgende secties worden twee benaderingen beschreven die u kunt gebruiken.
Zie voor meer informatie over het beheer van geheugen Host en implementeer ASP.NET Core-Blazor-apps.
Inline onderliggende onderdelen in hun ouders
Overweeg het volgende gedeelte van een oudercomponent dat subcomponenten in een lus weergeeft:
<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; }
}
Het voorgaande voorbeeld presteert goed als duizenden berichten niet tegelijk worden weergegeven. Als u duizenden berichten tegelijk wilt weergeven, kunt u overwegen niet het afzonderlijke ChatMessageDisplay
onderdeel uit te factoren. In plaats daarvan, plaats het kindonderdeel inline in de bovenliggende component. De volgende aanpak voorkomt de overhead per component van het weergeven van zoveel subcomponenten, ten koste van de mogelijkheid om de markering van elk subcomponent onafhankelijk opnieuw te renderen.
<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>
De herbruikbare RenderFragments
definiëren in code
Het kan zijn dat u subcomponenten uitfactort als een manier om renderlogica opnieuw te gebruiken. Als dat het geval is, kunt u herbruikbare renderinglogica maken zonder extra onderdelen te implementeren. Definieer een RenderFragmentin het @code
blok van een onderdeel. Render het fragment vanuit elke locatie zo vaak als nodig is.
@RenderWelcomeInfo
<p>Render the welcome content a second time:</p>
@RenderWelcomeInfo
@code {
private RenderFragment RenderWelcomeInfo = @<p>Welcome to your new app!</p>;
}
Als u RenderTreeBuilder code herbruikbaar wilt maken voor meerdere onderdelen, declareert u de RenderFragmentpublic
en static
:
public static RenderFragment SayHello = @<h1>Hello!</h1>;
SayHello
in het voorgaande voorbeeld kan worden aangeroepen vanuit een niet-gerelateerd onderdeel. Deze techniek is handig voor het bouwen van bibliotheken met herbruikbare markeringsfragmenten die worden weergegeven zonder overhead per onderdeel.
RenderFragment delegeren kunnen parameters accepteren. Het volgende onderdeel geeft het bericht (message
) door aan de RenderFragment gedelegeerde:
<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>;
}
De voorgaande benadering hergebruikt renderinglogica zonder overhead per onderdeel. De methode staat echter niet toe dat de substructuur van de gebruikersinterface onafhankelijk wordt vernieuwd, noch heeft deze de mogelijkheid om de substructuur van de gebruikersinterface over te slaan wanneer het bovenliggende element wordt weergegeven omdat er geen onderdeelgrens is. Toewijzing aan een RenderFragment gedelegeerde wordt alleen ondersteund in Razor onderdeelbestanden (.razor
).
Gebruik voor een niet-statisch veld, een methode of een eigenschap waarnaar niet kan worden verwezen door een initialisatiefunctie voor velden, zoals TitleTemplate
in het volgende voorbeeld, een eigenschap in plaats van een veld voor de RenderFragment:
protected RenderFragment DisplayTitle =>
@<div>
@TitleTemplate
</div>;
Niet te veel parameters ontvangen
Als een onderdeel extreem vaak wordt herhaald, bijvoorbeeld honderden of duizenden keren, wordt de overhead van het doorgeven en ontvangen van elke parameter opgebouwd.
Het is zeldzaam dat te veel parameters de prestaties ernstig beperken, maar dit kan een factor zijn. Voor een TableCell
-onderdeel dat 4000 keer binnen een raster weergeeft, voegt elke parameter die aan het onderdeel wordt doorgegeven ongeveer 15 ms toe aan de totale renderingkosten. Het doorgeven van tien parameters vereist ongeveer 150 ms en veroorzaakt een vertraging in de weergave van de gebruikersinterface.
Als u de belasting van de parameter wilt verminderen, bundelt u meerdere parameters in een aangepaste klasse. Een tabelcelonderdeel kan bijvoorbeeld een gemeenschappelijk object accepteren. In het volgende voorbeeld is Data
voor elke cel anders, maar Options
is gebruikelijk in alle celexemplaren:
@typeparam TItem
...
@code {
[Parameter]
public TItem? Data { get; set; }
[Parameter]
public GridOptions? Options { get; set; }
}
Houd er echter rekening mee dat het bundelen van primitieve parameters in een klasse niet altijd een voordeel is. Hoewel het aantal parameters kan verminderen, heeft dit ook invloed op de werking van wijzigingsdetectie en -rendering. Het doorgeven van niet-primitieve parameters activeert altijd een herweergave, omdat Blazor niet kan weten of willekeurige objecten intern veranderlijk zijn, terwijl het doorgeven van primitieve parameters alleen een herweergave activeert als de waarden daadwerkelijk zijn gewijzigd.
Houd er ook rekening mee dat het mogelijk een verbetering is om geen tabelcelonderdeel te hebben, zoals wordt weergegeven in het vorige voorbeeld, en in plaats daarvan inline de logica ervan in het bovenliggende onderdeel.
Notitie
Wanneer er meerdere benaderingen beschikbaar zijn voor het verbeteren van de prestaties, is benchmarking van de benaderingen meestal vereist om te bepalen welke benadering de beste resultaten oplevert.
Zie de volgende bronnen voor meer informatie over algemene typeparameters (@typeparam
):
- Razor syntaxisreferentie voor ASP.NET Core-
- ASP.NET Core Razor-onderdelen
- ASP.NET Core Blazor sjabloononderdelen
Zorg ervoor dat trapsgewijze parameters zijn vastgezet
Het CascadingValue
-onderdeel heeft een optionele IsFixed
parameter:
- Als
IsFixed
isfalse
(de standaardinstelling), stelt elke ontvanger van de trapsgewijze waarde een abonnement in om wijzigingsmeldingen te ontvangen. Elke[CascadingParameter]
is veel duurder dan een gewone[Parameter]
vanwege het abonnementsbeheer. - Als
IsFixed
istrue
(bijvoorbeeld<CascadingValue Value="someValue" IsFixed="true">
), ontvangen ontvangers de oorspronkelijke waarde, maar stellen ze geen abonnement in om updates te ontvangen. Elke[CascadingParameter]
is lichtgewicht en is niet duurder dan een gewone[Parameter]
.
Als u IsFixed
instelt op true
verbetert u de prestaties als er een groot aantal andere onderdelen is die de trapsgewijze waarde ontvangen. Stel IsFixed
waar mogelijk in op true
voor cascadewaarden. U kunt IsFixed
instellen op true
wanneer de opgegeven waarde na verloop van tijd niet verandert.
Wanneer een onderdeel this
als trapsgewijze waarde doorgeeft, kan IsFixed
ook worden ingesteld op true
, omdat this
nooit verandert tijdens de levenscyclus van het onderdeel:
<CascadingValue Value="this" IsFixed="true">
<SomeOtherComponents>
</CascadingValue>
Zie ASP.NET Core Blazor trapsgewijze waarden en parametersvoor meer informatie.
Vermijd het verspreiden van attributen met CaptureUnmatchedValues
Onderdelen kunnen ervoor kiezen om niet-overeenkomende parameterwaarden te ontvangen met behulp van de vlag CaptureUnmatchedValues:
<div @attributes="OtherAttributes">...</div>
@code {
[Parameter(CaptureUnmatchedValues = true)]
public IDictionary<string, object>? OtherAttributes { get; set; }
}
Met deze benadering kunnen willekeurige extra kenmerken aan het element worden doorgegeven. Deze benadering is echter duur omdat de renderer het volgende moet doen:
- Koppel alle opgegeven parameters aan de set bekende parameters om een woordenlijst te maken.
- Houd bij hoe meerdere exemplaren van hetzelfde kenmerk elkaar overschrijven.
Gebruik CaptureUnmatchedValues waarbij de prestaties van onderdeelweergave niet kritiek zijn, zoals onderdelen die niet regelmatig worden herhaald. Voor onderdelen die op schaal worden weergegeven, zoals elk item in een grote lijst of in de cellen van een raster, probeert u kenmerksplatting te voorkomen.
Voor meer informatie, zie ASP.NET Core Blazor attribute splatting en willekeurige parameters.
Handmatig SetParametersAsync
implementeren
Een belangrijke bron van overhead per onderdeel is het schrijven van binnenkomende parameterwaarden in [Parameter]
-eigenschappen. De renderer gebruikt reflectie om de parameterwaarden te schrijven, wat kan leiden tot slechte prestaties bij grootschalig gebruik.
In sommige extreme gevallen wilt u mogelijk de weerspiegeling vermijden en uw eigen logica voor parameters handmatig implementeren. Dit kan van toepassing zijn wanneer:
- Een onderdeel wordt extreem vaak weergegeven, bijvoorbeeld wanneer er honderden of duizenden kopieën van het onderdeel in de gebruikersinterface zijn.
- Een onderdeel accepteert veel parameters.
- U vindt dat de overhead van ontvangende parameters een waarneembare invloed heeft op de reactiesnelheid van de gebruikersinterface.
In extreme gevallen kunt u de methode voor virtuele SetParametersAsync van het onderdeel overschrijven en uw eigen componentspecifieke logica implementeren. In het volgende voorbeeld worden woordenlijstzoekacties opzettelijk vermeden:
@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);
}
}
In de voorgaande code wordt de basisklasse SetParametersAsync geretourneerd, waardoor de normale levenscyclusmethode wordt uitgevoerd zonder opnieuw parameters toe te wijzen.
Zoals u in de voorgaande code kunt zien, is het overschrijven van SetParametersAsync en het opgeven van aangepaste logica ingewikkeld en arbeidsintensief, dus we raden u over het algemeen niet aan deze benadering te gebruiken. In extreme gevallen kunt u de renderingprestaties verbeteren met 20-25%, maar u moet deze benadering alleen overwegen in de extreme scenario's die eerder in deze sectie zijn vermeld.
Niet te snel gebeurtenissen activeren
Sommige browser-gebeurtenissen worden extreem vaak geactiveerd.
onmousemove
en onscroll
kunnen bijvoorbeeld tientallen of honderden keren per seconde worden geactiveerd. In de meeste gevallen hoeft u de gebruikersinterface niet zo vaak bij te werken. Als gebeurtenissen te snel worden geactiveerd, kan dit de reactietijd van de gebruikersinterface schaden of overmatige CPU-tijd verbruiken.
In plaats van systeemeigen gebeurtenissen te gebruiken die snel worden geactiveerd, kunt u het gebruik van JS interop overwegen om een callback te registreren die minder vaak wordt geactiveerd. In het volgende onderdeel wordt bijvoorbeeld de positie van de muis weergegeven, maar wordt slechts één keer per 500 ms bijgewerkt:
@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();
}
De bijbehorende JavaScript-code registreert de DOM-gebeurtenislistener voor muisbewegingen. In dit voorbeeld gebruikt de gebeurtenislistener de throttle
-functie van Lodash om de frequentie van aanroepen te beperken:
<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>
Voorkom rerendering na het afhandelen van gebeurtenissen zonder statuswijzigingen
Onderdelen nemen over van ComponentBase, die automatisch StateHasChanged aanroept nadat de gebeurtenis-handlers van het onderdeel zijn aangeroepen. In sommige gevallen kan het onnodig of ongewenst zijn om een rerender te activeren nadat een gebeurtenis-handler is aangeroepen. Een gebeurtenis-handler kan bijvoorbeeld de onderdeelstatus niet wijzigen. In deze scenario's kan de app gebruikmaken van de IHandleEvent-interface om het gedrag van de gebeurtenisafhandeling van Blazorte beheren.
Notitie
De benadering in deze sectie geleidt geen uitzonderingen door naar foutrandvoorwaarden. Zie voor meer informatie en demonstratiecode die foutgrenzen ondersteunt door ComponentBase.DispatchExceptionAsyncaan te roepen, AsNonRenderingEventHandler + ErrorBoundary = onverwacht gedrag (dotnet/aspnetcore
#54543).
Als u rerenders voor alle gebeurtenis-handlers van een onderdeel wilt voorkomen, implementeert u IHandleEvent en geeft u een IHandleEvent.HandleEventAsync taak op die de gebeurtenis-handler aanroept zonder StateHasChangedaan te roepen.
In het volgende voorbeeld zorgt geen enkele aan het onderdeel toegevoegde evenementhandler ervoor dat er een rerender plaatsvindt, zodat HandleSelect
geen rerender veroorzaakt wanneer het wordt aangeroepen.
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);
}
Naast het voorkomen van rerenders nadat gebeurtenishandlers globaal in een component worden geactiveerd, is het mogelijk om rerenders na een enkele gebeurtenishandler te voorkomen door de volgende hulpmethode te gebruiken.
Voeg de volgende EventUtil
-klasse toe aan een Blazor-app. De statische acties en functies boven aan de klasse EventUtil
bieden handlers die betrekking hebben op verschillende combinaties van argumenten en retourtypen die Blazor gebruikt bij het verwerken van gebeurtenissen.
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);
}
}
Roep EventUtil.AsNonRenderingEventHandler
aan om een gebeurtenishandler aan te roepen die geen render activeert wanneer deze wordt aangeroepen.
In het volgende voorbeeld:
- Als u de eerste knop selecteert, die
HandleClick1
oproept, wordt een her-rendering geactiveerd. - Bij het selecteren van de tweede knop, die
HandleClick2
aanroept, wordt er geen opnieuw weergeven geactiveerd. - Door de derde knop te selecteren, die
HandleClick3
aanroept, wordt er geen nieuwe weergave geactiveerd en worden de gebeurtenisargumenten gebruikt (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);
}
}
Naast het implementeren van de IHandleEvent-interface, kan het gebruik van de andere aanbevolen procedures die in dit artikel worden beschreven, ook helpen bij het verminderen van ongewenste renders nadat gebeurtenissen zijn verwerkt. Het overschrijven van ShouldRender in onderliggende componenten van de doelcomponent kan bijvoorbeeld worden gebruikt om het opnieuw renderen te beheren.
Vermijd het opnieuw maken van gedelegeerden voor veel herhaalde elementen of onderdelen
Blazor's recreatie van lambda-expressie delegeren voor elementen of onderdelen in een lus kan leiden tot slechte prestaties.
Het volgende component, dat wordt getoond in het artikel over gebeurtenisafhandeling, rendert een set knoppen. Elke knop wijst een gedelegeerde toe aan de bijbehorende @onclick
gebeurtenis. Dit is prima als er niet veel knoppen zijn om weer te geven.
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}";
}
}
Als een groot aantal knoppen wordt weergegeven met behulp van de voorgaande benadering, wordt de renderingsnelheid nadelig beïnvloed, wat leidt tot een slechte gebruikerservaring. Als u een groot aantal knoppen met een callback voor klikgebeurtenissen wilt weergeven, gebruikt het volgende voorbeeld een verzameling knopobjecten die de @onclick
gedelegeerde van elke knop aan een Actiontoewijzen. De volgende aanpak vereist niet dat Blazor alle knopfuncties opnieuw maakt elke keer dat de knoppen worden weergegeven.
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 => { };
}
}
JavaScript-interopsnelheid optimaliseren
Voor aanroepen tussen .NET en JavaScript is extra overhead vereist, omdat:
- Aanroepen zijn asynchroon.
- Parameters en retourwaarden zijn JSON-geëncodeerd om een gemakkelijk te begrijpen conversiemechanisme te bieden tussen .NET en JavaScript types.
Daarnaast worden deze aanroepen doorgegeven via het netwerk voor Blazor-apps aan de serverzijde.
Vermijd overmatig fijnmazige aanroepen
Aangezien elke oproep enige overhead omvat, kan het waardevol zijn om het aantal oproepen te verminderen. Houd rekening met de volgende code, waarin een verzameling items in de localStorage
van de browser wordt opgeslagen:
private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
foreach (var item in items)
{
await JS.InvokeVoidAsync("localStorage.setItem", item.Id,
JsonSerializer.Serialize(item));
}
}
In het voorgaande voorbeeld wordt voor elk item een afzonderlijke JS interop-aanroep gedaan. In plaats daarvan vermindert de volgende benadering de JS interoperabiliteit tot één aanroep:
private async Task StoreAllInLocalStorage(IEnumerable<TodoItem> items)
{
await JS.InvokeVoidAsync("storeAllInLocalStorage", items);
}
De bijbehorende JavaScript-functie slaat de hele verzameling items op de client op:
function storeAllInLocalStorage(items) {
items.forEach(item => {
localStorage.setItem(item.id, JSON.stringify(item));
});
}
Voor Blazor WebAssembly apps verbetert het rollen van afzonderlijke JS interop-aanroepen in één aanroep meestal alleen de prestaties aanzienlijk als het onderdeel een groot aantal JS interop-aanroepen doet.
Overweeg het gebruik van synchrone aanroepen
JavaScript aanroepen vanuit .NET
Deze sectie is alleen van toepassing op onderdelen aan de clientzijde.
De JS interop-aanroepen zijn asynchroon, ongeacht of de aangeroepen code synchroon of asynchroon is. Aanroepen zijn asynchroon om ervoor te zorgen dat onderdelen compatibel zijn met de rendermodi aan de serverzijde en clientzijde. Op de server moeten alle JS interop-aanroepen asynchroon zijn omdat ze via een netwerkverbinding worden verzonden.
Als u zeker weet dat uw onderdeel alleen wordt uitgevoerd op WebAssembly, kunt u ervoor kiezen om synchrone JS interop-aanroepen uit te voeren. Dit heeft iets minder overhead dan het maken van asynchrone aanroepen en kan leiden tot minder rendercycli, omdat er geen tussenliggende status is tijdens het wachten op resultaten.
Als u een synchrone aanroep wilt maken van .NET naar JavaScript in een onderdeel aan de clientzijde, cast IJSRuntime naar IJSInProcessRuntime om de JS interop-aanroep te maken:
@inject IJSRuntime JS
...
@code {
protected override void HandleSomeEvent()
{
var jsInProcess = (IJSInProcessRuntime)JS;
var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
}
}
Wanneer u met IJSObjectReference werkt in ASP.NET Core 5.0- of hoger-onderdelen aan de clientzijde, kunt u in plaats daarvan IJSInProcessObjectReference synchroon gebruiken. IJSInProcessObjectReference implementeert IAsyncDisposable/IDisposable en moet worden verwijderd voor garbagecollection om een geheugenlek te voorkomen, zoals in het volgende voorbeeld wordt gedemonstreerd:
@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();
}
}
}
In het voorgaande voorbeeld wordt een JSDisconnectedException niet gevangen tijdens de ontmanteling van de module omdat er geen Blazor-SignalR circuit in een Blazor WebAssembly-app is die verloren kunnen gaan. Zie ASP.NET Core Blazor JavaScript-interoperabiliteit (JS interop)voor meer informatie.
.NET aanroepen vanuit JavaScript
Deze sectie is alleen van toepassing op onderdelen aan de clientzijde.
Interop-aanroepen van JS zijn asynchroon, ongeacht of de aangeroepen code synchroon of asynchroon is. Aanroepen zijn asynchroon om ervoor te zorgen dat onderdelen compatibel zijn met de rendermodi aan de serverzijde en clientzijde. Op de server moeten alle JS interop-aanroepen asynchroon zijn omdat ze via een netwerkverbinding worden verzonden.
Als u zeker weet dat uw onderdeel alleen wordt uitgevoerd op WebAssembly, kunt u ervoor kiezen om synchrone JS interop-aanroepen uit te voeren. Dit heeft iets minder overhead dan het maken van asynchrone aanroepen en kan leiden tot minder rendercycli, omdat er geen tussenliggende status is tijdens het wachten op resultaten.
Gebruik DotNet.invokeMethod
in plaats van DotNet.invokeMethodAsync
om een synchrone aanroep van JavaScript naar .NET te maken in een onderdeel aan de clientzijde.
Synchrone aanroepen werken als:
Deze sectie is alleen van toepassing op onderdelen aan de clientzijde.
JS interop-aanroepen asynchroon zijn, ongeacht of de aangeroepen code synchroon of asynchroon is. Aanroepen zijn asynchroon om ervoor te zorgen dat onderdelen compatibel zijn met de rendermodi aan de serverzijde en clientzijde. Op de server moeten alle JS interop-aanroepen asynchroon zijn omdat ze via een netwerkverbinding worden verzonden.
Als u zeker weet dat uw onderdeel alleen wordt uitgevoerd op WebAssembly, kunt u ervoor kiezen om synchrone JS interop-aanroepen uit te voeren. Dit heeft iets minder overhead dan het maken van asynchrone aanroepen en kan leiden tot minder rendercycli, omdat er geen tussenliggende status is tijdens het wachten op resultaten.
Als u een synchrone aanroep wilt maken van .NET naar JavaScript in een onderdeel aan de clientzijde, cast IJSRuntime naar IJSInProcessRuntime om de JS interop-aanroep te maken:
@inject IJSRuntime JS
...
@code {
protected override void HandleSomeEvent()
{
var jsInProcess = (IJSInProcessRuntime)JS;
var value = jsInProcess.Invoke<string>("javascriptFunctionIdentifier");
}
}
Wanneer u met IJSObjectReference werkt in ASP.NET Core 5.0- of hoger-onderdelen aan de clientzijde, kunt u in plaats daarvan IJSInProcessObjectReference synchroon gebruiken. IJSInProcessObjectReference implementeert IAsyncDisposable/IDisposable en moet worden vrijgegeven voor vuilnisophaling om een geheugenlek te voorkomen, zoals het volgende voorbeeld aantoont.
@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();
}
}
}
In het voorgaande voorbeeld wordt een JSDisconnectedException niet gevangen tijdens het verwijderen van de module omdat er geen Blazor-SignalR-circuit in een Blazor WebAssembly-app is om kwijt te raken. Zie ASP.NET Core Blazor JavaScript-interoperabiliteit (JS interop)voor meer informatie.
Overweeg het gebruik van niet-gemarshallede aanroepen
Deze sectie is alleen van toepassing op Blazor WebAssembly apps.
Wanneer u op Blazor WebAssemblyuitvoert, is het mogelijk om niet-gemarshalleerde aanroepen te maken van .NET naar JavaScript. Dit zijn synchrone aanroepen die geen JSON-serialisatie van argumenten of retourwaarden uitvoeren. Alle aspecten van geheugenbeheer en vertalingen tussen .NET- en JavaScript-weergaven worden aan de ontwikkelaar overgelaten.
Waarschuwing
Hoewel het gebruik van IJSUnmarshalledRuntime de minste overhead betekent vergeleken met de JS interop-benaderingen, zijn de JavaScript-API's die nodig zijn voor interactie met deze API's momenteel niet gedocumenteerd en kunnen ze onderhevig zijn aan ingrijpende wijzigingen in toekomstige releases.
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");
}
}
JavaScript-[JSImport]
/[JSExport]
-interop gebruiken
JavaScript [JSImport]
/[JSExport]
interop voor Blazor WebAssembly-apps biedt verbeterde prestaties en stabiliteit ten opzichte van de JS interop-API in frameworkreleases vóór ASP.NET Core in .NET 7.
Zie JavaScript JSImport/JSExport-interop met ASP.NET Core Blazorvoor meer informatie.
AOT-compilatie (Ahead-Of-Time)
De compilatie van AOT (Ahead-Of-Time) compileert de .NET-code van een Blazor-app rechtstreeks in systeemeigen WebAssembly voor directe uitvoering door de browser. Door AOT gecompileerde apps resulteren in grotere apps die langer duren om te downloaden, maar AOT-gecompileerde apps bieden meestal betere runtimeprestaties, met name voor apps die CPU-intensieve taken uitvoeren. Zie voor meer informatie ASP.NET Core Blazor WebAssembly build tools en ahead-of-time (AOT) compilatie.
Downloadgrootte voor apps minimaliseren
Runtime opnieuw koppelen
Zie ASP.NET Core Blazor WebAssembly build tools en ahead-of-time (AOT) compilatie, voor informatie over hoe runtime herkoppeling de downloadgrootte van een app minimaliseert.
Gebruik System.Text.Json
Blazor's JS interop-implementatie is afhankelijk van System.Text.Json, een krachtige JSON-serialisatiebibliotheek met weinig geheugentoewijzing. Als u System.Text.Json gebruikt, zou dat niet moeten resulteren in een extra belastinggrootte van de app in vergelijking met het toevoegen van een of meer alternatieve JSON-bibliotheken.
Zie Migreren van Newtonsoft.Json
naar System.Text.Json
voor hulp bij migratie.
Tussenliggende taal (IL) inkorten
Deze sectie is alleen van toepassing op Blazor scenario's aan de clientzijde.
Door ongebruikte assembly's uit een Blazor WebAssembly-app te beperken, wordt de grootte van de app verkleind door ongebruikte code in de binaire bestanden van de app te verwijderen. Zie De trimmer configureren voor ASP.NET Core Blazorvoor meer informatie.
Het koppelen van een Blazor WebAssembly-app verkleint de grootte van de app door ongebruikte code in de binaire bestanden van de app te verwijderen. De Tussenliggende taal (IL) Linker is alleen ingeschakeld bij het bouwen in configuratie Release
. Om hiervan te profiteren, publiceert u de app voor implementatie met behulp van de opdracht dotnet publish
met de optie -c|--configuration ingesteld op Release
:
dotnet publish -c Release
Luie laadsystemen assemblages
Deze sectie is alleen van toepassing op Blazor scenario's aan de clientzijde.
Laad assembly's tijdens runtime wanneer de assembly's vereist zijn voor een route. Zie Lazy load assembly's in ASP.NET Core Blazor WebAssemblyvoor meer informatie.
Compressie
Deze sectie is alleen van toepassing op Blazor WebAssembly apps.
Wanneer een Blazor WebAssembly-app wordt gepubliceerd, wordt de uitvoer statisch gecomprimeerd tijdens het publiceren om de grootte van de app te verminderen en de overhead voor runtimecompressie te verwijderen. Blazor is afhankelijk van de server om inhoudsonderhandeling uit te voeren en statisch gecomprimeerde bestanden te leveren.
Nadat een app is geïmplementeerd, controleert u of de app gecomprimeerde bestanden verwerkt. Controleer het tabblad Network in de ontwikkelhulpprogramma's van een browser en controleer of de bestanden worden geleverd met Content-Encoding: br
(Brotli-compressie) of Content-Encoding: gz
(Gzip-compressie). Als de host geen gecomprimeerde bestanden verwerkt, volgt u de instructies in Host en implementeert u ASP.NET Core Blazor WebAssembly.
Ongebruikte functies uitschakelen
Deze sectie is alleen van toepassing op Blazor scenario's aan de clientzijde.
Blazor WebAssemblyruntime bevat de volgende .NET-functies die kunnen worden uitgeschakeld voor een kleinere nettoladinggrootte:
-
Blazor WebAssembly beschikt over globalisatiebronnen die nodig zijn om waarden, zoals datums en valuta, weer te geven in de cultuur van de gebruiker. Als de app geen lokalisatie vereist, kunt u de app configureren ter ondersteuning van de invariante cultuur, die is gebaseerd op de
en-US
cultuur.
Het aannemen van invariante globalisering resulteert alleen in het gebruik van niet-gelokaliseerde tijdzonenamen. Als u tijdzonecode en gegevens uit de app wilt bijsnijden, past u de eigenschap
<InvariantTimezone>
MSBuild toe met een waarde vantrue
in het projectbestand van de app:<PropertyGroup> <InvariantTimezone>true</InvariantTimezone> </PropertyGroup>
Notitie
<BlazorEnableTimeZoneSupport>
overschrijft de eerdere<InvariantTimezone>
-instelling. U wordt aangeraden de<BlazorEnableTimeZoneSupport>
-instelling te verwijderen.
Er wordt een gegevensbestand opgenomen om tijdzonegegevens correct te maken. Als deze functie niet is vereist voor de app, kunt u deze uitschakelen door de eigenschap
<BlazorEnableTimeZoneSupport>
MSBuild in te stellen opfalse
in het projectbestand van de app:<PropertyGroup> <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport> </PropertyGroup>
Sorteringsgegevens worden opgenomen om API's zoals StringComparison.InvariantCultureIgnoreCase correct te laten werken. Als u zeker weet dat de app de sorteringsgegevens niet nodig heeft, kunt u overwegen deze uit te schakelen door de eigenschap
BlazorWebAssemblyPreserveCollationData
MSBuild in het projectbestand van de app in te stellen opfalse
:<PropertyGroup> <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData> </PropertyGroup>