Blazor WebAssembly in .NET 7: UI-Performance-Optimierung auf Komponentenebene

Stockende UI, keine Reaktion nach dem Klick auf einen Button oder einer Eingabe in einem Feld - dies sind nur wenige Beispiele alltäglicher Probleme, die der Nutzung von Client-Anwendungen im Allgemeinen, und bei Webanwendungen im Speziellen, immer wieder auftreten können. In diesem Artikel schauen wir uns an, wie wir komponentenbasierte UIs in Blazor WebAssembly optimieren können, um dadurch eine für die Benutzer zufriedenstellende Geschwindigkeit und ein flüssiges UI zu bekommen.

In diesem Artikel:

Version Information:

  • .NET SDK: 7.0.202
  • ASP.NET Core Blazor WebAssembly: 7.0.4
  • MudBlazor: 6.2.0

Der Beispiel-Code für diesen Artikel findet sich hier.

In der Entwicklung von Webanwendungen, und so auch in der Entwicklung von Blazor WebAssembly Single Page Applications (SPAs), ist es wichtig, die Laufzeit-Performance der Anwendung immer im Auge zu behalten und wenn nötig, zu optimieren. Daher schauen wir uns in diesem Artikel die Optimierungsmöglichkeiten einer Blazor-Komponente etwas genauer an. Bevor wir aber mit der Optimierung von Komponenten beginnen können, ist es wichtig zu verstehen, welchen Lebenszyklus eine Komponente beim Rendern durchläuft und wie dieser ausgelöst werden kann.

Render-Lebenszyklus einer Komponente

Lebenszyklus

Der Lebenszyklus einer Blazor-Komponente beginnt, wenn sie auf der Seite gerendert wird, d.h. sie wird zum ersten Mal sichtbar.

In der obigen Abbildung sehen wir die verschiedenen Schritte, die durchlaufen werden, sobald das Rendering einer Komponente angestoßen wird.

Im ersten Schritt wird geprüft, ob es sich um das initiale Rendern der Komponente handelt, oder andernfalls die Komponente neu gerendert werden soll. Dies wird anhand von zwei Optionen geprüft:

  • Innerhalb der Basisklasse einer Blazor-Komponente, der ComponentBase-Klasse, wird geprüft, ob es sich um das erste Rendern der Komponente handelt.
  • Handelt es sich nicht um das erste Rendering, wird die Methode ShouldRender(), welche im nächsten Abschnitt näher erläutert wird, der Basisklasse aufgerufen. Die Methode gibt einen bool-Wert zurück der angibt, ob Render-Prozess durchgeführt werden soll. Der Default Rückgabe-Wert ist true.

Trifft keine der beiden Optionen zu, wird der Render-Prozess beendet (2a). Andernfalls wird ein neuer Render-Tree erstellt (2b). Der Render-Tree beschreibt die HTML-Elemente des HTML-Dokuments, welche in der Blazor-Welt aktualisiert werden sollen. Danach wird dieser an das Document Object Model (DOM) weitergeben. Anhand des Render-Trees werden die Änderungen im DOM aktualisiert. Ist das Update abgeschlossen, werden schließlich die Methoden OnAfterRender und OnAfterRenderAsync aufgerufen und der Prozess ist beendet.

Hinweis: Der Render-Tree beinhaltet nur die Änderungen der Komponente, die sich im Vergleich zum aktuell gerenderten Zustand im DOM geändert haben. Dadurch wird nicht bei jedem Render-Prozess, das DOM-Element vollständig ersetzt, sondern nur die aktuellen Änderungen werden im DOM aktualisiert.

Weitere Informationen und Details über den Lebenszyklus von Blazor-Komponenten finden sich bei meinem Kollegen Pawel Gerr in seinem englischen Artikel Blazor Components Deep Dive – Lifecycle Is Not Always Straightforward.

Was kann ein Re-Rendering auslösen?

Nachdem wir gesehen haben, welchen Prozess eine Komponente beim Rendern durchläuft, schauen wir uns jetzt mal an, was den Render-Prozess einer Komponente auslösen kann:

  • Das erste Rendern findet, wie zu erwarten, beim Initialisieren einer Blazor-Komponente statt.
  • Das zweite Szenario, bei der die Komponente neu gerendert wird, ist es, wenn sich Parameter ändern. Dabei wird zwischen eigenen Parametern und Parametern der übergeordneten Komponente unterschieden:

    • SetParametersAsync: Diese Methode wird aufgerufen, sobald Parameter des übergeordneten Elements geändert wurden oder durch das Setzten eines Route-Parameters in der URL.
    • OnParametersSet/OnParametersSetAsync: Diese Methoden werden aufgerufen, sobald sich die Parameter der Komponente ändern oder die übergeordnete Komponente neu gerendert wird.
  • Wird ein DOM-Event ausgelöst, durch beispielsweise ein onlclick-Event, wird die Komponente ebenfalls neu gerendert. Aber nicht nur die Komponente, bei der das Event ausgelöst wurde, sondern auch alle untergeordneten Elemente werden neu gerendert
  • Zuletzt kann der Prozess über die Methode StateHasChanged aus der Basisklasse ComponentBase der Render-Prozess angestoßen werden. Die Methode StateHasChanged() benachrichtigt die Komponente, dass sich der aktuelle Status geändert hat. Dies führt dann dazu, dass die Komponente neu gerendert wird.

Betrachtet man die unterschiedlichen Szenarien, wird ersichtlich, dass der Render-Prozess sehr oft getriggert werden kann. Doch ist das wirklich immer notwendig? Im weiteren Verlauf dieses Artikels schauen wir uns mögliche Optionen an, wie wir den Render-Prozess einer Komponente optimieren können.

Optimierung des Render-Prozesses

ShouldRender überschreiben

Die erste Option, die wir nutzen können den Render-Prozess zu unterbinden, ist die Methode ShouldRender zu überschreiben. Wie wir in der obigen Abbildung gesehen haben, wird in Schritt 2, sobald es sich nicht um das initiale Rendering handelt und die Methode ShouldRender auch false zurückgibt, der Render-Prozess beendet.

Die Methode ShouldRender ist eine Methode der Basisklasse ComponentBase. Der Default Rückgabewert der Methode ist true. Infolgedessen wird bei jedem Render-Vorgang der Prozess durchgeführt. Nehmen wir beispielsweise ein Formular. Wird in einem Formular ein Feld geändert, wird dadurch der Render-Prozess der Komponenten angestoßen. Dies führt dazu, dass zusätzlich auch all die Komponenten neu gerendert werden, die sich im Formular befinden. In einem Formular ist es natürlich wichtig zu beachten, ob das aktuelle Feld Abhängigkeiten zu anderen Feldern hat. Hat das Feld keine Abhängigkeiten, so muss auch nicht bei jeder Änderung des Feldes neu gerendert werden. Um dies zu vermeiden, können wir die ShouldRender-Methode überschreiben.

				
					// CustomInputText.razor.cs

private int _valueHashCode;
private bool _shouldRender;

protected override bool ShouldRender() => _shouldRender;

protected override bool OnParametersSet()
{
    var lastHashCode = _valueHashCode;
    _valueHashCode = Value?.GetHashCode() ?? 0;
    _shouldRender = _valueHashCode != lastHashCode; 
}
				
			

Im obigen Code-Beispiel sehen wir, dass anhand des HashCode die Property Value der Komponente geprüft wird, ob sich diese geändert hat. Ist dies nicht der Fall, gibt die Methode false zurück und der Render-Prozess wird beendet. Sollte die Komponente keine Properties haben, die sich ändern, kann hier natürlich auch immer direkt false zurückgegeben werden.

Dies ist eine Möglichkeit den Render-Prozess frühzeitig zu beenden, wenn es nicht zwingend notwendig ist die Komponente neu zu rendern. Im nächsten Abschnitt schauen wir uns an, wie wir beim Auslösen eines Events den Render-Prozess optimieren können.

IHandleEvent implementieren

Wird ein Event geworfen, wird die Methode StateHasChanged der ComponentBase-Klasse aufgerufen. Das führt dazu, dass der Render-Prozess der Komponente angestoßen wird. Dies hat zwar den Vorteil, dass der Entwickler die Methode StateHasChanged nicht selbst aufrufen muss, wenn ein Event angestoßen wird. Jedoch hat das auch den Nachteil, dass die Komponente auch ohne jegliche Änderungen neu gerendert wird. Um in diesen Prozess einzugreifen, kann das Interface IHandleEvent implementiert werden. Über das Interface wird die Methode HandleEventAsync implementiert, die bei einem Event der Komponente aufgerufen wird.

				
					// HandleEventInputText.razor.cs

public partial class HandleEventInputText : ComponentBase, IHandleEvent
{
    private bool _preventRender;

    public Task HandleEventAsync(EventCallbackWorkItem item, object? arg)
    {
        try
        {
            var task = item.InvokeAsync(arg);
            var shouldAwaitTask = task.Status != TaskStatus.RanToCompletion &&
                                  task.Status != TaskStatus.Canceled;

            if (!_preventRender)
            {
                StateHasChanged();
            }

            return shouldAwaitTask
                ? CallStateHasChangedOnAsyncCompletion(task, _supressRender)
                : Task.CompletedTask;
        }
        finally
        {
            _preventRender = false;
        }
    }

    private async Task CallStateHasChangedOnAsyncCompletion(Task task, bool preventRender)
    {
        try
        {
            await task;
        }
        catch
        {
            if (task.IsCanceled)
            {
                return;
            }

            throw;
        }

        if (!preventRender)
        {
            StateHasChanged();
        }
    }

    void PreventRender()
    {
        _preventRender = true;
    }
}
				
			
				
					
<label>
    @Label <input class="form-control @CssClass" placeholder="Override ShouldRender" id="@Id" @bind="CurrentValue" @oninput="PreventRender"/>
</label>
				
			

Im obigen Code-Beispiel sehen wir die Klasse HandleEventInputText, die von der Basisklasse ComponentBase ableitet und das Interface IHandleEvent implementiert. In der Methode HandleEventAsync wird anhand der Property _preventRender geprüft, ob die Komponente gerendert werden soll oder nicht. Dadurch haben wir in der Komponente die Möglichkeit, bei einem Event die Methode PreventRender aufzurufen, wie wir im obigen Code-Beispiel beim Event oninput sehen können. Infolgedessen bekommt die Property _preventRender den Wert true zugewiesen. Solange die Property den Wert true besitzt, wird der Render-Prozess nicht angestoßen.

Event-Utilities

Eine weitere Variante das erneute Rendern bei Events zu vermeiden, ist der Einsatz der Hilfsklasse EventUtil, die von Microsoft empfohlen wird.

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

Die Hilfsklasse stellt die Methode AsNonRenderingEventHandler bereit. Die Methode kann genutzt werden, um die Func oder Action zwar auszuführen, aber das erneute Rendering des EventHandlers nicht auszulösen.

				
					<MudButton OnClick="Utils.EventUtil.AsNonRenderingEventHandler(TogglePreventRendering)"
           Target="_blank"
           Variant="Variant.Filled"
           EndIcon="@Icons.Material.Filled.Power"
           Color="Color.Secondary">
    Beim Klick auf diesen Button wird kein neues Rendering ausgelöst
</MudButton>
				
			

Im obigen Code-Beispiel, sehen wir einen Button, der einen OnClick-EventHandler, mit Hilfe der Methode AsNonRenderingEventHandler, aufruft. Ähnlich wie im vorherigen Abschnitt wird auch hier das IHandleEvent-Interface eingesetzt, wie es beim Datentyp ReceiverBase zu sehen ist. Der Unterschied zum vorherigen Ansatz liegt darin, dass das Rendering direkt vermieden wird und es kann vorab nicht geprüft werden, ob gerendert werden soll oder nicht.

Im nächsten Abschnitt schauen wir uns an, wie wir mit dem Einsatz von JavaScript ein DOM-Event verzögern können, um dadurch die Anzahl der Render-Vorgänge zu reduzieren.

 

Debounce DOM-Event

Hier vorab ein kleiner Hinweis: In diesem Abschnitt wird Javascript-Interop eingesetzt. Da wir uns in diesem Artikel aber auf die Optimierung von Komponenten fokussieren, gehe ich nicht weiter auf JS-Interop ein. Mehr Informationen hierzu finden sich hier.

Ein aus der JavaScript-Welt bekanntes Verfahren, um das Anstoßen von Events zu verzögern, ist das Debouncing. Beim Debounce-Verfahren wird der EventCallback erst dann ausgeführt, wenn nach einer bestimmten Zeitspanne keine neuen Events mehr geworfen werden. Das heißt, wenn wir z.B. in einem Textfeld tippen, wird das Event oninput erst dann gefeuert, sobald aufgehört wurde zu tippen und eine selbst gesetzte Zeitspanne abgelaufen ist. Dies hat den Vorteil, dass das Event nur einmal ausgeführt wird. Andernfalls würden wir ohne Debouncing für jeden einzelnen Tastenanschlag ein einzelnes Event bekommen das wir behandeln würden, und das jedes Mal ein Re-Rendering auslösen könnte.

Um das Debounce Verfahren in einer Blazor-WebAssembly-Komponente einzusetzen, müssen wir sowohl im C#-Code als auch im JavaScript-Code, Methoden implementieren.

 

C#-/Razor-Code

				
					
<label>
    @Label <textarea @ref="_textareaElement" placeholder="Debounce input" class="form-control @CssClass" id="@Id" @bind="CurrentValue"></textarea>
</label>
				
			
				
					// DebounceTextArea.razor.cs

public partial class DebounceTextArea : IDisposable
{
    //... Code above
    
    [Inject] public IJSRuntime JS { get; set; }

    private IJSObjectReference _module;
    private ElementReference _textareaElement;
    private DotNetObjectReference<DebounceTextArea> _selfReference;

    // JavaScript Event registrieren    
    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            if (_module is not null) 
            {
                _module = await JS.InvokeAsync<IJSObjectReference>("import", 
                    "./Components/DebounceTextArea.razor.js");
            }
        
            _selfReference = DotNetObjectReference.Create(this);
            // Event wird nach 500 ms geworfen
            var minInterval = 500; 
            // JavaScript-Code aufrufen mit dem HTML-Element, 
            // einer ObjectReferenz und dem Timeout bis das Event geworfen wird
           await _module.InvokeVoidAsync("onDebounceInput",
                    _textareaElement, _selfReference, minInterval);
        }

        Console.WriteLine("Debounced TextArea: After Render called.");
    }

    
    // Methode die vom JavaScript-Code aufgerufen werden kann. Als Parameter
    // wird der aktuelle Wert der TextArea übergeben.
    [JSInvokable]
    public void HandleOnInput(string value)
    {
        Console.WriteLine($"TextChanged {Value}. JS Value {value}");
        if (Value != value)
        {
            StateHasChanged();
        }
    }
    
    //... Code below   
 
    public void Dispose() => _selfReference?.Dispose();
}
				
			

Betrachten wir zunächst den C#-Code im obigen Code-Beispiel. In der Methode OnAfterRenderAsync wird beim initialen Rendering mit Hilfe von JS-Interop die Funktion onDebounceInput im JavaScript aufgerufen. Zum Aufrufen werden zwei Referenzen erwartet:

  • Die erste Referenz ist eine Objektreferenz der Komponente selbst, die mit Hilfe der Klasse DotNetObjectReference erstellt werden kann.

    Hinweis: Wird ein Disposeable-Objekt in einer Komponente genutzt, muss das Interface IDisposable implementiert werden. In der Methode Dispose müssen dann alle Objekte zerstört werden.

  • Die zweite Referenz verweist auf das HTML-Objekt. Im Code-Beispiel wird hierfür im HTML-Code das Attribut @ref genutzt, um eine ElementReference im C#-Code zu erstellen.

Die anderen beiden Parameter, um die JavaScript-Methode aufzurufen, sind natürlich der Name der Methode und das Intervall für das Verzögern des Events.

Weiter schauen wir uns die HandleOnInput-Methode näher an. Diese wird vom JavaScript-Code aufgerufen, sobald das Event geworfen wurde. Damit der JavaScript-Code die Methode aufrufen kann, wird das JSInvokable-Attribut benötigt. In der Methode selbst wird lediglich die Methode StateHasChanged aufgerufen, sobald sich der Wert der Property Value geändert hat.

JavaScript-Code

Um die Methode HandleOnInput, die wir im obigen Abschnitt gesehen haben, aufrufen zu können, müssen wir die JavaScript-Funktion onDebounceInput hinzufügen. Diese wird dann aus dem C#-Code via JS-Interop aufgerufen.

				
					// DebounceTextArea.razor.js
export function onDebounceInput(elem, component, interval) {
    elem.addEventListener('input', debounce(e => {
        component.invokeMethodAsync('HandleOnInput', e.target.value);
    }, interval));
}

function debounce(func, timeout = 300) {
    let timer;
    return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => { func.apply(this, args); }, timeout);
    };
}
				
			

Wie wir im obigen Code-Beispiel sehen, wird im JavaScript-Code die Funktion onDebounceInput implementiert. Diese finden wir auch im C# Code wieder, wenn wir nochmal auf die Methode OnAfterRenderAsync schauen. Hier wird nachdem alle Parameter definiert wurden, um die Funktion mit der folgenden Code-Zeile aufgerufen:

				
					// DebounceTextArea.razor.cs

await JS.InvokeVoidAsync(
    "onDebounceInput", _textareaElement, _selfReference, minInterval
);
				
			

Nachdem die C#-Methode InvokeVoidAsync aufgerufen wurde, wird via JSInterop die JavaScript-Funktion onDebounceInput aufgerufen. Die Funktion registriert einen EventListener der auf das Event input hört. Mit Hilfe der debounce-Funktion, wird der Aufruf der C#-Methode HandleOnInput solange verzögert, bis der Interval abgelaufen ist, nachdem aufgehört wurde in das Textfeld zu schreiben.

Hinweis: Um den Effekt im Video gut erkennen zu können, sollte das Video im Fullscreen abgespielt werden.

Bevor wir uns nun im nächsten Abschnitt mit dem Darstellen von Listen beschäftigen, hier noch ein Hinweis: Das Überschreiben von ShouldRender oder das Implementieren des Interface IHandleEvent kann mit dem Debounce verfahren auch kombiniert werden.

Listenoptimierung

Virtualize-Komponente

Nachdem wir nun Möglichkeiten gesehen haben, um den Render-Prozess von Komponenten in Blazor WebAssembly zu optimieren, schauen wir uns jetzt an was passiert, wenn wir eine Liste rendern. Als Beispiel nutzen wir hier eine Liste, in der wir einzelne Einträge selektieren können.

Wie wir im vorherigen Abschnitt des Artikels schon gesehen haben, wird bei einem Event wie z.b. ein OnClick der Render-Prozess angestoßen. Infolgedessen haben wir bei einer Liste das Problem, dass bei jeder Click auf ein Listen-Element, alle Einträge neu gerendert werden. Zusätzlich müssen bei einem for-Loop von Anfang an alle Einträge in den DOM geladen werden. Zur Optimierung kann die Virtualize-Komponente, die vom Framework zur Verfügung gestellt wird, genutzt werden. Die Komponente hat den Vorteil, dass nur die Komponenten geladen werden, die sich im Sichtbereich der Anwendung befinden. Was dazu führt das, auch wenn über die API die Daten nicht nach und nach geladen werden können, nicht alle Einträge direkt dem DOM hinzugefügt werden. Zusätzlich kann über den Parameter OverscanCount vor und nach dem Sichtbereich eine gewisse Anzahl an Einträgen im DOM vorgeladen werden. 

Seit .NET 7 gibt es die Möglichkeit den SpacerElement-Parameter zu setzen. Mit diesem Parameter ist wird angegeben welches Element genutzt werden soll, bei welchem die berechnete Höhe angegeben wird. Der Default-Wert ist ein div-element. Wie wir in der folgenden Abbildung sehen wird dieses Element vor und nach den im DOM gerenderten Komponenten hinzugefügt.

Dies kann hilfreich sein, um z.B. in einer Tabelle den Wert tr-Element zu ändern oder in einer Liste auf ein li-Element.

				
					

<Virtualize Context="contribution" ItemsProvider="@LoadContributions" OverscanCount="10">
    <ItemContent>
        <ContributionCard Contribution="@contribution"></ContributionCard>
    </ItemContent>
    <Placeholder>
        <PlaceholderCard></PlaceholderCard>
    </Placeholder>
</Virtualize>
				
			
				
					// Contributions.razor.cs

public partial class Contributions
{
    [Inject] public ContributionService ContributionService { get; set; }

    private async ValueTask<ItemsProviderResult<Contribution>> LoadContributions(
        ItemsProviderRequest request)
    {
        var maxCount = 200;
        var numConfs = Math.Min(request.Count, maxCount - request.StartIndex);
        var contributions =
            await ContributionService.GetContributionsAsync(request.StartIndex, numConfs,
                request.CancellationToken);
        return new ItemsProviderResult<Contribution>(contributions, maxCount);
    }
}
				
			

Im obigen Code-Beispiel sehen wir die Virtualize-Komponente. Die Liste an Personen wird mit dem ItemProvider geladen. Damit beim Scrollen der Liste keine zu große Verzögerung auftritt, werden mit dem OverscanCount vor und nach dem Sichtbereich 10 Einträge vorgehalten. Der ItemProvider bietet die Möglichkeit, die Daten via Lazy-Loading nachzuladen. Im obigen Code-Beispiel, sehen wir die Methode LoadContributions, die immer dann aufgerufen wird, sobald neue Einträge geladen werden müssen. Können die Daten nicht schnell genug nachgeladen werden, durch beispielsweise einer langsamen API oder einer schlechten Internetverbindung, bietet die Virtualize-Komponente die Möglichkeit an, einen Placeholder einzusetzen. Dieser wird so lange angezeigt, bis die nächsten Einträge geladen wurden.

Durch den Einsatz der Virtualize-Komponente haben wir also die Möglichkeit die Performance zu optimieren. Jedoch kann dies nicht zwingend mit den Optimierungen der vorherigen Abschnitte verbunden werden. Das liegt daran, dass Komponenten, die nicht im Sichtbereich liegen oder vorgeladen wurden, aus dem DOM entfernt wurden. Wenn diese wieder geladen werden, handelt es sich um das Initialisieren der Komponente, was automatisch zu einem Render-Prozess führt.

Daher ist eine wichtige Voraussetzung zum Einsetzten der Virtualize-Komponente, dass nicht alle Einträge einer Liste im DOM gerendert sein müssen.

EXPERIMENTAL: QuickGrid

Eine weitere Variante eine Liste bzw. eine Tabelle zu virtualisieren ist das QuickGird, welches mit .NET 7 als expertimentelles Package vorgestellt wurde. Der Fokus der Komponente liegt in der performanten Darstellung von Daten in Form eines Grids.

Ein weiterer herausragender Vorteil dieser Komponente ist es, dass das Grid sehr stark angepasst werden kann:

  • Die UI kann via CSS Klassen und eigenem Theme angepasst werden.
  • Die Spalten können durch ein eigenes Template ersetzt werden.
  • Für die Sortierung der Spalten kann eine eigene Funktion hinzugefügt werden. 
  • uvm.

Wie auch schon in der vorherigen Komponente, kann auch hier die Virtualisierung durch den Parameter Virtualize aktiviert werden. Dies hat den gleichen Effekt wie es im vorherigen Kapitel beschrieben wurde. Ein weiteres Feature ist die Option, Paging hinzuzufügen. So dass die Tabelle Seitenweise dargestellt werden kann und die Seiten über eine Navigation gewechselt werden können.

Im folgenden Codebeispiel sehen wir, wie die Komponente eingesetzt werden kann.

				
					//Contributions.razor

<div>
    <QuickGrid ItemsProvider="_contributionsProvider" Virtualize="true" Pagination="pagination">
        <PropertyColumn Title="Titel" Property="@(c => c.Title)" Sortable="true" />
        <PropertyColumn Title="Datum" Property="@(c => c.Date)" Sortable="true" />
        <PropertyColumn Title="Sprache" Property="@(c => c.Language)" />
        <PropertyColumn Title="PrimaryTag" Property="@(c => c.PrimaryTag)" Sortable="true" />
    </QuickGrid>
</div>
<div class="page-buttons">
    Page:
    @if (pagination.TotalItemCount.HasValue)
    {
        for (var pageIndex = 0; pageIndex <= pagination.LastPageIndex; pageIndex++)
        {
            var capturedIndex = pageIndex;
            <button @onclick="@(() => GoToPageAsync(capturedIndex))"
                    class="@PageButtonClass(capturedIndex)"
                    aria-current="@AriaCurrentValue(capturedIndex)"
                    aria-label="Go to page @(pageIndex + 1)">
                @(pageIndex + 1)
            </button>
        }
    }
</div>
				
			
				
					//Contributions.razor.cs

private GridItemsProvider<Contribution>? _contributionsProvider;
private PaginationState pagination = new PaginationState { ItemsPerPage = 100 };

protected override async Task OnInitializedAsync()
{
    _contributionsProvider = async req =>
    {
        var count = await _contributionService.GetContributionCountAsync(req.CancellationToken);
        var response = await _contributionService.GetContributionsAsync(req.StartIndex, req.Count ?? 100, req.CancellationToken);
        return GridItemsProviderResult.From(
            items: response ?? new(),
            totalItemCount: count);
    };
    pagination.TotalItemCountChanged += (sender, eventArgs) => StateHasChanged();

    await base.OnInitializedAsync();
}

private async Task GoToPageAsync(int pageIndex) =>
    await pagination.SetCurrentPageIndexAsync(pageIndex);

private string? PageButtonClass(int pageIndex)
    => pagination.CurrentPageIndex == pageIndex ? "current" : null;

private string? AriaCurrentValue(int pageIndex)
    => pagination.CurrentPageIndex == pageIndex ? "page" : null;
				
			

Mehr Information bzgl. Anpassung und Nutzung der Komponente finden sich hier.

Hinweis: Die Komponente befindet sich in .NET 7 zwar noch in einem experimentellen Zustand, jedoch wurde bereits bestätigt, dass in der Version .NET 8 das QuickGrid teil des Frameworks sein wird (das dazugehörige Github Issue findet sich hier).

Fazit

In diesem Artikel haben wir mögliche Varianten gesehen, den Render-Prozess von Blazor-WebAssembly-Komponenten zu verbessern. Schon mit kleinen Anpassungen können viele Render-Vorgänge vermieden werden, was sich positiv auf die Performance der ganzen Anwendung auswirken kann. Wichtig ist dabei zu beachten, wann welche Komponente gerendert werden muss und wann nicht. Durch das Überschreiben von der Methode ShouldRender oder das Implementieren des Interface IHandleEvent konnten wir sehen, dass dadurch bereits viele Render-Vorgänge eingespart wurden. Doch auch wenn ein Rendering nicht vermieden werden kann, haben wir durch den Einsatz von JavaScript bei einem Event, die Anzahl der Render-Vorgänge minimieren können. Um Listen performant darstellen zu können, haben wir gesehen das sowohl mit der Virtualize-Komponente als auch der QuickGrid-Komponente, bereits viele Funktionen eingesetzt werden konnten, wie beispielsweise das Lazy-Loading, Paging oder eine Ladeanimation als Placeholder. Dies trägt nicht nur zur besseren Performance bei, sondern auch einer erhöhten Usability der Anwendung. Daher lässt sich abschließend sagen, dass wenn man beim Entwickeln von Komponenten darauf achtet, die Notwendigkeit des (Re-)Renderns so gering wie möglich zu halten, die Performance deutlich gesteigert werden kann.
Mehr Artikel zu Blazor, ASP.NET Core, SPA
Kostenloser
Newsletter

Aktuelle Artikel, Screencasts, Webinare und Interviews unserer Experten für Sie

Verpassen Sie keine Inhalte zu Angular, .NET Core, Blazor, Azure und Kubernetes und melden Sie sich zu unserem kostenlosen monatlichen Dev-Newsletter an.

Newsletter Anmeldung
Diese Artikel könnten Sie interessieren
Database Access with Sessions
.NET
KP-round

Data Access in .NET Native AOT with Sessions

.NET 8 brings Native AOT to ASP.NET Core, but many frameworks and libraries rely on unbound reflection internally and thus cannot support this scenario yet. This is true for ORMs, too: EF Core and Dapper will only bring full support for Native AOT in later releases. In this post, we will implement a database access layer with Sessions using the Humble Object pattern to get a similar developer experience. We will use Npgsql as a plain ADO.NET provider targeting PostgreSQL.
15.11.2023
Old computer with native code
.NET
KP-round

Native AOT with ASP.NET Core – Overview

Originally introduced in .NET 7, Native AOT can be used with ASP.NET Core in the upcoming .NET 8 release. In this post, we look at the benefits and drawbacks from a general perspective and perform measurements to quantify the improvements on different platforms.
02.11.2023
ASP.NET Core
favicon

Architektur-Modernisierung: Migration von WCF zu gRPC mit ASP.NET Core – ein pragmatischer Ansatz

Viele Projekte mit verteilten Anwendungen in der .NET-Welt basieren noch auf der Windows Communication Foundation (WCF). Doch wie kommt man weg von der "Altlast" und wie stellt man seinen Code auf sowohl moderne als auch zukunftssichere Beine? Eine mögliche Lösung ist gRPC.

13.04.2023
Blazor
favicon

gRPC Code-First mit ASP.NET Core 7 und Blazor WebAssembly

Wie in allen anderen browserbasierten Single-Page-Application (SPA) Frameworks, ist Blazor WebAssembly JSON-over-HTTP (über Web- oder REST-APIs) die bei weitem häufigste Methode, um Daten auszutauschen und serverseitige Vorgänge auszulösen. Der Client sendet eine HTTP-Anfrage mit JSON-Daten an eine URL, mitunter über unterschiedliche HTTP-Verben. Anschließend führt der Server eine Operation aus und antwortet mit einem HTTP-Statuscode und den resultierenden JSON-Daten. Warum sollte man das ändern? Nun, es gibt Gründe - vor allem wenn man in einem geschlossenen System ist und .NET sowohl im Frontend als auch im Backend einsetzt.
30.03.2023
Blazor
sg

Understanding and Controlling the Blazor WebAssembly Startup Process

There are a lot of things going on in the background, when a Blazor WebAssembly application is being started. In some cases you might want to take a bit more control over that process. One example might be the wish to display a loading screen for applications that take some time for initial preparation, or when users are on a slow internet connection. However, in order to control something, we need to understand what is happening first. This article takes you down the rabbit hole of how a Blazor WASM application starts up.
07.03.2023
.NET
cl-neu

Adding Superpowers to your Blazor WebAssembly App with Project Fugu APIs

Blazor WebAssembly is a powerful framework for building web applications that run on the client-side. With Project Fugu APIs, you can extend the capabilities of these apps to access new device features and provide an enhanced user experience. In this article, learn about the benefits of using Project Fugu APIs, the wrapper packages that are available for Blazor WebAssembly, and how to use them in your application.

Whether you're a seasoned Blazor developer or just getting started, this article will help you add superpowers to your Blazor WebAssembly app.
28.02.2023