Best Practices zur Blazor-Leistung in ASP.NET Core

Hinweis

Dies ist nicht die neueste Version dieses Artikels. Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

Wichtig

Diese Informationen beziehen sich auf ein Vorabversionsprodukt, das vor der kommerziellen Freigabe möglicherweise noch wesentlichen Änderungen unterliegt. Microsoft gibt keine Garantie, weder ausdrücklich noch impliziert, hinsichtlich der hier bereitgestellten Informationen.

Informationen zum aktuellen Release finden Sie in der .NET 8-Version dieses Artikels.

Blazor ist für Hochleistung in den meisten realistischen Anwendungs-UI-Szenarien optimiert. Die beste Leistung hängt jedoch davon ab, dass Entwickler die richtigen Muster und Funktionen verwenden.

Hinweis

Die Codebeispiele in diesem Artikel verwenden Nullwerte zulassende Verweistypen (Nullable Reference Types, NRTs) und die statische Analyse des NULL-Zustands des .NET-Compilers, die in ASP.NET Core in .NET 6 oder höher unterstützt werden.

Optimieren der Renderinggeschwindigkeit

Optimieren Sie die Renderinggeschwindigkeit, um den Renderingaufwand zu minimieren und die Reaktionsfähigkeit der Benutzeroberfläche zu verbessern, was zu einer zehnfachen oder noch größeren Verbesserung der Renderinggeschwindigkeit der Benutzeroberfläche führen kann.

Vermeiden von unnötigem Rendering der Komponentenunterstrukturen

Möglicherweise können Sie einen Großteil der Renderingkosten für eine übergeordnete Komponente vermeiden, indem Sie das erneute Rendern von Teilstrukturen untergeordneter Komponenten beim Auftreten eines Ereignisses überspringen. Sie sollten das Überspringen des erneuten Renderns nur für Unterstrukturen in Erwägung ziehen, für die das Rendern besonders aufwändig ist und in der Benutzeroberfläche Verzögerungen verursacht.

Zur Laufzeit sind Komponenten in einer Hierarchie vorhanden. Eine Stammkomponente (die erste geladene Komponente) verfügt über untergeordnete Komponenten. Die untergeordneten Elemente des Stamms verfügen wiederum über eigene untergeordnete Komponenten usw. Wenn ein Ereignis auftritt, z. B. ein Benutzer eine Schaltfläche auswählt, entscheidet der folgende Prozess, welche Komponenten erneut gerendert werden:

  1. Das Ereignis wird an die Komponente gesendet, die den Handler des Ereignisses gerendert hat. Nach Ausführung des Ereignishandlers wird die Komponente erneut gerendert.
  2. Wenn eine Komponente erneut gerendert wird, wird eine neue Kopie der Parameterwerte für jede ihrer untergeordneten Komponenten bereitgestellt.
  3. Nach Empfang eines neuen Satzes von Parameterwerten entscheidet jede Komponente, ob sie erneut gerendert werden muss. Standardmäßig werden Komponenten erneut gerendert, wenn sich die Parameterwerte geändert haben (wenn sie z. B. veränderbare Objekte sind).

Die letzten beiden Schritte der oben beschriebenen Sequenz werden rekursiv die Komponentenhierarchie hinab fortgesetzt. In vielen Fällen wird die gesamte Unterstruktur erneut gerendert. Ereignisse, die auf Komponenten auf hoher Ebene abzielen, können teures erneutes Rendering verursachen, da jede Komponente unterhalb der Komponente auf hoher Ebene neu gerendert werden muss.

Um eine Renderingrekursion in einer bestimmten Teilstruktur zu verhindern, verwenden Sie einen der folgenden Ansätze:

  • Stellen Sie sicher, dass die Parameter der untergeordneten Komponenten primitive, unveränderliche Typen sind, etwa string, int, bool, DateTime und andere ähnliche Typen. Die integrierte Logik zur Erkennung von Änderungen überspringt automatisch das erneute Rendern, wenn sich die primitiven unveränderlichen Parameterwerte nicht geändert haben. Wenn Sie eine untergeordnete Komponente mit <Customer CustomerId="@item.CustomerId" /> rendern, wobei CustomerId ein int-Typ ist, wird die Customer-Komponente nicht neu gerendert, es sei denn, item.CustomerId ändert sich.
  • Überschreiben Sie ShouldRender:
    • Um nicht-primitive Parameterwerte zu akzeptieren, z. B. komplexe benutzerdefinierte Modelltypen, Ereignisrückrufe oder RenderFragment-Werte.
    • Wenn Sie eine reine UI-Komponente erstellen, die sich nach dem ersten Rendern nicht mehr ändert, unabhängig von Änderungen der Parameterwerte.

Im folgenden Beispiel für die Flugsuche einer Fluggesellschaft werden private Felder verwendet, um die erforderlichen Informationen zum Erkennen von Änderungen nachzuverfolgen. Der vorherige Bezeichner für eingehende Flüge (prevInboundFlightId) und der vorherige Bezeichner für ausgehende Flüge (prevOutboundFlightId) enthalten Informationen zur nächsten potenziellen Aktualisierung der Komponente. Ändert sich einer der Flugbezeichner, wenn die Parameter der Komponente in OnParametersSet festgelegt sind, wird die Komponente erneut gerendert, da shouldRender auf true festgelegt ist. Wenn shouldRender nach dem Überprüfen der Flugbezeichner in false ausgewertet wird, wird ein teures erneutes Rendern vermieden:

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

Ein Ereignishandler kann auch shouldRender auf true festlegen. Für die meisten Komponenten ist es in der Regel nicht erforderlich, das erneute Rendern auf der Ebene der einzelnen Ereignishandler zu bestimmen.

Weitere Informationen finden Sie in den folgenden Ressourcen:

Virtualisierung

Wenn große Teile der Benutzeroberfläche in einer Schleife gerendert werden, z. B. Listen oder Raster mit Tausenden von Einträgen, kann die schiere Menge an Renderingvorgängen zu einer Verzögerung beim Rendern der Benutzeroberfläche führen. Da der Benutzer ohne Scrollen nur eine kleine Anzahl von Elementen gleichzeitig sehen kann, ist es oft Verschwendung, Zeit in das Rendern von Elementen zu investieren, die derzeit nicht sichtbar sind.

Blazor stellt die Komponente Virtualize<TItem> bereit, die die Darstellung und das Scrollverhalten eine Liste mit beliebiger Größe erstellt, aber nur die Listenelemente rendert, die sich derzeit im Anzeigebereich befinden. Eine Komponente kann beispielsweise eine Liste mit 100.000 Einträgen darstellen, aber nur die Rendering-Kosten für 20 sichtbare Einträge tragen.

Weitere Informationen finden Sie unter Razor-Komponentenvirtualisierung in ASP.NET Core.

Erstellen von einfachen optimierten Komponenten

Die meisten Razor-Komponenten erfordern keine aggressiven Optimierungsanstrengungen, da sich die meisten Komponenten in der Benutzeroberfläche nicht wiederholen und nicht sehr häufig gerendert werden. Zum Beispiel werden routbare Komponenten mit einer @page-Anweisung und Komponenten, die zum Rendern von Teilen der Benutzeroberfläche auf hoher Ebene verwendet werden (z. B. Dialogfelder oder Formulare), höchstwahrscheinlich nur einzeln angezeigt und nur als Reaktion auf eine Benutzergeste neu gerendert. Diese Komponenten verursachen in der Regel keine hohe Renderinglast, sodass Sie jede beliebige Kombination von Frameworkfunktionen verwenden können, ohne sich große Sorgen um die Renderingleistung machen zu müssen.

Es gibt jedoch häufige Szenarien, in denen sich Komponenten in großem Umfang wiederholen und oft zu einer schlechten Leistung der Benutzeroberfläche führen:

  • Große geschachtelte Formulare mit Hunderten von einzelnen Elementen, z. B. Eingaben oder Bezeichnungen.
  • Raster mit Hunderten von Zeilen oder Tausenden von Zellen.
  • Streudiagramme mit Millionen von Datenpunkten.

Wenn jedes Element, jede Zelle oder jeder Datenpunkt als separate Komponenteninstanz modelliert wird, gibt es oft so viele davon, dass ihre Renderingleistung kritisch wird. In diesem Abschnitt wird erläutert, wie Sie solche Komponenten vereinfachen, sodass die Benutzeroberfläche schnell und reaktionsfähig bleibt.

Vermeiden von Tausenden von Komponenteninstanzen

Jede Komponente ist eine separate Insel, die unabhängig von ihren übergeordneten und untergeordneten Elementen gerendert werden kann. Indem Sie auswählen, wie die Benutzeroberfläche in eine Hierarchie von Komponenten aufgeteilt werden soll, bestimmen Sie die Granularität des Renderings der Benutzeroberfläche. Dies kann zu guter oder schlechter Leistung führen.

Wenn Sie die Benutzeroberfläche in separate Komponenten aufteilen, können kleinere Teile der Benutzeroberfläche erneut gerendert werden, wenn Ereignisse auftreten. In einer Tabelle mit vielen Zeilen, die in jeder Zeile eine Schaltfläche enthalten, können Sie möglicherweise nur diese eine Zeile erneut rendern lassen, indem Sie eine untergeordnete Komponente anstelle der gesamten Seite oder Tabelle verwenden. Jede Komponente erfordert jedoch zusätzlichen Speicher- und CPU-Mehraufwand, um ihren unabhängigen Status und ihren Renderinglebenszyklus zu verwalten.

In einem von den ASP.NET Core-Produktingenieuren durchgeführten Test wurde ein Renderingmehraufwand von etwa 0,06 ms pro Komponenteninstanz in einer Blazor WebAssembly-App festgestellt. Die Test-App hat eine einfache Komponente gerendert, die drei Parameter akzeptiert. Intern entsteht der Mehraufwand größtenteils durch das Abrufen des Status pro Komponente aus Wörterbüchern und Übergabe und Empfang von Parametern. Beim Hinzufügen 2.000 zusätzlicher Komponenteninstanzen ergibt sich durch Multiplikation, dass die Renderingzeit um 0,12 Sekunden verlängert und die Benutzeroberfläche den Benutzern langsam erscheinen würde.

Es ist möglich, Komponenten schlanker zu gestalten, sodass Sie mehr davon verwenden können. Eine leistungsfähigere Technik besteht jedoch oft darin, so viele Komponenten zu vermeiden, die gerendert werden müssen. In den folgenden Abschnitten werden zwei Ansätze beschrieben, die Sie nutzen können.

Weitere Informationen zur Arbeitsspeicherverwaltung finden Sie unter Hosten und Bereitstellen von serverseitigen ASP.NET Core Blazor-Apps.

Inline-Einfügung untergeordneter Komponenten in ihre übergeordneten Komponenten

Betrachten Sie den folgenden Teil einer übergeordneten Komponente, der untergeordnete Komponenten in einer Schleife rendert:

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

Das Beispiel oben funktioniert gut, wenn nicht Tausende von Nachrichten auf einmal angezeigt werden. Um Tausende von Nachrichten gleichzeitig anzuzeigen, sollten Sie die separate ChatMessageDisplay-Komponente nicht ausklammern. Stattdessen sollten Sie die untergeordnete Komponente inline in die übergeordnete Komponente einbinden. Der folgende Ansatz vermeidet den Mehraufwand pro Komponente, der durch das Rendern so vieler untergeordneter Komponenten entsteht, auf Kosten des Verlusts der Möglichkeit, das Markup jeder untergeordneten Komponente unabhängig erneut zu rendern:

<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>
Definieren wiederverwendbarer RenderFragments im Code

Sie können untergeordnete Komponenten ausschließlich als Möglichkeit zur Wiederverwendung von Renderinglogik ausklammern. In diesem Fall können Sie wiederverwendbare Renderinglogik erstellen, ohne zusätzliche Komponenten zu implementieren. Definieren Sie in einem beliebigen @code-Block einer Komponente ein RenderFragment. Rendern Sie das Fragment von einer beliebigen Stelle aus so oft wie erforderlich:

@RenderWelcomeInfo

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

@RenderWelcomeInfo

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

Um RenderTreeBuilder-Code für mehrere Komponenten wiederverwendbar zu machen, deklarieren Sie RenderFragment als public und static:

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

SayHello im Beispiel oben kann von einer nicht verknüpften Komponente aufgerufen werden. Dieses Verfahren eignet sich für das Erstellen von Bibliotheken von wiederverwendbaren Markupausschnitten, die ohne Mehraufwand pro Komponente gerendert werden können.

RenderFragment-Delegate können auch Parameter akzeptieren. Die folgende Komponente übergibt die Nachricht (message) an den RenderFragment-Delegaten:

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

Der Ansatz oben verwendet die Renderinglogik ohne Mehraufwand pro Komponente wieder. Allerdings lässt der Ansatz weder zu, dass seine Unterstruktur der Benutzeroberfläche unabhängig aktualisiert wird, noch kann das Rendering dieser Unterstruktur der Benutzeroberfläche übersprungen werden, wenn ihr übergeordnetes Element gerendert wird, weil es keine Komponentenbegrenzung gibt. Die Zuweisung zu einem RenderFragment-Delegaten wird nur in Razor-Komponentendateien (.razor) unterstützt. Ereignisrückrufe werden nicht unterstützt.

Verwenden Sie für ein nicht statisches Feld, eine Methode oder Eigenschaft, worauf kein Feldinitialisierer verweisen kann, wie z. B. TitleTemplate im folgenden Beispiel, eine Eigenschaft anstelle eines Felds für das RenderFragment:

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

Nicht zu viele Parameter empfangen

Wenn eine Komponente extrem häufig wiederholt wird, z. B. Hunderte oder Tausende Mal, kumuliert der Mehraufwand für das Übergeben und Empfangen der einzelnen Parameter.

Es ist selten, dass zu viele Parameter die Leistung erheblich einschränken, aber es kann ein Faktor sein. Bei einer TableCell-Komponente, die 4.000-mal innerhalb eines Rasters gerendert wird, könnte jeder Parameter, der an die Komponente übergeben wird, den gesamten Renderingaufwand um ungefähr 15 ms erhöhen. Das Übergeben von zehn Parametern erfordert ca. 150 ms und verursacht einen Verzögerungen beim Rendern der Benutzeroberfläche.

Um die Parameterlast zu reduzieren, bündeln Sie mehrere Parameter in einer benutzerdefinierten Klasse. Beispielsweise kann eine Tabellenzellenkomponente ein gemeinsames Objekt akzeptieren. Im folgenden Beispiel ist Data für jede Zelle unterschiedlich, aber Options ist für alle Zelleninstanzen gleich:

@typeparam TItem

...

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

Bedenken Sie jedoch, dass es eine Verbesserung bedeuten könnte, keine Tabellenzellenkomponente zu verwenden, wie im vorangegangenen Beispiel gezeigt, und die Logik stattdessen inline in die übergeordnete Komponente einzubinden.

Hinweis

Wenn mehrere Ansätze zur Verbesserung der Leistung zur Verfügung stehen, ist in der Regel ein Benchmarking der Ansätze erforderlich, um festzustellen, welcher Ansatz die besten Ergebnisse liefert.

Weitere Informationen zu den generischen Typparametern (@typeparam) finden Sie in den folgenden Ressourcen:

Sicherstellen, dass kaskadierende Parameter korrigiert sind

Die CascadingValue-Komponente hat einen optionalen Parameter IsFixed:

  • Wenn der IsFixed-Wert false ist (Standard), richtet jeder Empfänger des kaskadierenden Werts ein Abonnement ein, um Änderungsbenachrichtigungen zu empfangen. Jeder [CascadingParameter] erfordert aufgrund der Abonnementnachverfolgung erheblich mehr Aufwand als ein regulärer [Parameter].
  • Wenn IsFixedtrue ist (z. B. <CascadingValue Value="someValue" IsFixed="true">), erhalten Empfänger den Anfangswert, richten aber kein Abonnement für den Empfang von Aktualisierungen ein. Jeder [CascadingParameter] ist einfach und nicht aufwändiger als ein regulärer [Parameter].

Das Festlegen von IsFixed auf true verbessert die Leistung, wenn es eine große Anzahl anderer Komponenten gibt, die den kaskadierten Wert erhalten. Legen Sie nach Möglichkeit IsFixed auf true für kaskadierte Werte fest. Sie können IsFixed auf true festlegen, wenn sich der angegebene Wert im Laufe der Zeit nicht ändert.

Wenn eine Komponente this als kaskadierten Wert übergibt, kann IsFixed auch auf true festgelegt werden:

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

Weitere Informationen finden Sie unter Kaskadierende Werte und Parameter in Blazor in ASP.NET Core.

Vermeiden des Attribut-Splattings mit CaptureUnmatchedValues

Komponenten können mithilfe des CaptureUnmatchedValues-Flags auswählen, „nicht übereinstimmende“ Parameterwerte zu empfangen:

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

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

Dieser Ansatz ermöglicht, beliebige zusätzliche Attribute an das Element zu übergeben. Dieser Ansatz ist aber auch sehr aufwändig, da der Renderer Folgendes ausführen muss:

  • Abgleichen aller angegebenen Parameter mit dem Satz bekannter Parameter, um ein Wörterbuch zu erstellen.
  • Nachverfolgen, wie viele Kopien desselben Attributs sich gegenseitig überschreiben.

Verwenden Sie CaptureUnmatchedValues, wenn die Leistung des Komponentenrenderings nicht kritisch ist, z. B. bei Komponenten, die nicht häufig wiederholt werden. Versuchen Sie bei Komponenten, die in großem Umfang gerendert werden, z. B. die einzelnen Elemente in einer umfangreichen Liste oder Zellen in einem Raster, Attributsplatting zu vermeiden.

Weitere Informationen finden Sie unter ASP.NET Core Blazor-Attributsplatting und beliebige Parameter.

Manuelles Implementieren von SetParametersAsync

Eine bedeutende Quelle für den Renderingmehraufwand pro Komponente ist das Schreiben eingehender Parameterwerte in die [Parameter]-Eigenschaften. Der Renderer verwendet Reflexion, um die Parameterwerte zu schreiben, was zu einer schlechten Leistung im großen Stil führen kann.

In einigen Extremfällen möchten Sie möglicherweise die Reflexion vermeiden und Ihre eigene Logik für die Parametereinstellung manuell implementieren. Dies kann unter folgenden Umständen anwendbar sein:

  • Eine Komponente wird extrem häufig gerendert, z. B. wenn es Hunderte oder Tausende von Kopien der Komponente in der Benutzeroberfläche gibt.
  • Eine Komponente akzeptiert viele Parameter.
  • Sie finden, dass der Mehraufwand für das Empfangen von Parametern die Reaktionsfähigkeit der Benutzeroberfläche merklich beeinträchtigt.

In Extremfällen können Sie die virtuelle SetParametersAsync-Methode der Komponente überschreiben und Ihre eigene komponentenspezifische Logik implementieren. Im folgenden Beispiel werden Wörterbuchsuchvorgänge absichtlich vermieden:

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

Im vorangegangenen Code führt die Rückgabe der Basisklasse SetParametersAsync die normale Lebenszyklusmethode aus, ohne dass Parameter erneut zugewiesen werden.

Wie Sie im vorhergehenden Code sehen können, ist das Überschreiben von SetParametersAsync und das Bereitstellen benutzerdefinierter Logik kompliziert und aufwändig, daher wird die Verwendung dieses Ansatzes im Allgemeinen nicht empfohlen. In Extremfällen kann die Renderingleistung um 20 bis 25 % verbessert werden, aber Sie sollten diesen Ansatz nur in den in diesem Abschnitt zuvor aufgeführten Extremszenarien berücksichtigen.

Ereignisse nicht zu schnell auslösen

Einige Browserereignisse werden sehr häufig ausgelöst. Beispielsweise können onmousemove und onscroll Dutzende oder Hunderte Male pro Sekunde ausgelöst werden. In den meisten Fällen muss die Aktualisierung der Benutzeroberfläche nicht so häufig ausgeführt werden. Wenn Ereignisse zu schnell ausgelöst werden, kann dies die Reaktionsfähigkeit der Benutzeroberfläche beeinträchtigen oder zu viel CPU-Zeit verbrauchen.

Anstatt native Ereignisse zu verwenden, die schnell ausgelöst werden, sollten Sie die Verwendung von JS-Interop in Betracht ziehen, um einen Rückruf zu registrieren, der weniger häufig ausgelöst wird. Beispielsweise zeigt die folgende Komponente die Position der Maus an, wird jedoch nur höchstens ein Mal alle 500 ms aktualisiert:

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

Der entsprechende JavaScript-Code registriert den DOM-Ereignislistener für Mausbewegungen. In diesem Beispiel verwendet der Ereignislistener die throttle-Funktion von Lodash, um die Rate der Aufrufe einzuschränken:

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

Vermeiden des erneuten Renderns nach der Behandlung von Ereignissen ohne Zustandsänderungen

Standardmäßig erben Komponenten von ComponentBase, das automatisch StateHasChanged aufruft, nachdem die Ereignishandler der Komponente aufgerufen wurden. In einigen Fällen kann es unnötig oder unerwünscht sein, ein erneutes Rendern auszulösen, nachdem ein Ereignishandler aufgerufen wurde. Beispielsweise ändert ein Ereignishandler den Komponentenzustand möglicherweise nicht. In diesen Szenarien kann die App die IHandleEvent-Schnittstelle nutzen, um das Verhalten der Ereignisbehandlung von Blazor zu steuern.

Um das erneute Rendern für alle Ereignishandler einer Komponente zu verhindern, implementieren Sie IHandleEvent, und stellen Sie eine IHandleEvent.HandleEventAsync-Aufgabe bereit, die den Ereignishandler aufruft, ohne StateHasChanged aufzurufen.

Im folgenden Beispiel löst kein Ereignishandler, der der Komponente hinzugefügt wurde, ein erneutes Rendern aus, weshalb der Aufruf von HandleSelect nicht zu einem erneuten Rendern führt.

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

Zusätzlich zur Verhinderung von erneuten Rendervorgängen nach dem Auslösen von Ereignisbehandlern in einer Komponente auf globale Weise ist es möglich, erneutes Rendern nach einem einzelnen Ereignisbehandler zu verhindern, indem die folgende Hilfsmethode verwendet wird.

Fügen Sie die folgende EventUtil-Klasse zu einer Blazor-App hinzu. Die statischen Aktionen und Funktionen am Anfang der EventUtil-Klasse stellen Handler bereit, die mehrere Kombinationen von Argumenten und Rückgabetypen abdecken, die Blazor bei der Behandlung von Ereignissen verwendet.

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

Rufen Sie EventUtil.AsNonRenderingEventHandler auf, um einen Ereignishandler aufzurufen, der beim Aufrufen kein Rendern auslöst.

Im folgenden Beispiel:

  • Wenn Sie die erste Schaltfläche auswählen, die HandleClick1 aufruft, wird ein erneutes Rendern ausgelöst.
  • Wenn Sie die zweite Schaltfläche auswählen, die HandleClick2 aufruft, wird kein erneutes Rendern ausgelöst.
  • Wenn Sie die dritte Schaltfläche auswählen, die HandleClick3 aufruft, wird kein erneutes Rendern ausgelöst, und es werden Ereignisargumente verwendet (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);
    }
}

Neben der Implementierung der IHandleEvent-Schnittstelle kann die Nutzung der anderen in diesem Artikel beschriebenen bewährten Methoden auch dazu beitragen, unerwünschtes Rendern nach der Behandlung von Ereignissen zu reduzieren. Beispielsweise kann das Außerkraftsetzen von ShouldRender in untergeordneten Komponenten der Zielkomponente zum Steuern des erneuten Renderns verwendet werden.

Vermeiden der Neuerstellung von Delegaten für viele wiederholte Elemente oder Komponenten

Die Neudarstellung der BlazorLambdaausdrucksdelegaten von für Elementen oder Komponenten in einer Schleife kann dazu führen, dass die Leistung beeinträchtigt wird.

Die folgende Komponente, die im Artikel zur Ereignisbehandlung dargestellt wird, rendert einige Schaltflächen. Jede Schaltfläche weist ihrem @onclick-Ereignis einen Delegaten zu. Das ist in Ordnung, wenn nicht viele Schaltflächen gerendert werden müssen.

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

Wenn eine große Anzahl von Schaltflächen mit dem vorherigen Ansatz gerendert werden, wird die Renderinggeschwindigkeit beeinträchtigt, wodurch das Benutzererlebnis eingeschränkt wird. Um eine große Anzahl von Schaltflächen mit einem Rückruf für Klickereignisse zu rendern, verwendet das folgende Beispiel eine Sammlung von Schaltflächenobjekten, die den @onclick-Delegaten jeder Schaltfläche einem Action zuweisen. Der folgende Ansatz erfordert nicht, dass bei jedem Rendern der Schaltflächen alle Schaltflächen-Delegaten mit Blazor neu erstellt werden:

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

Optimieren der JavaScript-Interop-Geschwindigkeit

Aufrufe zwischen .NET und JavaScript sind aus folgenden Gründen mit zusätzlichem Mehraufwand verbunden:

  • Standardmäßig sind Aufrufe asynchron.
  • Standardmäßig sind Parameter und Rückgabewerte mit JSON serialisiert, damit der Konvertierungsmechanismus zwischen .NET- und JavaScript-Typen leicht verständlich ist.

Zusätzlich werden diese Aufrufe für serverseitige Blazor-Apps über das Netzwerk übergeben.

Vermeiden übermäßig differenzierter Aufrufe

Da jeder Aufruf einen gewissen Mehraufwand erfordert, kann es hilfreich sein, die Anzahl der Aufrufe zu verringern. Beachten Sie den folgenden Code, der eine Sammlung von Elementen im localStorage des Browsers speichert:

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

Im vorhergehenden Beispiel wird für jedes Element ein separater JS-Interopaufruf durchgeführt. Stattdessen reduziert der folgende Ansatz das JS-Interop auf einen einzelnen Aufruf:

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

Die entsprechende JavaScript-Funktion speichert die gesamte Sammlung von Elementen auf dem Client:

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

Bei Blazor WebAssembly-Apps führt die Zusammenfassung einzelner JS-Interopaufrufe zu einem einzigen Aufruf in der Regel nur dann zu einer erheblichen Leistungssteigerung, wenn die Komponente eine große Anzahl von JS-Interopaufrufen durchführt.

Erwägen der Verwendung von synchronen Aufrufen

Aufrufen von JavaScript über .NET

Dieser Abschnitt gilt nur für clientseitige Komponenten.

JS-Interop-Aufrufe sind standardmäßig asynchron, und zwar unabhängig davon, ob der aufgerufene Code synchron oder asynchron ist. Aufrufe sind standardmäßig asynchron, um sicherzustellen, dass Komponenten mit serverseitigen und clientseitigen Rendermodi kompatibel sind. Auf dem Server müssen alle JS-Interop-Aufrufe asynchron sein, da sie über eine Netzwerkverbindung gesendet werden.

Wenn Ihre Komponente ausschließlich in WebAssembly ausgeführt wird, können Sie sich für synchrone JS-Interop-Aufrufe entscheiden. Dies ist mit etwas weniger Mehraufwand verbunden als asynchrone Aufrufe und kann zu weniger Renderingzyklen führen, da es keinen Zwischenzustand gibt, während auf die Ergebnisse gewartet wird.

Um in einer clientseitigen Komponente einen synchronen Aufruf von .NET an JavaScript zu richten, wandeln Sie IJSRuntime in IJSInProcessRuntime um, um den JS-Interop-Aufruf auszuführen:

@inject IJSRuntime JS

...

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

Wenn Sie IJSObjectReference in clientseitigen Komponenten mit ASP.NET Core 5.0 oder höher verwenden, können Sie IJSInProcessObjectReference stattdessen synchron verwenden. IJSInProcessObjectReference implementiert IAsyncDisposable/IDisposable und sollte für die Garbage Collection verworfen werden, um Arbeitsspeicherverlust zu verhindern, wie das folgende Beispiel veranschaulicht:

@inject IJSRuntime JS
@implements IAsyncDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", 
            "./scripts.js");
        }
    }

    ...

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

Aufrufen von .NET über JavaScript

Dieser Abschnitt gilt nur für clientseitige Komponenten.

JS-Interop-Aufrufe sind standardmäßig asynchron, und zwar unabhängig davon, ob der aufgerufene Code synchron oder asynchron ist. Aufrufe sind standardmäßig asynchron, um sicherzustellen, dass Komponenten mit serverseitigen und clientseitigen Rendermodi kompatibel sind. Auf dem Server müssen alle JS-Interop-Aufrufe asynchron sein, da sie über eine Netzwerkverbindung gesendet werden.

Wenn Ihre Komponente ausschließlich in WebAssembly ausgeführt wird, können Sie sich für synchrone JS-Interop-Aufrufe entscheiden. Dies ist mit etwas weniger Mehraufwand verbunden als asynchrone Aufrufe und kann zu weniger Renderingzyklen führen, da es keinen Zwischenzustand gibt, während auf die Ergebnisse gewartet wird.

Um in einer clientseitigen Komponente einen synchronen Aufruf von JavaScript an .NET zu richten, verwenden Sie DotNet.invokeMethod anstelle von DotNet.invokeMethodAsync.

Synchrone Aufrufe funktionieren unter folgenden Voraussetzungen:

  • Die Komponente wird nur für die Ausführung in WebAssembly gerendert.
  • Die aufgerufene Funktion gibt synchron einen Wert zurück. Die Funktion ist keine async-Methode und gibt keinen .NET-Task bzw. kein JavaScript-Promise zurück.

Dieser Abschnitt gilt nur für clientseitige Komponenten.

JS-Interop-Aufrufe sind standardmäßig asynchron, und zwar unabhängig davon, ob der aufgerufene Code synchron oder asynchron ist. Aufrufe sind standardmäßig asynchron, um sicherzustellen, dass Komponenten mit serverseitigen und clientseitigen Rendermodi kompatibel sind. Auf dem Server müssen alle JS-Interop-Aufrufe asynchron sein, da sie über eine Netzwerkverbindung gesendet werden.

Wenn Ihre Komponente ausschließlich in WebAssembly ausgeführt wird, können Sie sich für synchrone JS-Interop-Aufrufe entscheiden. Dies ist mit etwas weniger Mehraufwand verbunden als asynchrone Aufrufe und kann zu weniger Renderingzyklen führen, da es keinen Zwischenzustand gibt, während auf die Ergebnisse gewartet wird.

Um in einer clientseitigen Komponente einen synchronen Aufruf von .NET an JavaScript zu richten, wandeln Sie IJSRuntime in IJSInProcessRuntime um, um den JS-Interop-Aufruf auszuführen:

@inject IJSRuntime JS

...

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

Wenn Sie IJSObjectReference in clientseitigen Komponenten mit ASP.NET Core 5.0 oder höher verwenden, können Sie IJSInProcessObjectReference stattdessen synchron verwenden. IJSInProcessObjectReference implementiert IAsyncDisposable/IDisposable und sollte für die Garbage Collection verworfen werden, um Arbeitsspeicherverlust zu verhindern, wie das folgende Beispiel veranschaulicht:

@inject IJSRuntime JS
@implements IAsyncDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            module = await JS.InvokeAsync<IJSInProcessObjectReference>("import", 
            "./scripts.js");
        }
    }

    ...

    async ValueTask IAsyncDisposable.DisposeAsync()
    {
        if (module is not null)
        {
            await module.DisposeAsync();
        }
    }
}

Erwägen der Verwendung von Aufrufen ohne Marshalling

Dieser Abschnitt gilt nur für Blazor WebAssembly-Apps.

Bei der Ausführung auf Blazor WebAssembly können unmarshallte Aufrufe von .NET an JavaScript durchgeführt werden. Dabei handelt es sich um synchrone Aufrufe, die keine JSON-Serialisierung für Argumente oder Rückgabewerte ausführen. Alle Aspekte der Speicherverwaltung und Übersetzungen zwischen .NET- und JavaScript-Darstellungen werden dem Entwickler überlassen.

Warnung

Während die Verwendung von IJSUnmarshalledRuntime den geringsten Aufwand der JS-Interopansätze darstellt, gibt es für die JavaScript-APIs, die erforderlich sind, um mit diesen APIs zu interagieren, aktuell keine Dokumentation. Außerdem kann es im Rahmen zukünftiger Releases zu Breaking Changes kommen.

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

Verwenden der JavaScript-Interoperabilität mit [JSImport]/[JSExport]

Das JavaScript-Interop [JSImport]/[JSExport] für Blazor WebAssembly-Apps bietet verbesserte Leistung und Stabilität im Vergleich zur JS-Interop-API in Frameworkreleases vor ASP.NET Core mit .NET 7.

Weitere Informationen finden Sie unter JavaScript-Interoperabilität mit JSImport/JSExport mit ASP.NET CoreBlazor.

Ahead-of-Time-Kompilierung (AOT)

Bei der AOT-Kompilierung (Ahead-of-Time) wird der .NET-Code einer Blazor-App direkt in die native WebAssembly kompiliert, um die direkte Ausführung durch den Browser zu ermöglichen. Apps mit AOT-Kompilierung sind zwar größer, und ihr Download dauert länger, doch bieten sie in der Regel eine bessere Laufzeitleistung, insbesondere bei Apps mit CPU-intensiven Aufgaben. Weitere Informationen finden Sie unter Hosten und Bereitstellen von Blazor WebAssembly in ASP.NET Core.

Minimieren der Größe des Anwendungsdownloads

Neuverknüpfung der Runtime

Informationen zur Minimierung der Größe des App-Downloads durch eine Neuverknüpfung der Runtime finden Sie unter Hosten und Bereitstellen von Blazor WebAssembly in ASP.NET Core.

Verwenden Sie System.Text.Json

Die JS-Interopimplementierung von Blazor basiert auf System.Text.Json. Dabei handelt es sich um eine JSON-Serialisierungsbibliothek mit hoher Leistung und niedriger Arbeitsspeicherbelegung. Das Verwenden von System.Text.Json sollte nicht zu einer zusätzlichen Nutzdatengröße für die App führen, wenn eine oder mehrere alternative JSON-Bibliotheken hinzufügt werden.

Eine Anleitung zur Migration finden Sie unter Migration von Newtonsoft.Json zu System.Text.Json.

IL-Kürzung (Intermediate Language, Zwischensprache)

Dieser Abschnitt gilt nur für Blazor WebAssembly-Apps.

Das Kürzen nicht verwendeter Assemblys über eine Blazor WebAssembly-App reduziert die Größe der App, indem nicht genutzter Code in den Binärdateien der App entfernt wird. Weitere Informationen finden Sie unter Konfigurieren des Trimmers für Blazor in ASP.NET Core.

Das Konfigurieren eines Linkers für eine Blazor WebAssembly-App reduziert die Größe der App, indem nicht genutzter Code in den Binärdateien der App gekürzt wird. Standardmäßig ist der IL-Linker (Intermediate Language, Zwischensprache) nur aktiviert, wenn der Erstellvorgang in der Release-Konfiguration ausgeführt wird. Veröffentlichen Sie die App für die Bereitstellung mithilfe des Befehls dotnet publish, bei dem die Option -c|--configuration auf Release festgelegt ist, um diesen Vorteil zu nutzen:

dotnet publish -c Release

Lazy Loading-Assemblys

Dieser Abschnitt gilt nur für Blazor WebAssembly-Apps.

Laden Sie Assemblys zur Laufzeit, wenn die Assemblys von einer Route benötigt werden. Weitere Informationen finden Sie unter Verzögertes Laden von Assemblys in Blazor WebAssembly in ASP.NET Core.

Komprimierung

Dieser Abschnitt gilt nur für Blazor WebAssembly-Apps.

Wenn eine Blazor WebAssembly-App veröffentlicht wird, wird die Ausgabe bei der Veröffentlichung statisch komprimiert, um die App-Größe zu verringern und den Aufwand für eine Laufzeitkomprimierung zu beseitigen. Blazor basiert darauf, dass der Server eine Inhaltsaushandlung ausführt und statisch komprimierte Dateien bereitstellt.

Nach der Bereitstellung einer App überprüfen Sie, ob die App komprimierte Dateien bereitstellt. Überprüfen Sie in den Entwicklertools eines Browsers auf der Registerkarte Netzwerk, ob die Dateien mit Content-Encoding: br (Brotli-Kompression) oder Content-Encoding: gz (Gzip-Komprimierung) bereitgestellt werden. Wenn der Host keine komprimierten Dateien bereitstellt, befolgen Sie die Anweisungen in Hosten und Bereitstellen von Blazor WebAssembly in ASP.NET Core.

Deaktivieren nicht genutzter Features

Dieser Abschnitt gilt nur für Blazor WebAssembly-Apps.

Die Blazor WebAssembly-Runtime schließt die folgenden .NET-Features ein, die deaktiviert werden können, um eine kleinere Nutzdatengröße zu erzielen:

  • Eine Datendatei wird eingeschlossen, damit die Zeitzoneninformationen korrekt sind. Wenn die App dieses Feature nicht benötigt, ziehen Sie in Betracht, es zu deaktivieren, indem Sie die BlazorEnableTimeZoneSupport-MSBuild-Eigenschaft in der Projektdatei der App auf false festlegen:

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • Die Sortierungsinformationen werden eingeschlossen, damit APIs wie StringComparison.InvariantCultureIgnoreCase ordnungsgemäß funktionieren. Wenn Sie sicher sind, dass die App die Sortierungsinformationen nicht benötigt, ziehen Sie in Betracht, sie zu deaktivieren, indem Sie die BlazorWebAssemblyPreserveCollationData-MSBuild-Eigenschaft in der Projektdatei der App auf false festlegen:

    <PropertyGroup>
      <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
    </PropertyGroup>
    
  • Blazor WebAssembly enthält standardmäßig Globalisierungsressourcen, die zum Anzeigen von Werten wie Datums- und Währungsangaben in der Kultur des Benutzers erforderlich sind. Wenn für die App keine Lokalisierung erforderlich ist, können Sie die App so konfigurieren, dass die invariante Kultur unterstützt wird, die auf der en-US-Kultur basiert.