Compartir a través de


Procedimientos recomendados de rendimiento de Blazor en ASP.NET Core

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión de .NET 9 de este artículo.

Advertencia

Esta versión de ASP.NET Core ya no se admite. Para obtener más información, consulta la Directiva de soporte técnico de .NET y .NET Core. Para la versión actual, consulta la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión de .NET 9 de este artículo.

Blazor está optimizado para un alto rendimiento en escenarios de interfaz de usuario de aplicaciones más realistas. Pero un mejor rendimiento depende de que los desarrolladores adopten los patrones y características correctos.

Nota:

En los ejemplos de código de este artículo se adoptan tipos de referencia que admiten un valor NULL (NRT) y análisis estático de estado NULL del compilador de .NET, que se admiten en ASP.NET Core en .NET 6 o posterior.

Optimización de la velocidad de representación

Optimiza la velocidad de representación para minimizar la carga de trabajo de representación y mejorar la capacidad de respuesta de la interfaz de usuario, lo que puede producir una mejora de diez veces o superior en la velocidad de representación de la interfaz de usuario.

Evitación de la representación innecesaria de subárboles de componentes

Puedes quitar la mayoría del coste de representación de un componente primario omitiendo la representación de subárboles de componentes secundarios cuando se produce un evento. Solo debes preocuparte por omitir los subárboles de nueva representación si estos tienen una representación especialmente costosa y están causando un retraso en la IU.

En el entorno de ejecución, los componentes existen en una jerarquía. Un componente raíz (el primer componente cargado) tiene componentes secundarios. A su vez, los elementos secundarios de la raíz tienen sus propios componentes secundarios, etc. Cuando se produce un evento, como la selección de un botón por un usuario, el proceso siguiente decide qué componentes se representarán:

  1. El evento se envía al componente que represente el controlador del evento. Después de ejecutar el controlador de eventos, el componente se volverá a representar.
  2. Cuando se vuelve a representar un componente, este proporciona una nueva copia de los valores de los parámetros a cada uno de sus componentes secundarios.
  3. Después de recibir un nuevo conjunto de valores de parámetros, cada componente decide si quiere volver a representarlo. Los componentes se vuelven a representar si los valores de los parámetros pueden haber cambiado, por ejemplo, si son objetos mutables.

Los dos últimos pasos de la anterior secuencia continúan de forma recursiva hacia abajo en la jerarquía de componentes. En muchos casos, se volverá a representar el subárbol completo. Los eventos que tienen como destino componentes de alto nivel pueden provocar que la nueva representación sea costosa porque se deben volver a representar todos los componentes por debajo del componente de alto nivel.

Para evitar la recursividad de la representación en un subárbol determinado, usa cualquiera de los enfoques siguientes:

  • Asegúrate de que los parámetros de componentes secundarios son de tipos primitivos inmutables, comostring, int, bool, DateTime y otros tipos similares. La lógica integrada para detectar cambios omite automáticamente la nueva representación si los valores de los parámetros inmutables primitivos no han cambiado. Si representas un componente secundario con <Customer CustomerId="item.CustomerId" />, donde CustomerId es un tipo de int, no se volverá a representar el componente Customer a menos que cambie item.CustomerId.
  • Invalidar ShouldRender:
    • Para aceptar valores de parámetros no primitivos, como tipos de modelos personalizados complejos, devoluciones de llamada de eventos o valores RenderFragment.
    • Si creas un componente de solo interfaz de usuario que nunca cambia tras la representación inicial, independientemente de los cambios en el valor del parámetro.

En el ejemplo siguiente de una herramienta de búsqueda de vuelos de una aerolínea se usan campos privados para realizar un seguimiento de la información necesaria para detectar cambios. El identificador de vuelo de entrada anterior (prevInboundFlightId) y el de salida anterior (prevOutboundFlightId) realizan un seguimiento de la información de la siguiente posible actualización del componente. Si alguno de los identificadores de vuelo cambia cuando los parámetros del componente se establecen en OnParametersSet, el componente se vuelve a representar porque shouldRender está establecido en true. Si shouldRender se evalúa como false después de comprobar los identificadores de vuelo, se evita una nueva representación costosa:

@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 controlador de eventos también puede establecer shouldRender en true. Para la mayoría de los componentes, no suele ser necesario determinar la nueva representación en el nivel de controladores de eventos individuales.

Para obtener más información, consulta los siguientes recursos:

Virtualización

Al representar grandes cantidades de interfaz de usuario dentro de un bucle, por ejemplo, una lista o una cuadrícula con miles de entradas, la gran cantidad de operaciones de representación puede provocar un retraso en la representación de la interfaz de usuario. Dado que el usuario solo puede ver un pequeño número de elementos a la vez sin desplazarse, a menudo no vale la pena dedicarle tiempo a representar elementos que no son visibles actualmente.

Blazor proporciona el componente Virtualize<TItem> que crea los comportamientos de apariencia y desplazamiento de una lista arbitrariamente grande, pero que solo representa los elementos de lista que están dentro de la ventanilla de desplazamiento actual. Por ejemplo, un componente puede representar una lista con 100 000 entradas, pero solo debe pagar el costo de representación de 20 elementos que son visibles.

Para obtener más información, consulta Virtualización de componentes Razor de ASP.NET Core.

Creación de componentes ligeros y optimizados

La mayoría de los componentes Razor no requieren esfuerzos de optimización agresivos porque la mayoría de los componentes no se repiten en la interfaz de usuario y no se representan con alta frecuencia. Por ejemplo, los componentes enrutables con una directiva @page y los componentes que se usan para representar partes de la interfaz de usuario de alto nivel como cuadros de diálogo o formularios, lo más probable es que solo aparezcan de uno en uno y que solo se vuelvan a representar en respuesta a un gesto del usuario. Estos componentes no suelen suponer una gran carga de trabajo de representación, por lo que puedes usar libremente cualquier combinación de características del marco sin preocuparte demasiado por el rendimiento de la representación.

Pero hay escenarios comunes en los que los componentes se repiten a escala y a menudo tienen como resultado un rendimiento deficiente de la interfaz de usuario:

  • Formularios anidados grandes con cientos de elementos individuales, como entradas o etiquetas.
  • Cuadrículas con cientos de filas o miles de celdas.
  • Gráficos de dispersión con millones de puntos de datos.

Al modelar cada elemento, celda o punto de datos como una instancia de componente independiente, suele haber tantos que su rendimiento de representación se ve gravemente afectado. En esta sección se proporcionan consejos sobre cómo crear estos componentes ligeros para que la interfaz de usuario se mantenga rápida y con capacidad de respuesta.

Evitación de miles de instancias de componentes

Cada componente es una isla independiente que se puede representar independientemente de sus elementos primarios y secundarios. Al elegir cómo dividir la interfaz de usuario en una jerarquía de componentes, toma el control sobre el nivel de detalle de la representación de la interfaz de usuario. Esto puede dar lugar a un rendimiento bueno o deficiente.

Al dividir la interfaz de usuario en componentes independientes, puede ocurrir que se vuelvan a representar partes más pequeñas de la interfaz de usuario cuando se produzcan eventos. En una tabla con muchas filas que tienen un botón en cada fila, es posible que solo se pueda volver a representar esa única fila mediante un componente secundario en lugar de toda la página o tabla. Pero cada componente requiere memoria adicional y sobrecarga de CPU para tratar con su estado independiente y su ciclo de vida de representación.

En una prueba realizada por los ingenieros de unidad de producto de ASP.NET Core, se ha observado una sobrecarga de representación de aproximadamente 0,06 ms por instancia de componente en una aplicación Blazor WebAssembly. La aplicación de prueba representó un componente simple que acepta tres parámetros. Internamente, la sobrecarga se debe en gran parte a la recuperación del estado de cada componente de los diccionarios y al paso y recepción de parámetros. Mediante la multiplicación, puedes ver que agregar 2000 instancias de componentes adicionales agregaría 0,12 segundos al tiempo de representación y la interfaz de usuario comenzaría a ser lenta para los usuarios.

Es posible hacer que los componentes sean más ligeros para que pueda tener mayor cantidad, pero con frecuencia una técnica más eficaz suele ser evitar tener que representar tantos componentes. En las secciones siguientes se describen las dos opciones que tiene a su disposición.

Para obtener más información sobre la administración de memoria, consulta Hospedaje e implementación de aplicaciones de Blazor del lado servidor de ASP.NET Core.

Componentes secundarios insertados en sus elementos primarios

Ten en cuenta la siguiente parte de un componente primario que representa los componentes secundarios en un bucle:

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

El ejemplo anterior se ejecuta correctamente si no se muestran miles de mensajes a la vez. Para mostrar miles de mensajes a la vez, considera la posibilidad de no separar el componente ChatMessageDisplay independiente. En su lugar, inserta el componente secundario en el elemento primario. El siguiente enfoque evita la sobrecarga por componente de representar tantos componentes secundarios a costa de perder la capacidad de volver a representar el marcado de cada componente secundario de forma independiente:

<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>
Definición de RenderFragments reutilizables en el código

Puede que estés separando los componentes secundarios únicamente como una manera de reutilizar la lógica de representación. Si ese es el caso, puedes crear lógica de representación reutilizable sin implementar componentes adicionales. En el bloque @code de cualquier componente, defina RenderFragment. Representa el fragmento desde cualquier ubicación tantas veces como sea necesario:

@RenderWelcomeInfo

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

@RenderWelcomeInfo

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

Para que el código RenderTreeBuilder sea reutilizable en varios componentes, declare RenderFragment public y static:

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

SayHello en el ejemplo anterior se puede invocar desde un componente no relacionado. Esta técnica resulta útil para crear bibliotecas de fragmentos de marcado reutilizables que se representan sin sobrecarga por componente.

Los delegados de RenderFragment pueden aceptar parámetros. El componente siguiente pasa el mensaje (message) al delegado 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>;
}

El enfoque anterior reutiliza la lógica de representación sin la sobrecarga por componente. Sin embargo, el enfoque no permite actualizar el subárbol de la interfaz de usuario de forma independiente, ni tampoco tiene la capacidad de omitir la representación de dicho subárbol cuando su elemento primario se representa, ya que no hay ningún límite de componente. La asignación a un delegado de RenderFragment solo se admite en archivos de componentes de Razor (.razor) y no se admiten devoluciones de llamada de eventos.

Para un campo, método o propiedad no estáticos al que no pueda hacer referencia un inicializador de campo, como TitleTemplate en el ejemplo siguiente, use una propiedad en lugar de un campo para RenderFragment:

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

No recepción de demasiados parámetros

Si un componente se repite con mucha frecuencia, por ejemplo, cientos o miles de veces, la sobrecarga de pasar y recibir cada parámetro aumenta.

Es raro que demasiados parámetros restrinjan gravemente el rendimiento, pero puede ser un factor. En el caso de un componente TableCell que se represente 4 000 veces dentro de una cuadrícula, cada parámetro que se le pase al componente agrega alrededor de 15 ms al costo total de representación. Pasar diez parámetros requiere alrededor de 150 ms y provoca un retraso en la representación de la interfaz de usuario.

Para reducir la carga de parámetros, agrupe varios en una clase personalizada. Por ejemplo, un componente de celda de tabla podría aceptar un objeto común. En el ejemplo siguiente, Data es diferente para cada celda, pero Options es común en todas las celdas:

@typeparam TItem

...

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

Pero considere que podría ser una mejora no tener un componente de celda de tabla, como se muestra en el ejemplo anterior, y en su lugar alinear su lógica en el componente primario.

Nota:

Cuando hay varios enfoques disponibles para mejorar el rendimiento, normalmente es necesario realizar pruebas comparativas de los enfoques para determinar cuál produce los mejores resultados.

Para obtener más información sobre los parámetros de tipo genérico (@typeparam), consulte los siguientes recursos:

Garantía de que los parámetros en cascada son fijos

El componente CascadingValue tiene un parámetro opcional denominado IsFixed:

  • Si el valor IsFixed es false (el predeterminado), cada destinatario del valor en cascada configura una suscripción para recibir notificaciones de los cambios. Cada [CascadingParameter] es sustancialmente más caro que un parámetro [Parameter] normal debido al seguimiento de las suscripciones.
  • Si el valor IsFixed es true (por ejemplo, <CascadingValue Value="someValue" IsFixed="true">), los destinatarios reciben el valor inicial, pero no configuran ninguna suscripción para recibir actualizaciones. Cada [CascadingParameter] es ligero y no es más caro que un parámetro [Parameter] normal.

Establecer IsFixed en true mejora el rendimiento si hay un gran número de componentes diferentes que reciben el valor en cascada. Siempre que sea posible, establezca IsFixed en true en valores en cascada. Puede establecer IsFixed en true cuando el valor proporcionado no cambie con el tiempo.

Cuando un componente pasa this como un valor en cascada, también se puede establecer IsFixed en true:

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

Para más información, vea Valores y parámetros en cascada de Blazor en ASP.NET Core.

Evitación de la expansión de atributos con CaptureUnmatchedValues

Los componentes pueden optar por recibir valores de parámetro "no coincidentes" mediante la marca CaptureUnmatchedValues:

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

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

Este enfoque permite pasar atributos adicionales arbitrarios al elemento. Pero este enfoque es costoso porque el representador debe:

  • Hacer coincidir todos los parámetros proporcionados con el conjunto de parámetros conocidos para compilar un diccionario.
  • Supervisar el modo en que varias copias del mismo atributo se sobrescriben entre sí.

Use CaptureUnmatchedValues donde el rendimiento de representación de los componentes no sea crítico, como los componentes que no se repiten con frecuencia. Pero para los componentes que se representan a escala, como cada elemento en una lista extensa o las celdas de una cuadrícula, intente evitar la expansión de atributos.

Para más información, consulte Parámetros arbitrarios y de expansión de atributos de ASP.NET Core Blazor.

Implementación manual de SetParametersAsync

Un origen importante de la sobrecarga de representación por componente es escribir valores de parámetros entrantes en las propiedades de [Parameter]. El representador usa la reflexión para escribir los valores de parámetro, lo que puede provocar un rendimiento deficiente a escala.

En algunos casos extremos, puede que desee evitar la reflexión e implementar su propia lógica de configuración de parámetros de forma manual. Esto puede ser aplicable cuando:

  • Tiene un componente que se representa con mucha frecuencia, por ejemplo, hay cientos o miles de copias del componente en la interfaz de usuario.
  • Un componente acepta muchos parámetros.
  • Observa que la sobrecarga de recibir parámetros tiene una incidencia observable en la capacidad de respuesta de la interfaz de usuario.

En casos extremos, puede invalidar el método SetParametersAsync virtual del componente e implementar su propia lógica específica del componente. En el ejemplo siguiente se evitan deliberadamente las búsquedas en el diccionario:

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

En el código anterior, la devolución de la clase base SetParametersAsync ejecuta los métodos de ciclo de vida normales sin asignar los parámetros de nuevo.

Como puedes ver en el código anterior, invalidar SetParametersAsync y proporcionar lógica personalizada es complicado y laborioso, por lo que normalmente no se recomienda adoptar este enfoque. En casos extremos, puedes mejorar el rendimiento de la representación en un 20-25 %, pero solo debes considerar este enfoque en los escenarios extremos mencionados anteriormente en esta sección.

No desencadenación de eventos demasiado rápido

Algunos eventos del explorador se desencadenan muy a menudo. Por ejemplo, onmousemove y onscroll se pueden desencadenar decenas o cientos de veces por segundo. En la mayoría de los casos, no es necesario realizar actualizaciones de la interfaz de usuario con frecuencia. Si los eventos se desencadenan demasiado rápido, puede dañar la capacidad de respuesta de la interfaz de usuario o consumir un tiempo excesivo de CPU.

En lugar de usar eventos nativos que se desencadenan rápidamente, considera la posibilidad de usar la interoperabilidad de JS para registrar una devolución de llamada que se desencadene con menos frecuencia. Por ejemplo, el siguiente componente muestra la posición del mouse, pero solo se actualiza como máximo una vez cada 500 ms:

@implements IDisposable
@inject IJSRuntime JS

<h1>@message</h1>

<div @ref="mouseMoveElement" style="border:1px dashed red;height:200px;">
    Move mouse here
</div>

@code {
    private ElementReference mouseMoveElement;
    private DotNetObjectReference<MyComponent>? selfReference;
    private string message = "Move the mouse in the box";

    [JSInvokable]
    public void HandleMouseMove(int x, int y)
    {
        message = $"Mouse move at {x}, {y}";
        StateHasChanged();
    }

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            selfReference = DotNetObjectReference.Create(this);
            var minInterval = 500;

            await JS.InvokeVoidAsync("onThrottledMouseMove", 
                mouseMoveElement, selfReference, minInterval);
        }
    }

    public void Dispose() => selfReference?.Dispose();
}

El código JavaScript correspondiente registra el agente de escucha de eventos DOM para el movimiento del mouse. En este ejemplo, el cliente de escucha de eventos usa la función throttle de Lodash para limitar la velocidad de las invocaciones:

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

Evitación de volver a representar después de controlar eventos sin cambios de estado

Los componentes heredan de ComponentBase, que invoca automáticamente StateHasChanged después de invocar los controladores de eventos del componente. En algunos casos, puede que no sea necesario, o no deseado, desencadenar volver a representarlos después de invocar un controlador de eventos. Por ejemplo, un controlador de eventos podría no modificar el estado del componente. En estos escenarios, la aplicación puede aprovechar la interfaz IHandleEvent para controlar el comportamiento del control del evento Blazor.

Nota:

El enfoque de esta sección no genera flujo de excepciones para límites de error. Para obtener más información y código de demostración que admita límites de error mediante una llamada a ComponentBase.DispatchExceptionAsync, consulta AsNonRenderingEventHandler + ErrorBoundary = comportamiento inesperado (dotnet/aspnetcore #54543).

Para evitar volver a representar todos los controladores de eventos de un componente, implementa IHandleEvent y proporciona una tarea IHandleEvent.HandleEventAsync que invoque al controlador de eventos sin llamar a StateHasChanged.

En el ejemplo siguiente, ningún controlador de eventos agregado al componente desencadena la representación, por lo que HandleSelect no da como resultado su representación cuando se invoca.

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

Además de evitar repetir la representación después de que se desencadenen los controladores de eventos de un componente de forma global, es posible evitar las repeticiones de representaciones después de un único controlador de eventos mediante el siguiente método de utilidad.

Agrega la siguiente clase EventUtil a una aplicación Blazor. Las funciones y acciones estáticas de la parte superior de la clase EventUtil proporcionan controladores que cubren varias combinaciones de argumentos y tipos de valor devuelto que usa Blazor al controlar eventos.

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

Llama a EventUtil.AsNonRenderingEventHandler para llamar a un controlador de eventos que no desencadene una representación cuando se invoque.

En el ejemplo siguiente:

  • Al seleccionar el primer botón, que llama a HandleClick1, se desencadena una repetición de la representación.
  • Al seleccionar el segundo botón, que llama a HandleClick2, no se desencadena una repetición de la representación.
  • Al seleccionar el tercer botón, que llama a HandleClick3, no se desencadena una repetición de la representación y se usan argumentos de evento (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);
    }
}

Además de implementar la interfaz IHandleEvent, aprovechar los otros procedimientos recomendados descritos en este artículo también puede ayudar a reducir las representaciones no deseadas una vez que se controlan los eventos. Por ejemplo, se puede usar la invalidación de ShouldRender en componentes secundarios del componente de destino para controlar la repetición de la representación.

Evita volver a crear delegados para muchos elementos o componentes repetidos

La recreación de Blazor de delegados de expresiones lambda para los elementos o componentes de un bucle puede dar lugar a un rendimiento deficiente.

El componente siguiente que se muestra en el artículo de control de eventos representa un conjunto de botones. Cada botón asigna un delegado a su evento @onclick, lo que está bien si no hay muchos botones que representar.

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 se representa un gran número de botones con el enfoque anterior, la velocidad de representación se ve afectada negativamente, lo que da lugar a una mala experiencia de usuario. Para representar un gran número de botones con una devolución de llamada para eventos click, en el ejemplo siguiente se usa una colección de objetos de botón que asignan el delegado @onclick de cada botón a un elemento Action. El siguiente enfoque no requiere que Blazor recompile todos los delegados de botón cada vez que los botones se representen:

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

Optimización de la velocidad de interoperabilidad de JavaScript

Las llamadas entre .NET y JavaScript requieren una sobrecarga adicional porque:

  • Las llamadas son asincrónicas.
  • Los parámetros y los valores devueltos se serializan en JSON para proporcionar un mecanismo de conversión fácil de entender entre los tipos de JavaScript y .NET.

Además, para las aplicaciones del lado servidor Blazor, estas llamadas se pasan a través de la red.

Evitación de llamadas excesivamente detalladas

Puesto que cada llamada implica cierta sobrecarga, puede ser útil reducir el número de llamadas. Considera el siguiente código, que almacena una colección de elementos en el almacén localStorage del explorador:

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

En el ejemplo anterior se realiza una llamada de interoperabilidad de JS independiente para cada elemento. En su lugar, el enfoque siguiente reduce la interoperabilidad de JS a una sola llamada:

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

La función de JavaScript correspondiente almacena toda la colección de elementos en el cliente:

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

En el caso de las aplicaciones Blazor WebAssembly, la conversión de llamadas de interoperabilidad de JS individuales en una sola llamada normalmente solo mejora significativamente el rendimiento si el componente realiza un gran número de llamadas de interoperabilidad de JS.

Considera el uso de llamadas sincrónicas.

Llamada a JavaScript desde .NET

Esta sección solo se aplica a los componentes del lado cliente.

Las llamadas de interoperabilidad de JS son asincrónicas, independientemente de si el código al que se llama es sincrónico o asincrónico. Las llamadas son asíncronas para garantizar que los componentes sean compatibles entre los modos de representación del lado del servidor y del lado del cliente. En el servidor, todas las llamadas de interoperabilidad de JS deben ser asincrónicas porque se envían a través de una conexión de red.

Si sabes con certeza que la aplicación solo se ejecuta en WebAssembly, puedes optar por realizar llamadas de interoperabilidad de JS sincrónicas. Esto tiene una sobrecarga ligeramente menor que la realización de llamadas asincrónicas y puede dar lugar a menos ciclos de representación porque no hay ningún estado intermedio mientras se esperan los resultados.

Para realizar una llamada sincrónica de .NET a JavaScript en un componente del lado cliente, convierte IJSRuntime en IJSInProcessRuntime para realizar la llamada de interoperabilidad de JS :

@inject IJSRuntime JS

...

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

Al trabajar con componentes del lado del cliente de IJSObjectReferenceASP.NET Core 5.0 o posterior, puedes usarIJSInProcessObjectReference sincrónicamente en su lugar. IJSInProcessObjectReference implementa IAsyncDisposable/IDisposable y debe eliminarse para la recolección de elementos no utilizados para evitar una fuga de memoria, como se muestra en el ejemplo siguiente:

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

Llamada a .NET desde JavaScript

Esta sección solo se aplica a los componentes del lado cliente.

Las llamadas de interoperabilidad de JS son asincrónicas, independientemente de si el código al que se llama es sincrónico o asincrónico. Las llamadas son asíncronas para garantizar que los componentes sean compatibles entre los modos de representación del lado del servidor y del lado del cliente. En el servidor, todas las llamadas de interoperabilidad de JS deben ser asincrónicas porque se envían a través de una conexión de red.

Si sabes con certeza que la aplicación solo se ejecuta en WebAssembly, puedes optar por realizar llamadas de interoperabilidad de JS sincrónicas. Esto tiene una sobrecarga ligeramente menor que la realización de llamadas asincrónicas y puede dar lugar a menos ciclos de representación porque no hay ningún estado intermedio mientras se esperan los resultados.

Para realizar una llamada sincrónica de JavaScript a .NET en un componente del lado cliente, use DotNet.invokeMethod en lugar de DotNet.invokeMethodAsync.

Las llamadas sincrónicas funcionan si:

  • El componente solo se representa para su ejecución en WebAssembly.
  • La función llamada devuelve un valor de forma sincrónica. La función no es un método async y no devuelve un valor Task de .NET o Promise de JavaScript.

Esta sección solo se aplica a los componentes del lado cliente.

Las llamadas de interoperabilidad de JS son asincrónicas, independientemente de si el código al que se llama es sincrónico o asincrónico. Las llamadas son asíncronas para garantizar que los componentes sean compatibles entre los modos de representación del lado del servidor y del lado del cliente. En el servidor, todas las llamadas de interoperabilidad de JS deben ser asincrónicas porque se envían a través de una conexión de red.

Si sabes con certeza que la aplicación solo se ejecuta en WebAssembly, puedes optar por realizar llamadas de interoperabilidad de JS sincrónicas. Esto tiene una sobrecarga ligeramente menor que la realización de llamadas asincrónicas y puede dar lugar a menos ciclos de representación porque no hay ningún estado intermedio mientras se esperan los resultados.

Para realizar una llamada sincrónica de .NET a JavaScript en un componente del lado cliente, convierte IJSRuntime en IJSInProcessRuntime para realizar la llamada de interoperabilidad de JS :

@inject IJSRuntime JS

...

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

Al trabajar con componentes del lado del cliente de IJSObjectReferenceASP.NET Core 5.0 o posterior, puedes usarIJSInProcessObjectReference sincrónicamente en su lugar. IJSInProcessObjectReference implementa IAsyncDisposable/IDisposable y debe eliminarse para la recolección de elementos no utilizados para evitar una fuga de memoria, como se muestra en el ejemplo siguiente:

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

Considera el uso de llamadas deserializadas

Esta sección solo se aplica a las aplicaciones Blazor WebAssembly.

Cuando se ejecuta en Blazor WebAssembly, es posible realizar llamadas deserializadas de .NET a JavaScript. Se trata de llamadas sincrónicas que no realizan la serialización de JSON de argumentos o valores devueltos. Todos los aspectos de la administración de la memoria y las traslaciones entre las representaciones de .NET y JavaScript quedan en manos del desarrollador.

Advertencia

Aunque el uso de IJSUnmarshalledRuntime es el método de interoperabilidad de JS con menor sobrecarga, las API de JavaScript necesarias para interactuar con estas API no están documentadas en este momento y están sujetas a cambios importantes en versiones futuras.

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

Uso de la interoperabilidad [JSImport]/[JSExport] de JavaScript

La interoperabilidad de JavaScript [JSImport]/[JSExport] para aplicaciones Blazor WebAssembly ofrece un mejor rendimiento y estabilidad a través de la API de interoperabilidad JS en las versiones del marco anteriores a ASP.NET Core en .NET 7.

Para obtener más información, consulta Interoperabilidad JSImport/JSExport de JavaScript con ASP.NET Core Blazor.

Compilación ahead-of-time (AOT)

La compilación ahead-of-time (AOT) compila el código .NET de una aplicación Blazor directamente en WebAssembly nativo para su ejecución directa por el explorador. Las aplicaciones compiladas AOT dan como resultado aplicaciones más grandes que tardan más en descargarse, pero que suelen proporcionar un mejor rendimiento en tiempo de ejecución, especialmente para las aplicaciones que ejecutan tareas que consumen mucha CPU. Para obtener más información, consulta Herramientas de creación de ASP.NET Core Blazor WebAssembly y compilación anticipada (AOT).

Minimización del tamaño de descarga de la aplicación

Revinculación en tiempo de ejecución

Para obtener información sobre cómo la revinculación de runtime minimiza el tamaño de descarga de una aplicación, consulta las herramientas de creación de ASP.NET Core Blazor WebAssembly y compilación anticipada (AOT).

Uso de System.Text.Json

La implementación de interoperabilidad JS de Blazor se basa en System.Text.Json, que es una biblioteca de serialización de JSON de alto rendimiento con una asignación de memoria reducida. El uso de System.Text.Json no debería dar como resultado un mayor tamaño de carga de aplicación frente a la adición de una o varias bibliotecas JSON alternativas.

Para obtener instrucciones sobre la migración, consulta Procedimiento para realizar la migración de Newtonsoft.Json a System.Text.Json.

Recorte de lenguaje intermedio (IL)

Esta sección solo se aplica a los escenarios Blazor del lado cliente.

Cuando una aplicación Blazor WebAssembly se recorta, el tamaño de la aplicación se reduce quitando el código que no se usa en los archivos binarios de la aplicación. Para obtener más información, consulta Configuración del recortador de ASP.NET Core Blazor.

Cuando una aplicación Blazor WebAssembly se vincula, el tamaño de la aplicación se reduce recortando el código que no se usa en los archivos binarios de la aplicación. El enlazador de lenguaje intermedio (IL) solo está habilitado cuando se compila en la configuración de Release. Para sacar partido de esto, publica la aplicación de implementación con el comando dotnet publish, con la opción -c|--configuration establecida en Release:

dotnet publish -c Release

Ensamblados de carga diferida

Esta sección solo se aplica a los escenarios Blazor del lado cliente.

Carga los ensamblados en tiempo de ejecución cuando una ruta los requiera. Para obtener más información, consulta Ensamblados de carga diferida en Blazor WebAssembly de ASP.NET Core.

Compresión

Esta sección solo se aplica a las aplicaciones Blazor WebAssembly.

Cuando se publica una aplicación Blazor WebAssembly, la salida se comprime estáticamente durante la publicación para reducir el tamaño de la aplicación y acabar con la necesidad de compresión en tiempo de ejecución. Blazor se basa en el servidor para realizar negociación de contenido y proporcionar archivos comprimidos estáticamente.

Cuando una aplicación se haya implementado, compruebe que proporciona archivos comprimidos. Inspecciona la pestaña Red de las herramientas de desarrollo del explorador y compruebe que los archivos se proporcionan con Content-Encoding: br (compresión de Brotli) o Content-Encoding: gz (compresión Gzip). Si el host no proporciona archivos comprimidos, sigue las instrucciones de Hospedaje e implementación de ASP.NET Core Blazor WebAssembly.

Deshabilitar las características sin usar

Esta sección solo se aplica a los escenarios Blazor del lado cliente.

El runtime de Blazor WebAssembly incluye las siguientes características de .NET, que se pueden deshabilitar cuando el tamaño de la carga útil es más pequeño:

  • Blazor WebAssembly incluye los recursos de globalización necesarios para mostrar valores, como las fechas y la moneda, en la referencia cultural del usuario. Si la aplicación no necesita localización, es posible configurarla para que admita la referencia cultural invariable, que se basa en la referencia cultural en-US.
  • La adopción de la globalización invariable solo da como resultado el uso de nombres de zona horaria sin localizar. Para recortar el código y los datos de la zona horaria de la aplicación, aplica la propiedad MSBuild <InvariantTimezone> con un valor de true en el archivo del proyecto de la aplicación:

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

    Nota:

    <BlazorEnableTimeZoneSupport> invalida una configuración <InvariantTimezone> anterior. Se recomienda quitar la configuración <BlazorEnableTimeZoneSupport>.

  • Se incluye un archivo de datos para que la información de zona horaria sea correcta. Si la aplicación no necesita esta característica, considera la posibilidad de deshabilitarla estableciendo la propiedad MSBuild <BlazorEnableTimeZoneSupport> en false en el archivo del proyecto de la aplicación:

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • Se incluye información de intercalación para que API como StringComparison.InvariantCultureIgnoreCase funcionen correctamente. Si estás convencido de que la aplicación no necesita datos de intercalación, considera la posibilidad de deshabilitarla estableciendo en false la propiedad de MSBuild BlazorWebAssemblyPreserveCollationData del archivo de proyecto de la aplicación:

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