Bonnes pratiques sur le plan des performances de Blazor ASP.NET Core

Note

Ceci n’est pas la dernière version de cet article. Pour la version actuelle, consultez la version .NET 8 de cet article.

Important

Ces informations portent sur la préversion du produit, qui est susceptible d’être en grande partie modifié avant sa commercialisation. Microsoft n’offre aucune garantie, expresse ou implicite, concernant les informations fournies ici.

Pour la version actuelle, consultez la version .NET 8 de cet article.

Blazor est optimisé pour la haute performance dans la plupart des scénarios d’interface utilisateur d’application. Cependant, pour atteindre le meilleur niveau de performance, les développeurs doivent adopter les modèles et les fonctionnalités.

Remarque

Les exemples de code de cet article utilisent les types de référence null (NRT, nullable reference types) et l'analyse statique de l'état null du compilateur .NET, qui sont pris en charge dans ASP.NET Core 6 et ses versions ultérieures.

Optimiser la vitesse de rendu

Optimisez la vitesse de rendu de façon à réduire la charge de travail de rendu et à améliorer la réactivité de l’interface utilisateur, ce qui peut se traduire par une vitesse de rendu de l’interface utilisateur multipliée par dix ou plus.

Éviter le rendu inutile de sous-arborescences de composants

Vous pouvez peut-être éliminer une grande partie du coût de rendu d’un composant parent en ignorant le renouvellement du rendu des sous-arborescences de composants enfants qui intervient lorsqu’un événement se produit. Vous devez uniquement vous soucier des sous-arborescences dont le renouvellement du rendu est particulièrement onéreux et provoque le ralentissement de l’interface utilisateur.

Au moment de l’exécution, les composants sont hiérarchisés. Un composant racine (le premier composant chargé) possède des composants enfants. De même, les enfants de la racine présentent leurs propres composants enfants, et ainsi de suite. Quand un événement se produit, par exemple un utilisateur qui sélectionne un bouton, le processus suivant détermine les composants qui doivent faire l’objet d’un nouveau rendu :

  1. L’événement est distribué au composant qui a rendu le gestionnaire de l’événement. Après l’exécution du gestionnaire d’événements, le composant fait l’objet d’un nouveau rendu.
  2. À cette occasion, le composant adresse une nouvelle copie des valeurs de paramètres à chacun de ses composants enfants.
  3. Après avoir reçu un nouvel ensemble de valeurs de paramètres, chaque composant décide ou pas de renouveler le rendu. Par défaut, les composants le décident si les valeurs de paramètres ont changé, par exemple dans le cas d’objets mutables.

Les deux dernières étapes de la séquence précédente se poursuivent de manière récursive en descendant dans la hiérarchie des composants. Dans de nombreux cas, la sous-arborescence entière fait l’objet d’un nouveau rendu. Les événements ciblant les composants de haut niveau peuvent être à l’origine d’un nouveau rendu coûteux, car chaque composant situé en dessous du composant de haut niveau doit faire l’objet d’un nouveau rendu.

Pour empêcher la récursivité du rendu dans une sous-arborescence déterminée, utilisez l’une des approches suivantes :

  • Vérifiez que les paramètres des composants enfants sont de types primitifs immuables, tels que string, int, bool, DateTime et d’autres types similaires. La logique intégrée de détection des changements ignore automatiquement les nouvelles opérations de rendu si les valeurs de paramètres immuables primitifs n’ont pas changé. Si vous effectuez le rendu d’un composant enfant avec <Customer CustomerId="@item.CustomerId" />, sachant que CustomerId est de type int, le composant Customer ne fait pas l’objet d’un nouveau rendu, sauf si item.CustomerId change.
  • Substituer ShouldRender :
    • Pour accepter des valeurs de paramètres non primitifs, telles que des types de modèles personnalisés complexes, des rappels d’événements ou des valeurs de RenderFragment.
    • Si vous créez un composant d’interface utilisateur uniquement qui ne change pas après le rendu initial, qu’il y ait ou pas des changements de valeurs de paramètres.

Dans l’exemple d’outil de recherche de vols aériens suivant, des champs privés sont utilisés pour suivre les informations nécessaires à la détection des changements. L’identificateur de vol entrant précédent (prevInboundFlightId) et l’identificateur de vol sortant précédent (prevOutboundFlightId) assurent le suivi d’informations pour la prochaine mise à jour potentielle du composant. Si l’un des identificateurs de vol change quand les paramètres du composant sont définis dans OnParametersSet, le composant fait l’objet d’un nouveau rendu, car shouldRender est défini sur true. Si shouldRender est évalué à false après la vérification des identificateurs de vol, un nouveau rendu coûteux est évité :

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

Un gestionnaire d’événements peut aussi définir shouldRender sur true. Pour la plupart des composants, il n’est généralement pas nécessaire de déterminer s’il convient d’effectuer un nouveau rendu au niveau des gestionnaires d’événements individuels.

Pour plus d’informations, consultez les ressources suivantes :

Virtualisation

Quand le rendu porte sur de grandes quantités d’éléments d’interface utilisateur dans une boucle, par exemple une liste ou une grille constituée de milliers d’entrées, la quantité d’opérations de rendu peut entraîner un décalage dans le rendu de l’interface utilisateur. Sachant que l’utilisateur ne peut voir qu’un petit nombre d’éléments à la fois sans faire défiler l’écran, il est souvent déraisonnable de passer du temps à effectuer le rendu d’éléments qui ne sont actuellement pas visibles.

Blazor propose le composant Virtualize<TItem> pour créer l’apparence et les comportements de défilement d’une liste arbitrairement volumineuse tout en assurant le rendu des seuls éléments de liste qui se trouvent dans la fenêtre d’affichage active du défilement. Par exemple, un composant peut afficher une liste de 100 000 entrées, mais payer uniquement le coût de rendu de 20 éléments visibles.

Pour plus d’informations, consultez Virtualisation des composants ASP.NET Core Razor.

Créer des composants légers et optimisés

La plupart des composants Razor ne demandent pas un effort d’optimisation immense, car la plupart d’entre eux ne se répètent pas dans l’interface utilisateur et ne font pas l’objet d’un nouveau rendu à fréquence élevée. Par exemple, il est très probable que les composants routables avec une directive @page et les composants servant à afficher les éléments importants de l’interface utilisateur, tels que les boîtes de dialogue ou les formulaires, apparaissent seulement l’un après l’autre et que leur rendu se renouvelle en réponse à un geste de l’utilisateur. Ces composants ne créent généralement pas de charge de travail de rendu intense. Vous pouvez donc utiliser librement n’importe quelle combinaison de fonctionnalités du framework sans trop vous soucier des performances de rendu.

Cependant, il peut arriver que les composants soient répétés à grande échelle, ce qui nuit souvent aux performances de l’interface utilisateur, notamment dans les scénarios courants suivants :

  • Formulaires volumineux imbriqués avec des centaines d’éléments individuels, tels que des entrées ou des étiquettes.
  • Grilles comportant des centaines de lignes ou des milliers de cellules.
  • Nuages de points comportant des millions de points de données.

Si vous modélisez chaque élément, cellule ou point de données sous forme d’instance de composant distincte, il en existe souvent tellement que leurs performances de rendu deviennent critiques. Cette section fournit des conseils pour alléger ces composants et permettre ainsi à l’interface utilisateur de rester rapide et réactive.

Éviter la présence de milliers d’instances de composant

Chaque composant est une île distincte qui peut effectuer un rendu indépendamment de ses parents et enfants. En choisissant la façon dont l’interface utilisateur est divisée en une hiérarchie de composants, vous contrôlez la granularité du rendu de l’interface utilisateur. Le niveau de performance qui en résulte peut être bon ou mauvais.

En divisant l’interface utilisateur en différents composants, vous pouvez faire en sorte que le nouveau rendu porte sur des parties plus petites de l’interface utilisateur lorsque des événements se produisent. Si vous disposez d’une table constituée d’un grand nombre de lignes dotées chacune d’un bouton, vous pouvez faire en sorte que nouveau rendu porte sur une seule une ligne, et non sur la page ou la table entière, en utilisant un composant enfant. Chaque, chaque composant a besoin d’une surcharge de mémoire et de processeur supplémentaire pour gérer son état et son cycle de vie de rendu indépendants.

À l’occasion d’un test effectué par les ingénieurs de l’unité produit ASP.NET Core, une surcharge de rendu d’environ 0,06 ms par instance de composant a été observée dans une application Blazor WebAssembly. L’application de test a effectué le rendu d’un composant simple qui accepte trois paramètres. En interne, la surcharge est en grande partie due à la récupération de l’état par composant auprès des dictionnaires ainsi qu’à la transmission et à la réception des paramètres. Par multiplication, vous pouvez constater que l’ajout de 2 000 instances de composant supplémentaires ajouterait 0,12 seconde au temps de rendu et que les utilisateurs commenceraient à trouver que l’interface utilisateur est lente.

Il est possible d’alléger les composants pour vous permettre d’en avoir davantage. Cependant, une technique plus puissante est souvent d’éviter d’avoir autant de composants à afficher. Les sections suivantes décrivent deux approches que vous pouvez adopter.

Si vous souhaitez en savoir plus sur la gestion de la mémoire, veuillez consulter la rubrique Héberger et déployer des applications Blazor ASP.NET Core côté serveur.

Intégrer des composants enfants dans leurs parents

Examinez la partie suivante d’un composant parent qui assure le rendu des composants enfants dans une boucle :

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

L’exemple précédent fonctionne bien à condition qu’il n’y ait pas plusieurs milliers de messages à afficher simultanément. Pour afficher des milliers de messages simultanément, évitez de factoriser le composant ChatMessageDisplay seul. Au lieu de cela, intégrez le composant enfant dans le parent. L’approche suivante évite la surcharge par composant liée au rendu d’un si grand nombre de composants enfants mais fait perdre la possibilité d’un nouveau rendu du balisage de chaque composant enfant de manière indépendante :

<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>
Définir des éléments RenderFragments réutilisables dans le code

Vous pouvez factoriser des composants enfants dans le seul but de réutiliser la logique de rendu. Si c’est le cas, vous pouvez créer une logique de rendu réutilisable sans implémenter de composants supplémentaires. Dans le bloc @code d’un composant, définissez un RenderFragment. Effectuez le rendu du fragment de n’importe où autant de fois que nécessaire :

@RenderWelcomeInfo

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

@RenderWelcomeInfo

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

Pour rendre le code RenderTreeBuilder réutilisable dans plusieurs composants, déclarez le RenderFragmentpublic et static :

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

SayHello dans l’exemple précédent peut être appelé à partir d’un composant non lié. Cette technique est utile pour créer des bibliothèques d’extraits de balisage réutilisables qui s’affichent sans surcharge par composant.

Les délégués RenderFragment peuvent accepter des paramètres. Le composant suivant transmet le message (message) au délégué RenderFragment :

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

L’approche précédente réutilise la logique de rendu sans surcharge par composant. Cependant, l’approche ne permet pas d’actualiser la sous-arborescence de l’interface utilisateur de manière indépendante, ni d’ignorer le rendu de la sous-arborescence de l’interface utilisateur lorsque son parent est rendu, car il n’y a pas de limite de composants. L’affectation à un délégué RenderFragment est uniquement prise en charge dans les fichiers de composant Razor (.razor), et les rappels d’événements ne sont pas pris en charge.

Pour un champ, une méthode ou une propriété non statique qui ne peut pas être référencé par un initialiseur de champ, comme TitleTemplate dans l’exemple suivant, utilisez une propriété plutôt qu’un champ pour le RenderFragment :

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

Ne pas recevoir trop de paramètres

Si un composant se répète extrêmement souvent, par exemple des centaines voire des milliers de fois, la surcharge liée à la transmission et à la réception de chaque paramètre augmente.

Il est rare qu’un trop grand nombre de paramètres limite sévèrement le niveau de performance, mais il peut s’agir d’un facteur. Pour un composant TableCell dont le rendu se produit 4 000 fois dans une grille, chaque paramètre transmis au composant ajoute environ 15 ms au coût de rendu total. La transmission de dix paramètres nécessite environ 150 ms et entraîne un décalage du rendu de l’interface utilisateur.

Pour réduire la charge de paramètres, regroupez plusieurs paramètres dans une classe personnalisée. Par exemple, un composant de cellules de table peut accepter un objet commun. Dans l’exemple suivant, Data est différent pour chaque cellule, mais Options est commun à toutes les instances de cellule :

@typeparam TItem

...

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

Cependant, il peut être préférable de ne pas avoir de composant de cellules de table, comme le montre l’exemple précédent, et de plutôt intégrer sa logique dans le composant parent.

Note

Quand il existe plusieurs approches pour améliorer le niveau de performance, il est généralement nécessaire d’effectuer une analyse comparative de ces approches pour déterminer celle qui offre les meilleurs résultats.

Pour plus d’informations sur les paramètres de type générique (@typeparam), consultez les ressources suivantes :

Vérifier que les paramètres en cascade sont corrigés

Le composant CascadingValue possède un paramètre IsFixed facultatif :

  • Si IsFixed a la valeur false (par défaut), chaque destinataire de la valeur en cascade configure un abonnement pour recevoir des notifications de modification. Chaque [CascadingParameter] est nettement plus cher qu’un [Parameter] standard en raison du suivi d’abonnement.
  • Si IsFixed a la valeur true (par exemple, <CascadingValue Value="someValue" IsFixed="true">), les destinataires reçoivent la valeur initiale, mais ne configurent pas un abonnement pour recevoir des mises à jour. Chaque [CascadingParameter] est léger et pas plus cher qu’un [Parameter] normal.

Le fait de définir IsFixed sur true a pour effet d’améliorer le niveau de performance si un grand nombre d’autres composants reçoivent la valeur en cascade. Dans la mesure du possible, définissez IsFixed sur true pour les valeurs en cascade. Vous pouvez définir IsFixed sur true quand la valeur fournie ne change pas dans le temps.

Quand un composant transmet this en tant que valeur en cascade, IsFixed peut aussi être défini sur true :

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

Pour plus d’informations, consultez Valeurs et paramètres en cascade de Blazor ASP.NET Core.

Éviter la projection d’attributs avec CaptureUnmatchedValues

Les composants peuvent choisir de recevoir des valeurs de paramètres « sans correspondance » en utilisant l’indicateur CaptureUnmatchedValues :

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

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

Cette approche permet de transmettre d’autres attributs arbitraires à l’élément. Or, cette approche est coûteuse, car le convertisseur doit :

  • Faire correspondre tous les paramètres fournis au jeu de paramètres connus pour créer un dictionnaire.
  • Enregistrer la façon dont plusieurs copies d’un même attribut se remplacent mutuellement.

Utilisez CaptureUnmatchedValues là où les performances de rendu des composants ne sont pas critiques, notamment dans le cas des composants qui ne sont pas répétés fréquemment. Pour les composants dont le rendu s’effectue à grande échelle, par exemple pour chaque élément d’une liste volumineuse ou des cellules d’une grille, essayez d’éviter la projection d’attributs.

Pour plus d’informations, consultez ASP.NET Core Blazor paramètres de platissement d’attributs et arbitraires.

Implémenter SetParametersAsync manuellement

Une source importante de surcharge de rendu par composant est l’écriture des valeurs de paramètres entrantes dans les propriétés [Parameter]. Le renderer utilise la réflexion pour écrire les valeurs de paramètres, ce qui peut nuire au niveau de performance à grande échelle.

Dans certains cas extrêmes, vous pouvez souhaiter éviter la réflexion et implémenter manuellement votre propre logique de définition de paramètres. Cela peut être pertinent dans les cas suivants :

  • Un composant fait l’objet d’un rendu extrêmement fréquent, par exemple quand il existe des centaines ou des milliers de copies du composant dans l’interface utilisateur.
  • Un composant accepte de nombreux paramètres.
  • Vous constatez que la surcharge liée à la réception de paramètres a un impact observable sur la réactivité de l’interface utilisateur.

Dans les cas extrêmes, vous pouvez remplacer la méthode SetParametersAsync virtuelle du composant et implémenter votre propre logique propre au composant. L’exemple suivant évite délibérément les recherches dans un dictionnaire :

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

Dans le code précédent, le retour de la classe de base SetParametersAsync exécute la méthode de cycle de vie normale sans attribuer une nouvelle fois des paramètres.

Comme vous pouvez le constater dans le code précédent, remplacer SetParametersAsync et fournir une logique personnalisée sont des tâches complexes et laborieuses. C’est pour cette raison que nous déconseillons généralement d’adopter cette approche. Dans les cas extrêmes, elle peut améliorer les performances de rendu de 20 à 25 %, mais réservez plutôt cette approche aux scénarios extrêmes cités plus haut dans cette section.

Ne pas déclencher les événements trop rapidement

Certains événements de navigateur se déclenchent très fréquemment. Par exemple, onmousemove et onscroll peuvent se déclencher des dizaines voire des centaines de fois à la seconde. Dans la plupart des cas, vous n’avez pas besoin d’effectuer de mises à jour fréquentes de l’interface utilisateur. Si les événements sont déclenchés trop rapidement, vous risquez de nuire à la réactivité de l’interface utilisateur ou de consommer un temps processeur excessif.

Plutôt que d’utiliser des événements natifs qui se déclenchent rapidement, envisagez d’utiliser l’interopérabilité JS pour inscrire un rappel qui se déclenche moins fréquemment. Par exemple, le composant suivant affiche la position de la souris, mais ne se met à jour qu’une fois toutes les 500 ms au maximum :

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

Le code JavaScript correspondant inscrit l’écouteur d’événements DOM pour le déplacement de la souris. Dans cet exemple, l’écouteur d’événements utilise la fonction de throttle Lodash pour limiter le taux d’appels :

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

Éviter le renouvellement du rendu après avoir géré des événements sans changement d’état

Par défaut, les composants héritent de ComponentBase, qui appelle automatiquement StateHasChanged après l’appel des gestionnaires d’événements du composant. Dans certains cas, il peut être inutile voire inopportun de déclencher un nouveau rendu après l’appel d’un gestionnaire d’événements. Par exemple, il peut arriver qu’un gestionnaire d’événements ne modifie pas l’état du composant. Dans ces scénarios, l’application peut tirer parti de l’interface IHandleEvent pour contrôler le comportement de la gestion d’événements de Blazor.

Pour empêcher les nouveaux rendus pour tous les gestionnaires d’événements d’un composant, implémentez IHandleEvent et fournissez une tâche IHandleEvent.HandleEventAsync qui appelle le gestionnaire d’événements sans appeler StateHasChanged.

Dans l’exemple suivant, aucun gestionnaire d’événements ajouté au composant ne déclenche de nouveau rendu. Ainsi, HandleSelect n’occasionne pas de nouveau rendu quand il est appelé.

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

En plus d’empêcher les nouveaux rendus après le déclenchement de gestionnaires d’événements dans un composant de manière globale, il est possible d’empêcher les nouveaux rendus après le déclenchement d’un seul gestionnaire d’événements en utilisant la méthode utilitaire suivante.

Ajoutez la classe EventUtil ci-dessous à une application Blazor. Les actions et fonctions statiques situées en haut de la classe EventUtil fournissent des gestionnaires qui couvrent plusieurs combinaisons d’arguments et de types de retour dont se sert Blazor pendant la gestion des événements.

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

Appelez EventUtil.AsNonRenderingEventHandler pour appeler un gestionnaire d’événements qui ne déclenche pas de rendu quand il est appelé.

Dans l’exemple suivant :

  • La sélection du premier bouton, qui appelle HandleClick1, déclenche un nouveau rendu.
  • La sélection du deuxième bouton, qui appelle HandleClick2, ne déclenche pas de nouveau rendu.
  • La sélection du troisième bouton, qui appelle HandleClick3, ne déclenche pas de nouveau rendu et utiliser elle activation et utilise des arguments d’événement (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);
    }
}

En plus d’implémenter l’interface IHandleEvent, la mise à profit des autres bonnes pratiques décrites dans cet article peut aussi contribuer à réduire les rendus indésirables après la gestion d’événements. Par exemple, le remplacement de ShouldRender dans les composants enfants du composant cible permet de contrôler le renouvellement du rendu.

Éviter de recréer des délégués pour de nombreux éléments ou composants répétés

La recréation par Blazor de délégués d’expression lambda pour les éléments ou les composants d’une boucle peut avoir un impact négatif sur les performances.

Le composant suivant présenté dans l’article sur la gestion des événements assure le rendu d’un ensemble de boutons. Chaque bouton attribue un délégué à son événement @onclick, ce qui est acceptable s’il n’y a pas beaucoup de boutons à afficher.

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

Si un grand nombre de boutons sont rendus à l’aide de l’approche précédente, la vitesse de rendu est impactée négativement, ce qui entraîne une expérience utilisateur médiocre. Pour afficher un grand nombre de boutons avec un rappel pour les événements de clic, l’exemple suivant utilise une collection d’objets de bouton qui affectent le délégué @onclick de chaque bouton à un Action. L’approche suivante n’a pas besoin de Blazor pour reconstruire tous les délégués de bouton à chaque rendu des boutons :

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

Optimiser la vitesse d’interopérabilité JavaScript

Les appels entre .NET et JavaScript nécessitent une surcharge supplémentaire, car :

  • Par défaut, les appels sont asynchrones.
  • Par défaut, les paramètres et les valeurs de retour sont sérialisés par JSON pour offrir un mécanisme de conversion facile à comprendre entre les types .NET et JavaScript.

En outre, pour les applications Blazor côté serveur, ces appels sont passés sur le réseau.

Éviter les appels à granularité excessivement fine

Sachant que chaque appel implique une certaine surcharge, il peut être utile de réduire le nombre d’appels. Examinez le code suivant, qui stocke une collection d’éléments dans le localStorage du navigateur :

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

L’exemple précédent effectue un appel d’interopérabilité JS distinct pour chaque élément. Au lieu de cela, l’approche suivante réduit l’interopérabilité JS à un seul appel :

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

La fonction JavaScript correspondante stocke toute la collection d’éléments sur le client :

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

Pour les applications Blazor WebAssembly, le regroupement de différents appels d’interopérabilité JS dans un même appel a généralement pour effet d’améliorer nettement les performances à condition que le composant effectue un grand nombre d’appels d’interopérabilité JS.

Envisager l’utilisation d’appels synchrones

Appeler JavaScript à partir de .NET

Cette section s'applique uniquement aux composants côté client.

Les appels d’interopérabilité JS sont asynchrones par défaut, que le code appelé soit synchrone ou asynchrone. Les appels sont asynchrones par défaut pour garantir que les composants sont compatibles entre les modes de rendu côté serveur et côté client. Sur le serveur, tous les appels interop JS doivent être asynchrones car ils sont envoyés via une connexion réseau.

Si vous savez avec certitude que votre composant s'exécute uniquement sur WebAssembly, vous pouvez choisir d'effectuer des appels interop synchrones JS. Cela représente un peu moins de surcharge que d’effectuer des appels asynchrones, et peut entraîner moins de cycles de rendu, car il n’y a pas d’état intermédiaire lors de l’attente des résultats.

Pour effectuer un appel synchrone de .NET vers JavaScript dans un composant côté client, convertissez IJSRuntime en IJSInProcessRuntime pour effectuer l'appel d'interopérabilité JS :

@inject IJSRuntime JS

...

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

Lorsque vous travaillez avec IJSObjectReference des composants côté client dans ASP.NET Core 5.0 ou version ultérieure, vous pouvez utiliser IJSInProcessObjectReference de manière synchrone. IJSInProcessObjectReference implémente IAsyncDisposable/IDisposable et doit être supprimé à des fins de nettoyage de la mémoire pour empêcher une fuite de mémoire, comme l’illustre l’exemple suivant :

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

Appeler .NET à partir de JavaScript

Cette section s'applique uniquement aux composants côté client.

Les appels d’interopérabilité JS sont asynchrones par défaut, que le code appelé soit synchrone ou asynchrone. Les appels sont asynchrones par défaut pour garantir que les composants sont compatibles entre les modes de rendu côté serveur et côté client. Sur le serveur, tous les appels interop JS doivent être asynchrones car ils sont envoyés via une connexion réseau.

Si vous savez avec certitude que votre composant s'exécute uniquement sur WebAssembly, vous pouvez choisir d'effectuer des appels interop synchrones JS. Cela représente un peu moins de surcharge que d’effectuer des appels asynchrones, et peut entraîner moins de cycles de rendu, car il n’y a pas d’état intermédiaire lors de l’attente des résultats.

Pour effectuer un appel synchrone de JavaScript vers .NET dans un composant côté client, utilisez DotNet.invokeMethod à la place de DotNet.invokeMethodAsync.

Les appels synchrones fonctionnent si :

  • Le composant est uniquement rendu pour exécution sur WebAssembly.
  • La fonction appelée retourne une valeur de manière synchrone. La fonction n’est pas une méthode async et ne retourne pas de Task .NET ou Promise JavaScript.

Cette section s'applique uniquement aux composants côté client.

Les appels d’interopérabilité JS sont asynchrones par défaut, que le code appelé soit synchrone ou asynchrone. Les appels sont asynchrones par défaut pour garantir que les composants sont compatibles entre les modes de rendu côté serveur et côté client. Sur le serveur, tous les appels interop JS doivent être asynchrones car ils sont envoyés via une connexion réseau.

Si vous savez avec certitude que votre composant s'exécute uniquement sur WebAssembly, vous pouvez choisir d'effectuer des appels interop synchrones JS. Cela représente un peu moins de surcharge que d’effectuer des appels asynchrones, et peut entraîner moins de cycles de rendu, car il n’y a pas d’état intermédiaire lors de l’attente des résultats.

Pour effectuer un appel synchrone de .NET vers JavaScript dans un composant côté client, convertissez IJSRuntime en IJSInProcessRuntime pour effectuer l'appel d'interopérabilité JS :

@inject IJSRuntime JS

...

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

Lorsque vous travaillez avec IJSObjectReference des composants côté client dans ASP.NET Core 5.0 ou version ultérieure, vous pouvez utiliser IJSInProcessObjectReference de manière synchrone. IJSInProcessObjectReference implémente IAsyncDisposable/IDisposable et doit être supprimé à des fins de nettoyage de la mémoire pour empêcher une fuite de mémoire, comme l’illustre l’exemple suivant :

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

Envisager l’utilisation d’appels démarshalés

Cette section s’applique uniquement aux applications Blazor WebAssembly.

Quand l’exécution s’effectue sur Blazor WebAssembly, il est possible d’effectuer des appels démarshalés de .NET vers JavaScript. Il s’agit d’appels synchrones qui n’effectuent pas une sérialisation JSON des arguments ou valeurs de retour. Tous les aspects de la gestion de mémoire et les traductions entre les représentations .NET et JavaScript translation sont laissés au développeur.

Avertissement

Bien que l’utilisation de IJSUnmarshalledRuntime offre la surcharge la plus faible parmi les approches d’interopérabilité JS, les API JavaScript nécessaires pour interagir avec ces API ne sont actuellement pas documentées et pourraient faire l’objet de changements cassants dans les futures versions.

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

Utiliser l’interopérabilité [JSImport]/[JSExport] JavaScript

L’interopérabilité [JSImport]/[JSExport] JavaScript pour les applications Blazor WebAssembly offre un niveau de performance et une stabilité améliorés par rapport à l’API d’interopérabilité JS des versions du framework antérieures à ASP.NET Core dans .NET 7.

Pour plus d’informations, consultez interopérabilité JSImport/JSExport JavaScript avec Blazor ASP.NET Core.

Compilation anticipée (AOT)

La compilation anticipée (AOT, « Ahead-of-time ») compile le code .NET d’une application Blazor directement en WebAssembly natif pour une exécution directe par le navigateur. Si la compilation AOT génère des applications plus volumineuses qui prennent plus de temps à télécharger, elles offrent généralement de meilleures performances d’exécution, en particulier dans le cas des applications qui exécutent des tâches gourmandes en ressources processeur. Pour plus d’informations, consultez Outils de génération ASP.NET Core Blazor WebAssembly et compilation AOT (ahead-of-time).

Réduire la taille de téléchargement de l’application

Nouvelle liaison du runtime

Pour plus d’informations sur la façon dont la liaison du runtime réduit la taille de téléchargement d’une application, consultez Outils de génération ASP.NET Core Blazor WebAssembly et compilation AOT (ahead-of-time).

Utilisez System.Text.Json.

L’implémentation de l’interopérabilité JS de Blazor s’appuie sur System.Text.Json, qui est une bibliothèque de sérialisation JSON hautes performances offrant une faible allocation de mémoire. En utilisant System.Text.Json, la taille de charge d’utile d’application ne doit être supérieure à celle résultant de l’ajout d’une ou plusieurs autres bibliothèques JSON.

Pour obtenir une aide pour la migration, consultez Guide pratique pour migrer de Newtonsoft.Json vers System.Text.Json.

Suppression de code en langage intermédiaire (IL)

Cette section s’applique uniquement aux applications Blazor WebAssembly.

La suppression des assemblys non utilisés dans une application Blazor WebAssembly a pour effet de réduire la taille de l’application en supprimant le code inutilisé dans les fichiers binaires de l’application. Pour plus d’informations, consultez Configurer l’outil de suppression pour Blazor ASP.NET Core.

La liaison d’une application Blazor WebAssembly a pour effet de réduire la taille de l’application en supprimant le code non utilisé dans les fichiers binaires de l’application. Par défaut, l’éditeur de liens du langage intermédiaire (IL) n’est activé que lors de la génération dans la configuration Release. Pour en bénéficier, publiez l’application pour déploiement à l’aide de la commande dotnet publish avec l’option -c|--configuration définie sur Release :

dotnet publish -c Release

Charger des assemblys en mode différé

Cette section s’applique uniquement aux applications Blazor WebAssembly.

Chargez des assemblys au moment de l’exécution quand leur utilisation est requise par une route. Pour plus d’informations, consultez Charger des assemblys en mode différé dans ASP.NET Core Blazor WebAssembly.

Compression

Cette section s’applique uniquement aux applications Blazor WebAssembly.

Quand une application Blazor WebAssembly est publiée, la sortie est compressée statiquement pendant la publication pour réduire la taille de l’application et ôter la surcharge liée à la compression du runtime. Blazor s’appuie sur le serveur pour effectuer la négociation de contenu et fournir des fichiers compressés de manière statique.

Une fois qu’une application est déployée, vérifiez qu’elle fournit des fichiers compressés. Inspectez l’onglet Réseau dans les outils de développement d’un navigateur et vérifiez que les fichiers sont fournis avec Content-Encoding: br (compression Brotli) ou Content-Encoding: gz (compression Gzip). Si l’hôte ne fournit pas de fichiers compressés, suivez les instructions fournies dans Héberger et déployer Blazor WebAssembly ASP.NET Core.

Désactiver les fonctionnalités non utilisées

Cette section s’applique uniquement aux applications Blazor WebAssembly.

Le runtime de Blazor WebAssembly inclut les fonctionnalités .NET suivantes qui peuvent être désactivées pour réduire la taille de la charge utile :

  • Un fichier de données est inclus pour que les informations de fuseau horaire soient correctes. Si l’application n’a pas besoin de cette fonctionnalité, désactivez-la éventuellement en attribuant à la propriété MSBuild BlazorEnableTimeZoneSupport au niveau du fichier projet de l’application la valeur false :

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • Des informations de classement sont incluses pour assurer un bon fonctionnement d’API telles que StringComparison.InvariantCultureIgnoreCase. Si vous avez la certitude que l’application n’a pas besoin des données de classement, envisagez de la désactiver en attribuant à la propriété MSBuild BlazorWebAssemblyPreserveCollationData au niveau du fichier projet de l’application la valeur false :

    <PropertyGroup>
      <BlazorWebAssemblyPreserveCollationData>false</BlazorWebAssemblyPreserveCollationData>
    </PropertyGroup>
    
  • Par défaut, Blazor WebAssembly contient les ressources de globalisation nécessaires pour afficher les valeurs, telles que les dates et la devise, dans la culture de l’utilisateur. Si l’application n’a pas besoin d’être localisée, vous pouvez configurer l’application pour prendre en charge la culture invariante, qui est basée sur la culture en-US.