Dela via


bästa praxis för ASP.NET Core Blazor-prestanda

Not

Det här är inte den senaste versionen av den här artikeln. Den aktuella versionen finns i den .NET 9-versionen av den här artikeln.

Varning

Den här versionen av ASP.NET Core stöds inte längre. Mer information finns i .NET och .NET Core Support Policy. Den aktuella versionen finns i .NET 9-versionen av denna artikel.

Viktig

Den här informationen gäller en förhandsversionsprodukt som kan ändras avsevärt innan den släpps kommersiellt. Microsoft lämnar inga garantier, uttryckliga eller underförstådda, med avseende på den information som tillhandahålls här.

Den aktuella versionen finns i den .NET 9-versionen av den här artikeln.

Blazor är optimerad för höga prestanda i de flesta realistiska scenarier för programgränssnittet. Den bästa prestandan beror dock på att utvecklare använder rätt mönster och funktioner.

Notera

Kodexemplen i den här artikeln använder nullbara referenstyper (NRT) och .NET-kompilatorn null-state static analysis, som stöds i ASP.NET Core i .NET 6 eller senare.

Optimera återgivningshastigheten

Optimera återgivningshastigheten för att minimera renderingsarbetsbelastningen och förbättra användargränssnittsresponsiviteten, vilket kan ge en tiofaldig eller högre förbättring i UI-återgivningshastigheten.

Undvik onödig rendering av komponentunderträd

Du kanske kan ta bort största delen av renderingskostnaden för en överordnad komponent genom att undvika återrenderingen av de underordnade elementen när en händelse inträffar. Du bör endast vara orolig för att undvika att rendera om underträd som är särskilt kostsamma att rendera och orsakar användargränssnittslagg.

Vid körning är komponenter organiserade i en hierarki. En rotkomponent (den första komponenten som lästs in) har delkomponenter. Rotens barn har i sin tur sina egna barnkomponenter och så vidare. När en händelse inträffar, till exempel när en användare väljer en knapp, avgör följande process vilka komponenter som ska återskapas:

  1. Händelsen skickas till komponenten som renderade händelsens hanterare. När händelsehanteraren har körts återskapas komponenten.
  2. När en komponent återskapas, tillhandahåller den en ny kopia av parametervärdena till var och en av dess underordnade komponenter.
  3. När en ny uppsättning parametervärden har tagits emot avgör varje komponent om de ska återskapas. Komponenterna återskapas om parametervärdena kan ha ändrats, till exempel om de är föränderliga objekt.

De två sista stegen i föregående sekvens fortsätter rekursivt nedåt i komponenthierarkin. I många fall renderas hela underträdet om. Händelser som riktar sig till komponenter på hög nivå kan orsaka dyr rerendering eftersom varje komponent under komponenten på hög nivå måste återskapas.

Om du vill förhindra återrekursion till ett visst underträd använder du någon av följande metoder:

  • Se till att parametrar för barnkomponenter är av primitiva oföränderliga typer, som till exempel string, int, bool, DateTimeoch andra liknande typer. Den inbyggda logiken för att identifiera ändringar hoppar automatiskt över rerendering om de primitiva oföränderliga parametervärdena inte har ändrats. Om en underordnad komponent renderas med <Customer CustomerId="item.CustomerId" />, där CustomerId är av typen int, så renderas inte Customer-komponenten om inte item.CustomerId förändras.
  • Åsidosätt ShouldRender:
    • Att acceptera värden för icke-primitiva parametrar, till exempel komplexa anpassade modelltyper, händelseåterrop eller värden av typen RenderFragment.
    • Om du redigerar en komponent med endast användargränssnitt som inte ändras efter den första återgivningen, oavsett parametervärdeändringar.

I följande exempel på flygsökningsverktyget används privata fält för att spåra nödvändig information för att identifiera ändringar. Den tidigare inkommande flygidentifieraren (prevInboundFlightId) och den tidigare utgående flygidentifieraren (prevOutboundFlightId) spårar information för nästa potentiella komponentuppdatering. Om någon av flygidentifierarna ändras när komponentens parametrar anges i OnParametersSet, hämtas komponenten igen eftersom shouldRender är inställd på true. Om shouldRender utvärderas till false efter kontroll av flygidentifierarna undviks en dyr rerender:

@code {
    private int prevInboundFlightId = 0;
    private int prevOutboundFlightId = 0;
    private bool shouldRender;

    [Parameter]
    public FlightInfo? InboundFlight { get; set; }

    [Parameter]
    public FlightInfo? OutboundFlight { get; set; }

    protected override void OnParametersSet()
    {
        shouldRender = InboundFlight?.FlightId != prevInboundFlightId
            || OutboundFlight?.FlightId != prevOutboundFlightId;

        prevInboundFlightId = InboundFlight?.FlightId ?? 0;
        prevOutboundFlightId = OutboundFlight?.FlightId ?? 0;
    }

    protected override bool ShouldRender() => shouldRender;
}

En händelsehanterare kan också ange shouldRender till true. För de flesta komponenter är det vanligtvis inte nödvändigt att bestämma omrendering på nivån för enskilda händelsehanterare.

Mer information finns i följande resurser:

Virtualisering

När du återger stora mängder användargränssnitt i en loop, till exempel en lista eller ett rutnät med tusentals poster, kan den stora mängden renderingsåtgärder leda till en fördröjning i användargränssnittsrendering. Med tanke på att användaren bara kan se ett litet antal element samtidigt utan att rulla, är det ofta slöseri att ägna tid åt att återge element som för närvarande inte är synliga.

Blazor tillhandahåller den Virtualize<TItem>-komponenten för att skapa utseendet och rullningsbeteendena för en godtyckligt stor lista samtidigt som endast de listobjekt som finns i den aktuella rullningsvyporten renderas. En komponent kan till exempel återge en lista med 100 000 poster men bara betala renderingskostnaden för 20 objekt som är synliga.

Mer information finns i ASP.NET Core Razor-komponentvirtualisering.

Skapa enkla, optimerade komponenter

De flesta Razor komponenter kräver inte aggressiva optimeringsåtgärder eftersom de flesta komponenter inte upprepas i användargränssnittet och inte rerenderar med hög frekvens. Routbara komponenter med ett @page-direktiv och komponenter som används för att återge delar av användargränssnittet på hög nivå, till exempel dialogrutor eller formulär, visas troligen bara en i taget och endast återskapas som svar på en användargest. Dessa komponenter skapar vanligtvis inte hög återgivningsbelastning, så du kan fritt använda valfri kombination av ramverkets funktioner utan att behöva oroa dig för prestandan.

Det finns dock vanliga scenarier där komponenter upprepas i stor skala och ofta resulterar i dåliga användargränssnittsprestanda:

  • Stora kapslade formulär med hundratals enskilda element, till exempel indata eller etiketter.
  • Rutnät med hundratals rader eller tusentals celler.
  • Punktdiagram med miljontals datapunkter.

Om du modellerar varje element, cell eller datapunkt som en separat komponentinstans finns det ofta så många av dem att deras renderingsprestanda blir kritisk. Det här avsnittet innehåller råd om hur du gör sådana komponenter lätta så att användargränssnittet förblir snabbt och dynamiskt.

Undvik tusentals komponentinstanser

Varje komponent är en separat ö som kan återges oberoende av sina föräldrar och barn. Genom att välja hur användargränssnittet ska delas upp i en hierarki med komponenter tar du kontroll över detaljrikedomen för UI-återgivning. Detta kan resultera i antingen bra eller dåliga prestanda.

Genom att dela upp användargränssnittet i separata komponenter kan mindre delar av användargränssnittet rendreras om när händelser inträffar. I en tabell med många rader som har en knapp i varje rad kanske du bara kan ha den enda radrerendern med hjälp av en underordnad komponent i stället för hela sidan eller tabellen. Varje komponent kräver dock ytterligare minne och processorkostnader för att hantera dess oberoende tillstånd och återgivningslivscykel.

I ett test som utfördes av ASP.NET Core-produktenhetstekniker sågs en renderingskostnad på cirka 0,06 ms per komponentinstans i en Blazor WebAssembly app. Testappen renderade en enkel komponent som accepterar tre parametrar. Internt beror omkostnaderna till stor del på att tillståndet per komponent hämtas från ordlistor och att parametrar skickas och tas emot. Med multiplikation kan du se att lägga till 2 000 extra komponentinstanser skulle lägga till 0,12 sekunder till återgivningstiden och att användargränssnittet skulle börja kännas långsamt för användarna.

Det är möjligt att göra komponenterna enklare så att du kan få fler av dem. En mer kraftfull teknik är dock ofta att undvika att ha så många komponenter att rendera. I följande avsnitt beskrivs två metoder som du kan använda.

Mer information om minneshantering finns i Värd och distribuera ASP.NET Core-Blazor-appar på serversidan.

Infoga barnkomponenter i sina föräldrar

Överväg följande del av en överordnad komponent som renderar underordnade komponenter i en loop:

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

Föregående exempel fungerar bra om tusentals meddelanden inte visas samtidigt. För att visa tusentals meddelanden samtidigt, överväg i stället för att bortse från den separata ChatMessageDisplay-komponenten. Infoga i stället den underordnade komponenten i den överordnade komponenten. Följande metod undviker omkostnaderna per komponent för att återge så många underordnade komponenter på bekostnad av att förlora möjligheten att återskapa varje underordnad komponents markering oberoende av varandra:

<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>
Definiera återanvändbara RenderFragments i kod

Kanske bryter du ut barnkomponenter bara för att återanvända renderingslogik. I så fall kan du skapa återanvändbar renderingslogik utan att implementera ytterligare komponenter. I varje komponents @code-block definierar du en RenderFragment. Rendera fragmentet från valfri plats så många gånger som det behövs:

@RenderWelcomeInfo

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

@RenderWelcomeInfo

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

Om du vill göra RenderTreeBuilder kod återanvändbar för flera komponenter deklarerar du RenderFragmentpublic och static:

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

SayHello i föregående exempel kan anropas från en orelaterad komponent. Den här tekniken är användbar för att skapa bibliotek med återanvändbara markeringsfragment som renderas utan omkostnader per komponent.

RenderFragment ombud kan acceptera parametrar. Följande komponent skickar meddelandet (message) till RenderFragment ombudet:

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

Föregående metod återanvänder renderingslogik utan omkostnader per komponent. Metoden tillåter dock inte uppdatering av underträdet i användargränssnittet oberoende av varandra, och den har inte heller möjlighet att hoppa över återgivningen av underträdet i användargränssnittet när dess överordnade renderas eftersom det inte finns någon komponentgräns. Tilldelning till en RenderFragment delegat stöds endast i Razor komponentfiler (.razor).

För ett icke-statiskt fält, en metod eller en egenskap som inte kan refereras till av en fältinitierare, till exempel TitleTemplate i följande exempel, använder du en egenskap i stället för ett fält för RenderFragment:

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

Ta inte emot för många parametrar

Om en komponent upprepas mycket ofta, till exempel hundratals eller tusentals gånger, byggs omkostnaderna för att skicka och ta emot varje parameter upp.

Det är ovanligt att för många parametrar kraftigt begränsar prestanda, men det kan vara en faktor. För en TableCell komponent som renderar 4 000 gånger i ett rutnät lägger varje parameter som skickas till komponenten till cirka 15 ms i den totala renderingskostnaden. Att skicka tio parametrar kräver cirka 150 ms och orsakar en UI-återgivningsfördröjning.

Om du vill minska parameterbelastningen paketar du flera parametrar i en anpassad klass. En tabellcellskomponent kan till exempel acceptera ett gemensamt objekt. I följande exempel är Data olika för varje cell, men Options är vanligt för alla cellinstanser:

@typeparam TItem

...

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

Kom dock ihåg att paketering av primitiva parametrar i en klass inte alltid är en fördel. Även om det kan minska antalet parametrar påverkar det också hur ändringsidentifiering och återgivning fungerar. Att skicka icke-primitiva parametrar utlöser alltid en återåtergivning, eftersom Blazor inte kan veta om godtyckliga objekt har internt föränderligt tillstånd, medan överföring av primitiva parametrar bara utlöser en återåtergivning om deras värden faktiskt har ändrats.

Tänk också på att det kan vara en förbättring att inte ha en tabellcellskomponent, som du ser i föregående exempel, och i stället infoga logiken i den överordnade komponenten.

Notera

När flera metoder är tillgängliga för att förbättra prestandan krävs vanligtvis benchmarking av metoderna för att avgöra vilken metod som ger bäst resultat.

Mer information om generiska typparametrar (@typeparam) finns i följande resurser:

Se till att sammanhängande parametrar är fasta

Komponenten CascadingValue har en valfri parameter IsFixed:

  • Om IsFixed är false (standard) konfigurerar varje mottagare av det överlappande värdet en prenumeration för att ta emot ändringsmeddelanden. Varje [CascadingParameter] är betydligt dyrare än en vanlig [Parameter] på grund av prenumerationsspårningen.
  • Om IsFixed är true (till exempel <CascadingValue Value="someValue" IsFixed="true">) får mottagarna det ursprungliga värdet men konfigurerar inte en prenumeration för att ta emot uppdateringar. Varje [CascadingParameter] är lätt och inte dyrare än en vanlig [Parameter].

Om du anger IsFixed till true förbättras prestandan om det finns ett stort antal andra komponenter som tar emot det överlappande värdet. Ange om möjligt IsFixed till true på överlappande värden. Du kan ange IsFixed till true när det angivna värdet inte ändras över tid.

När en komponent skickar this som ett överlappande värde kan IsFixed också anges till trueeftersom this aldrig ändras under komponentens livscykel:

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

Mer information finns i ASP.NET Core Blazor sammanhängande värden och parametrar.

Undvik attributsplätering med CaptureUnmatchedValues

Komponenter kan välja att ta emot "omatchade" parametervärden med hjälp av flaggan CaptureUnmatchedValues:

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

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

Med den här metoden kan du skicka godtyckliga ytterligare attribut till elementet. Den här metoden är dock dyr eftersom renderaren måste:

  • Matcha alla angivna parametrar mot uppsättningen kända parametrar för att skapa en ordlista.
  • Håll reda på hur flera kopior av samma attribut skriver över varandra.

Använd CaptureUnmatchedValues där komponentrenderingsprestanda inte är kritiska, till exempel komponenter som inte upprepas ofta. För komponenter som renderas i stor skala, till exempel varje objekt i en stor lista eller i cellerna i ett rutnät, försöker du undvika attributsplattning.

Mer information finns i ASP.NET Core Blazor attribute splatting och godtyckliga parametrar.

Implementera SetParametersAsync manuellt

En betydande källa till omkostnader för rendering per komponent är att skriva de inkommande parametervärdena till [Parameter]-egenskaper. Renderaren använder reflektion för att skriva parametervärdena, vilket kan leda till dåliga prestanda i stor skala.

I vissa extrema fall kanske du vill undvika reflektionen och implementera din egen parameterinställningslogik manuellt. Detta kan vara tillämpligt när:

  • En komponent återges mycket ofta, till exempel när det finns hundratals eller tusentals kopior av komponenten i användargränssnittet.
  • En komponent accepterar många parametrar.
  • Du upptäcker att omkostnaderna för att ta emot parametrar har en observerbar inverkan på UI-svarstiden.

I extrema fall kan du åsidosätta komponentens virtuella SetParametersAsync-metod och implementera din egen komponentspecifika logik. I följande exempel undviks avsiktligt ordlistesökningar:

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

I den föregående koden, genom att returnera basklassen SetParametersAsync, kommer den normala livscykelmetoden att köras utan att parametrarna tilldelas igen.

Som du ser i föregående kod är det komplicerat och besvärligt att åsidosätta SetParametersAsync och tillhandahålla anpassad logik, så vi rekommenderar vanligtvis inte att du använder den här metoden. I extrema fall kan det förbättra renderingsprestanda med 20–25%, men du bör bara överväga den här metoden i de extrema scenarier som anges tidigare i det här avsnittet.

Utlös inte händelser för snabbt

Vissa webbläsarhändelser utlöses väldigt ofta. Till exempel kan onmousemove och onscroll utlösas tiotals eller hundratals gånger per sekund. I de flesta fall behöver du inte utföra användargränssnittsuppdateringar ofta. Om händelser utlöses för snabbt kan du skada UI-svarstiden eller förbruka för hög CPU-tid.

I stället för att använda inbyggda händelser som snabbt utlöses bör du överväga att använda JS interop för att registrera ett återanrop som utlöses mindre ofta. Följande komponent visar till exempel musens position men uppdateras bara högst en gång var 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();
}

Motsvarande JavaScript-kod registrerar DOM-händelselyssnaren för musrörelse. I det här exemplet använder händelselyssnaren Lodashs throttle funktion för att begränsa antalet anrop:

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

Undvik att rendera om efter att ha hanterat händelser utan ändringar av tillståndet

Komponenter ärver från ComponentBase, som automatiskt anropar StateHasChanged efter att komponentens händelsehanterare har anropats. I vissa fall kan det vara onödigt eller oönskat att utlösa en rerender när en händelsehanterare anropas. En händelsehanterare kanske till exempel inte ändrar komponenttillståndet. I dessa scenarier kan appen använda IHandleEvent-gränssnittet för att styra beteendet för Blazorhändelsehantering.

Obs

Metoden i det här avsnittet hanterar inte undantag inom felgränser. För mer information och demonstrationskod som stöder felgränser genom att anropa ComponentBase.DispatchExceptionAsync, se AsNonRenderingEventHandler + ErrorBoundary = oväntat beteende (dotnet/aspnetcore #54543).

För att förhindra omritningar för alla händelsehanterare i en komponent, implementera IHandleEvent och tillhandahåll ett IHandleEvent.HandleEventAsync-uppdrag som anropar händelsehanteraren utan att anropa StateHasChanged.

I följande exempel utlöser ingen händelsehanterare som läggs till komponenten en rerender, så HandleSelect resulterar inte i en rerender när den anropas.

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

Förutom att förhindra omrenderingar efter att en händelsehanterare har körts globalt, är det möjligt att förhindra omrenderingar efter en enskild händelsehanterare genom att använda följande metod.

Lägg till följande EventUtil-klass i en Blazor-app. Statiska åtgärder och funktioner överst i klassen EventUtil tillhandahåller hanterare som omfattar flera kombinationer av argument och returtyper som Blazor använder vid hantering av händelser.

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

Anropa EventUtil.AsNonRenderingEventHandler för att anropa en händelsehanterare som inte utlöser en återgivning när den anropas.

I följande exempel:

  • Om du väljer den första knappen, som anropar HandleClick1, utlöses en rerender.
  • Att välja den andra knappen, som triggar HandleClick2, initierar ingen omrendering.
  • Om du väljer den tredje knappen, som anropar HandleClick3, utlöses inte en rerender och använder händelseargument (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);
    }
}

Förutom att implementera IHandleEvent-gränssnittet kan användning av andra metodtips som beskrivs i den här artikeln också bidra till att minska oönskade återgivningar när händelser har hanterats. Till exempel kan att åsidosätta ShouldRender i målkomponentens underordnade komponenter användas för att styra omrendering.

Undvik att återskapa ombud för många upprepade element eller komponenter

Blazorrekreation av lambda uttryck delegerar för element eller komponenter i en loop kan leda till dålig prestanda.

Följande komponent som visas i artikeln händelsehantering renderar en uppsättning knappar. Varje knapp tilldelar en delegerad till sin @onclick-händelse, vilket är bra om det inte finns många knappar att rendera.

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

Om ett stort antal knappar återges med föregående metod påverkas återgivningshastigheten negativt, vilket leder till en dålig användarupplevelse. För att rendera ett stort antal knappar med ett återanrop för klickhändelser, använder följande exempel en samling av knappobjekt som tilldelar varje knapps @onclick-delegat till dess Action-delegat. Följande metod kräver inte Blazor för att återskapa alla knappdelegater varje gång knapparna återges:

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

Optimera JavaScript-interophastighet

Anrop mellan .NET och JavaScript kräver ytterligare omkostnader eftersom:

  • Anrop är asynkrona.
  • Parametrar och returvärden är JSON-serialiserade för att ge en lättförstårlig konverteringsmekanism mellan .NET- och JavaScript-typer.

Dessutom skickas dessa anrop över nätverket för Blazor appar på serversidan.

Undvik alltför detaljerade samtal

Eftersom varje anrop medför vissa omkostnader kan det vara värdefullt att minska antalet anrop. Tänk på följande kod som lagrar en samling objekt i webbläsarens localStorage:

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

I föregående exempel görs ett separat JS interop-anrop för varje objekt. I stället minskar följande metod JS interop till ett enda anrop:

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

Motsvarande JavaScript-funktion lagrar hela samlingen med objekt på klienten:

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

För Blazor WebAssembly appar förbättras prestanda avsevärt genom att slå samman enskilda JS interop-anrop till ett enda anrop vanligtvis endast om komponenten gör ett stort antal JS interop-anrop.

Överväg att använda synkrona anrop

Anropa JavaScript från .NET

Det här avsnittet gäller endast komponenter på klientsidan.

JS interop-anrop är asynkrona, oavsett om den anropade koden är synkron eller asynkron. Anrop är asynkrona för att säkerställa att komponenterna är kompatibla i återgivningslägen på serversidan och på klientsidan. På servern måste alla JS interop-anrop vara asynkrona eftersom de skickas via en nätverksanslutning.

Om du vet att komponenten bara körs på WebAssembly kan du välja att göra synkrona JS interop-anrop. Detta har något mindre omkostnader än att göra asynkrona anrop och kan resultera i färre återgivningscykler eftersom det inte finns något mellanliggande tillstånd i väntan på resultat.

Om du vill göra ett synkront anrop från .NET till JavaScript i en komponent på klientsidan, konvertera IJSRuntime till IJSInProcessRuntime för att göra interop-anropet JS:

@inject IJSRuntime JS

...

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

När du arbetar med IJSObjectReference i ASP.NET Core 5.0 eller senare komponenter på klientsidan kan du använda IJSInProcessObjectReference synkront i stället. IJSInProcessObjectReference implementerar IAsyncDisposable/IDisposable och ska tas bort för skräpinsamling för att förhindra en minnesläcka, vilket visas i följande exempel:

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

I föregående exempel fångas en JSDisconnectedException inte under modulavveckling eftersom det inte finns någon Blazor–SignalR-krets i en Blazor WebAssembly-app som går förlorad. Mer information finns i ASP.NET Core Blazor JavaScript-samverkan (JS interop).

Anropa .NET från JavaScript

Det här avsnittet gäller endast komponenter på klientsidan.

JS interop-anrop är asynkrona, oavsett om den anropade koden är synkron eller asynkron. Anrop är asynkrona för att säkerställa att komponenterna är kompatibla i återgivningslägen på serversidan och på klientsidan. På servern måste alla JS interop-anrop vara asynkrona eftersom de skickas via en nätverksanslutning.

Om du vet att komponenten bara körs på WebAssembly kan du välja att göra synkrona JS interop-anrop. Detta har något mindre omkostnader än att göra asynkrona anrop och kan resultera i färre återgivningscykler eftersom det inte finns något mellanliggande tillstånd i väntan på resultat.

Om du vill göra ett synkront anrop från JavaScript till .NET i en komponent på klientsidan använder du DotNet.invokeMethod i stället för DotNet.invokeMethodAsync.

Synkrona anrop fungerar om:

  • Komponenten utfördes endast på WebAssembly.
  • Den anropade funktionen returnerar ett värde synkront. Funktionen är inte en async-metod och returnerar inte en .NET-Task eller JavaScript-Promise.

Det här avsnittet gäller endast komponenter på klientsidan.

JS interop-anrop är asynkrona, oavsett om den anropade koden är synkron eller asynkron. Anrop är asynkrona för att säkerställa att komponenterna är kompatibla i återgivningslägen på serversidan och på klientsidan. På servern måste alla JS interop-anrop vara asynkrona eftersom de skickas via en nätverksanslutning.

Om du vet att komponenten bara körs på WebAssembly kan du välja att göra synkrona JS interop-anrop. Detta har något mindre omkostnader än att göra asynkrona anrop och kan resultera i färre återgivningscykler eftersom det inte finns något mellanliggande tillstånd i väntan på resultat.

Om du vill göra ett synkront anrop från .NET till JavaScript, i en komponent på klientsidan, kastar du IJSRuntime till IJSInProcessRuntime för att kunna göra ett JS interop-anrop.

@inject IJSRuntime JS

...

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

När du arbetar med IJSObjectReference i ASP.NET Core 5.0 eller senare komponenter på klientsidan kan du använda IJSInProcessObjectReference synkront i stället. IJSInProcessObjectReference implementerar IAsyncDisposable/IDisposable och ska tas bort för skräpinsamling för att förhindra en minnesläcka, vilket visas i följande exempel:

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

I det föregående exemplet fångas inte en JSDisconnectedException under modulhantering eftersom det inte finns någon Blazor–SignalR-krets i en Blazor WebAssembly-applikation som kan gå förlorad. Mer information finns i ASP.NET Core Blazor JavaScript-samverkan (JS interop).

Överväg att använda oformatterade anrop

Det här avsnittet gäller endast för Blazor WebAssembly appar.

När du kör på Blazor WebAssemblygår det att göra ohörda anrop från .NET till JavaScript. Det här är synkrona anrop som inte utför JSON-serialisering av argument eller returvärden. Alla aspekter av minneshantering och översättningar mellan .NET- och JavaScript-representationer är upp till utvecklaren.

Varning

Även om användningen av IJSUnmarshalledRuntime har minst omkostnader av JS interop-lösningar, är de JavaScript-API:er som krävs för att interagera med dessa API:er för närvarande odokumenterade och kan komma att ändras i framtida versioner.

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

Använd JavaScript [JSImport]/[JSExport] interop

JavaScript-[JSImport]/[JSExport] interop för Blazor WebAssembly-appar ger bättre prestanda och stabilitet över JS interop-API:et i ramverksversioner före ASP.NET Core i .NET 7.

Mer information finns i JavaScript JSImport/JSExport interop med ASP.NET Core Blazor.

AOT-kompilering

AOT-kompilering i förväg kompilerar en Blazor-appens .NET-kod direkt till den interna WebAssembly för direktkörning av webbläsaren. AOT-kompilerade appar resulterar i större appar som tar längre tid att ladda ned, men AOT-kompilerade appar ger vanligtvis bättre körningsprestanda, särskilt för appar som kör processorintensiva uppgifter. Mer information finns i ASP.NET Core Blazor WebAssembly byggverktyg och ahead-of-time (AOT) kompilering.

Minimera appens nedladdningsstorlek

Omlänkning vid körtid

Information om hur relinkning vid körning minimerar en appens nedladdningsstorlek finns i ASP.NET Core Blazor WebAssembly bygghjälpmedel och kompilering i förväg (AOT).

Använd System.Text.Json

Blazor JS interop-implementering förlitar sig på System.Text.Json, vilket är ett JSON-serialiseringsbibliotek med höga prestanda med låg minnesallokering. Att använda System.Text.Json bör inte resultera i ytterligare appnyttolaststorlek jämfört med att lägga till ett eller flera alternativa JSON-bibliotek.

Information om migrering finns i Så här migrerar du från Newtonsoft.Json till System.Text.Json.

Trimning av mellanliggande språk (IL)

Det här avsnittet gäller endast för scenarier på klientsidan Blazor.

Om du trimmar oanvända sammansättningar från en Blazor WebAssembly app minskar appens storlek genom att ta bort oanvänd kod i appens binärfiler. Mer information finns i Konfigurera Trimmer för ASP.NET Core Blazor.

Att länka en Blazor WebAssembly app minskar appens storlek genom att trimma oanvänd kod i appens binärfiler. IL-länkaren (Intermediate Language) aktiveras bara när du bygger i konfigurationen Release. Om du vill dra nytta av detta publicerar du appen för distribution med hjälp av kommandot dotnet publish med alternativet -c|--configuration inställt på Release:

dotnet publish -c Release

Lazy load-sammansättningar

Det här avsnittet gäller endast för scenarier på klientsidan Blazor.

Läs in sammansättningar vid körning när sammansättningarna krävs av en väg. Mer information finns i Lazy Load-sammansättningar i ASP.NET Core Blazor WebAssembly.

Komprimering

Det här avsnittet gäller endast för Blazor WebAssembly appar.

När en Blazor WebAssembly app publiceras komprimeras utdata statiskt vid publicering för att minska appens storlek och ta bort överbelastning vid körningskomprimering. Blazor förlitar sig på servern för att utföra innehållsförhandling och hantera statiskt komprimerade filer.

När en app har distribuerats kontrollerar du att appen hanterar komprimerade filer. Granska fliken Network i en webbläsares utvecklarverktyg och kontrollera att filerna hanteras med Content-Encoding: br (Brotli-komprimering) eller Content-Encoding: gz (Gzip-komprimering). Om värden inte hanterar komprimerade filer följer du anvisningarna i Host och distribuerar ASP.NET Core Blazor WebAssembly.

Inaktivera oanvända funktioner

Det här avsnittet gäller endast för scenarier på klientsidan Blazor.

Blazor WebAssembly's runtime innehåller följande .NET-funktioner som kan inaktiveras för en mindre nyttolaststorlek:

  • Att införa invariant globalisering resulterar bara i att icke-lokaliserade tidszonsnamn används. Om du vill trimma tidszonskod och data från appen använder du egenskapen <InvariantTimezone> MSBuild med värdet true i appens projektfil:

    <PropertyGroup>
      <InvariantTimezone>true</InvariantTimezone>
    </PropertyGroup>
    

    Notera

    <BlazorEnableTimeZoneSupport> åsidosätter en tidigare <InvariantTimezone> inställning. Vi rekommenderar att du tar bort inställningen <BlazorEnableTimeZoneSupport>.

  • En datafil ingår för att göra tidszonsinformationen korrekt. Om appen inte kräver den här funktionen kan du inaktivera den genom att ange egenskapen <BlazorEnableTimeZoneSupport> MSBuild till false i appens projektfil:

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • Sorteringsinformation ingår för att få API:er som StringComparison.InvariantCultureIgnoreCase att fungera korrekt. Om du är säker på att appen inte kräver sorteringsdata kan du inaktivera den genom att ange egenskapen BlazorWebAssemblyPreserveCollationData MSBuild i appens projektfil till false:

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