Delen via


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:

  1. De gebeurtenis wordt gestuurd naar de component die de gebeurtenishandler heeft gerenderd. Nadat de evenementhandler is uitgevoerd, wordt het onderdeel hergerenderd.
  2. Wanneer een onderdeel opnieuw wordt gebruikt, wordt er een nieuwe kopie van parameterwaarden aan elk van de onderliggende onderdelen geleverd.
  3. 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, DateTimeen 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, waarbij CustomerId een int type is, wordt het Customer onderdeel niet gewijzigd, tenzij item.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):

Zorg ervoor dat trapsgewijze parameters zijn vastgezet

Het CascadingValue-onderdeel heeft een optionele IsFixed parameter:

  • Als IsFixed is false (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 is true (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 HandleClick1oproept, wordt een her-rendering geactiveerd.
  • Bij het selecteren van de tweede knop, die HandleClick2aanroept, wordt er geen opnieuw weergeven geactiveerd.
  • Door de derde knop te selecteren, die HandleClick3aanroept, 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 localStoragevan 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.invokeMethodAsyncom een synchrone aanroep van JavaScript naar .NET te maken in een onderdeel aan de clientzijde.

Synchrone aanroepen werken als:

  • Het onderdeel wordt alleen weergegeven voor uitvoering op WebAssembly.
  • De aangeroepen functie retourneert een waarde synchroon. De functie is geen async methode en retourneert geen .NET-Task of JavaScript-Promise.

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.Jsonvoor 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 van true 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 op false 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 op false:

    <PropertyGroup>
      <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
    </PropertyGroup>