Menú de navegaciónMenú
Categorías

La mejor forma de Aprender Programación online y en español www.campusmvp.es

?id=51f7f131-ea49-4af8-9f87-6f78ea09408f

Consejos para mejorar el rendimiento de Blazor en ASP.NET Core

Imagen ornamental

NOTA: todas estas secciones se ha obtenido a partir de la documentación oficial de Microsoft, y están basadas en sus recomendaciones.

Blazor es una plataforma optimizada para ofrecer un alto rendimiento en escenarios realistas de aplicaciones de interfaz de usuario. Pero, aun así, obtener un buen rendimiento depende de que los desarrolladores adoptemos los patrones y las características apropiadas para cada caso.

Vamos a ver algunas de las buenas prácticas que nos pueden ayudar a obtener un rendimiento óptimo.

1.- Optimización de la velocidad de renderizado

Si optimizas la velocidad de renderizado y minimizas la carga de trabajo a la hora de hacerlo, mejorarás la capacidad de respuesta de la UI, pudiendo conseguir mejoras de hasta diez veces o más a la hora de hacer el renderizado.

1.1.- Evita el renderizado innecesario de los subárboles de componentes

Puedes evitar la mayoría del coste de representación de un componente primario si omites el renderizado innecesario de los subárboles de los componentes secundarios cuando se produce un evento. Solo debes preocuparte por omitir el renderizado de subárboles si tienen un renderizado especialmente costoso y están causando un retraso en la UI.

A la hora de ejecutar la aplicación, los componentes pertenecen a una jerarquía. Un componente "raíz" tiene componentes secundarios. A su vez, estos elementos secundarios tienen sus propios componentes secundarios, etc. Cuando se produce un evento, como por ejemplo la pulsación de un botón, este es el proceso que decide los componentes que se deben renderizar:

  1. El evento se envía al componente que gestiona el evento. Después de ejecutar el manejador de eventos correspondiente, se vuelve a renderizar el componente que lo gestiona.
  2. Al renderizar un determinado componente, este pasa de nuevo los valores de los parámetros a cada uno de sus componentes secundarios.
  3. Tras recibir los nuevos valores para sus parámetros, cada componente decide si debe volver a renderizarse o no. Por defecto, los componentes se renderizan si los valores de los parámetros han cambiado, por ejemplo, si son objetos mutables.

Los dos últimos pasos de la secuencia anterior continúan de forma recursiva hacia abajo en la jerarquía de componentes. En muchos casos, se volverá a renderizar el subárbol completo. Los eventos que tienen como destino componentes de alto nivel pueden provocar que el nuevo renderizado sea costoso porque se tienen volver a renderizar cada uno de los componentes inferiores, hijos del componente de nivel superior en el árbol.

Para evitar el renderizado recursivo en un subárbol concreto, podemos utilizar alguno de los siguientes enfoques:

  • Asegurarnos de que los parámetros que le pasamos a los componentes de segundo nivel o inferiores, son de tipos primitivos inmutables, como string, int, bool, DateTime y similares. La lógica integrada para detectar cambios omite automáticamente un nuevo renderizado si los valores de los parámetros inmutables primitivos no han cambiado. Si el componente secundario tiene un parámetro como este: <Customer CustomerId="@item.CustomerId" />, donde CustomerId es un tipo de int, Customer no se volverá a renderizar a menos que cambie el valor item.CustomerId.

  • Sobrescribir el método ShouldRender de la clase base:

    • Para aceptar valores de parámetros no primitivos, como tipos de modelos personalizados complejos, devoluciones de llamada de eventos o valores RenderFragment.
    • Si crea un componente que solo tenga interfaz de usuario y que nunca cambia tras el renderizado inicial, independientemente de los cambios que haya en el valor del parámetro (esto es algo menos habitual).

Por ejemplo, imaginemos una herramienta de búsqueda de vuelos para una línea aérea que utiliza un par de campos privados: el identificador de vuelo de llegada anterior (prevInboundFlightId) y el identificador del vuelo de salida anterior (prevOutboundFlightId), que sirven para llevar un seguimiento de la información entre actualizaciones del componente. Si alguno de estos dos identificadores de vuelo cambia cuando los parámetros del componente se establecen en OnParametersSet, el componente se vuelve a renderizar porque shouldRender está establecido en true por defecto. Si shouldRender devolviese un false tras comprobar los identificadores de vuelo una primera vez, se evitaría un nuevo y costoso renderizado. Sería algo así:

@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 manejador de eventos también puede establecer shouldRender en true. En cualquier caso, para la mayoría de los componentes no es necesario tener que decidir si se va a renderizar o no tras cada evento individual, pero conviene conocer esta posibilidad.

1.2.- Virtualización de componentes con muchos datos

Al renderizar 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 renderizado puede provocar retrasos y lentitud. Dado que el usuario solo puede ver un pequeño número de elementos a la vez sin desplazarse, no vale la pena renderizar aquellos elementos que quizá no se vayan a ver nunca.

Blazor proporciona el componente Virtualize que permite crear listas tan grandes como necesitemos, pero que solo renderiza los elementos de lista que están dentro de la ventana de desplazamiento actual. Por ejemplo, un componente puede representar una lista con 100.000 entradas, pero solo pagaría el coste de renderizar los 20 elementos que son visibles.

Puedes ver cómo funciona el componente Virtualiza de Blazor en este vídeo de nuestro canal de YouTube.

2.- Creación de componentes ligeros y optimizados

La mayoría de los componentes Razor no requieren esfuerzos de optimización importantes porque la mayoría de ellos no se repiten en la interfaz de usuario y no se renderizan con mucha 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 renderizar en respuesta a una acción del usuario. Estos componentes no suelen suponer una gran carga de trabajo de representación, por lo que puede usar libremente cualquier combinación de características del framework sin preocuparse demasiado por el rendimiento de la representación.

Pero existen escenarios comunes en los que los componentes sí que se repiten a gran escala, y muchas veces eso tiene como resultado un rendimiento muy malo en la interfaz de usuario:

  • Formularios anidados grandes con cientos de elementos individuales, como inputs 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 te proporcionamos consejos sobre cómo crear estos componentes de modo que sean ligeros para que la interfaz de usuario siga funcionando de manera ágil y con capacidad de respuesta.

2.1.- Evita crear miles de instancias de componentes

Cada componente es una isla independiente que se puede renderizar de manera independiente de sus componentes "padre" (los que lo contienen) y de sus "hijos" (los que contiene el propio componente). Al elegir cómo dividir la interfaz de usuario en una jerarquía de componentes, estás tomando el control sobre el nivel de detalle del renderizado de la interfaz de usuario. Esto puede dar lugar a un rendimiento bueno o muy malo.

Al dividir la interfaz de usuario en componentes independientes, puede ocurrir que se vuelvan a renderizar partes más pequeñas de la UI cuando se produzcan eventos, como ya hemos visto. Por ejemplo, en una tabla con muchas filas con un botón en cada una de ellas, es posible que solo se vuelva a renderizar esa única fila mediante un componente específico, en lugar de toda la página o toda la tabla, que sería mucho peor. 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 ASP.NET Core, se observó una carga de renderizado de aproximadamente 0,06 ms por instancia de componente en una aplicación Blazor WebAssembly. La aplicación de prueba renderizaba un componente simple que aceptaba tres parámetros. Internamente, la carga de renderizado se debe en gran parte a la recuperación del estado de cada componente desde los diccionarios, y al paso y recepción de parámetros. Si multiplicas, puedes ver fácilmente que agregar 2000 instancias de componentes adicionales agregaría 0,12 segundos (120ms) al tiempo de renderizado, y la interfaz de usuario podría comenzar a ser lenta para los usuarios.

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

2.1.1.- Componentes secundarios insertados en sus elementos primarios

Consideremos este fragmento de un componente primario que renderiza a sus componentes secundarios usando un bucle:

<div class="chat">
    @foreach (var message in messages)
    {
        <ChatMessageDisplay Message="@message" />
    }
</div>

Y este es un fragmento del código de uno de los componentes hijo que renderiza:

<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. Si realmente tienes que mostrar miles de mensajes al mismo tiempo, sería bueno considerar la posibilidad de no separar ChatMessageDisplay como un componente independiente. En su lugar, inserta el código del componente secundario directamente en el elemento primario. Este enfoque evita la sobrecarga de renderizado de la que hablábamos, por cada componente, aunque como contrapartida, lógicamente, no deja reutilizar esa parte en concreto:

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

2.1.2.- Definición de RenderFragments reutilizables

Quizá estés separando los componentes secundarios tan solo como una manera de reutilizar la lógica de renderizado, pero no tanto para insertarlos como unidades independientes en otros contextos. Si ese fuera el caso, puedes crear lógica de representación reutilizable sin implementar componentes extra sólo para ello. En el bloque @code de cualquier componente, puedes definir un RenderFragment y luego renderizarlo desde cualquier sitio tantas veces como sea necesario, así:

<h1>Hello, world!</h1>

@RenderWelcomeInfo

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

@RenderWelcomeInfo

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

Como demuestra el ejemplo anterior, los componentes pueden emitir marcado a partir del código dentro de sus bloques @code y también fuera de ellos. El delegado de tipo RenderFragment acepta un parámetro denominado __builder de tipo RenderTreeBuilder para que el compilador de Razor pueda generar instrucciones de renderizado para el fragmento.

Nota: la asignación a un delegado de tipo RenderFragment solo se admite en archivos de componente Razor (.razor) y no admiten callbacks de eventos.

Para que nuestro código RenderTreeBuilder sea reutilizable entre varios componentes, debemos declarar el delegado RenderFragment como público y estático:

public static RenderFragment SayHello = __builder =>
{
    <h1>Hello!</h1>
};

En este ejemplo, SayHello se puede invocar desde cualquier componente no relacionado. Esta técnica resulta útil para crear bibliotecas de fragmentos de marcado reutilizables que se renderizan sin sobrecarga por componente.

Los delegados de tipo RenderFragment también pueden aceptar parámetros. Por ejemplo, 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 => __builder =>
    {
        <div class="chat-message">
            <span class="author">@message.Author</span>
            <span class="text">@message.Text</span>
        </div>
    };
}

Este enfoque proporciona la ventaja de reutilizar la lógica de representación sin la sobrecarga por componente. Pero no tiene la ventaja de poder actualizar su subárbol de la interfaz de usuario de forma independiente, ni tampoco tiene la capacidad de omitir el renderizado de ese subárbol de la interfaz de usuario cuando su elemento primario se representa, ya que no está establecido ningún límite para el componente.

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, puedes utilizar una propiedad en lugar de un campo en el delegado RenderFragment:

protected RenderFragment DisplayTitle => __builder =>
{
    <div>
        @TitleTemplate
    </div>
};

2.2.- No recibas 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 mucho.

Es raro que demasiados parámetros impacten gravemente en el rendimiento, pero puede ser un factor más a tener en cuenta. Por ejemplo, en el caso de un componente TableCell que se renderice 1.000 veces dentro de una cuadrícula, cada parámetro adicional que se le pase al componente podría agregar alrededor de 15 ms al costo total de renderizado. Si cada celda acepta 10 parámetros, el paso de parámetros tardaría aproximadamente 150 ms por componente para un coste de renderizado total de 150.000 ms (¡150 segundos!) y provocaría impacto grande en la interfaz de usuario.

Para reducir la carga de parámetros, podemos agruparlos en una clase personalizada. Por ejemplo, un componente para la celda de una tabla podría aceptar un objeto común con sus propiedades, de modo que se comparta entre todas las instancias. En el ejemplo siguiente, Data es diferente para cada celda, pero Options es común a todas ellas:

@typeparam TItem

...

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

    [Parameter]
    public GridOptions? Options { get; set; }
}

Pero no estaría de más considerar el hecho de no tener un componente para cada celda de la tabla, como ya vimos antes, y en su lugar introducir su renderizado en el componente padre (la tabla en este caso), como un RenderFragment.

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

2.3.- Asegúrate de que los parámetros en cascada son fijos

El componente CascadingValue tiene un parámetro opcional denominado IsFixed del que quizá no hayas oído hablar:

  • Si el valor IsFixed es false (es el valor por defecto), cada componente destinatario del valor en cascada configura una suscripción para recibir notificaciones de los posibles cambios en éste. Por eso cada CascadingParameter es mucho más caro que un parámetro (Parameter) normal debido al seguimiento que hay que hacer de esas 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. Así, cada CascadingParameter es ligero y no es más caro en términos de renderizado y cómputo que un parámetro normal.

Establecer IsFixed a true mejora el rendimiento si hay un gran número de componentes distintos que reciben el valor en cascada. Siempre que sea posible, establece IsFixed a true en los parámetros en cascada. Puedes hacerlo cuando sepas que el valor proporcionado no va a cambiar.

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>

2.4.- Evita la expansión de atributos con CaptureUnmatchedValues

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

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

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

Este enfoque permite pasar atributos adicionales arbitrarios al componente, pero es costoso porque el renderizador de Blazor debe:

  • Buscar una coincidencia de todos los parámetros proporcionados con un conjunto de parámetros conocidos, para compilar un diccionario.
  • Controlar el modo en que varias copias del mismo atributo se sobrescriben entre sí.

Utiliza CaptureUnmatchedValues donde el rendimiento de renderizado de los componentes no sea crítico, como los componentes que no se repiten con frecuencia. Pero para los componentes que se renderizan a gran escala, como en cada elemento en una lista larga o en las celdas de una cuadrícula grande, intenta evitar la expansión de atributos.

2.5.- Implementa manualmente SetParametersAsync

Un origen importante de la sobrecarga de representación para cada componente es escribir valores de parámetros entrantes en las propiedades de [Parameter]. El renderizador utiliza reflexión para escribir los valores de parámetro, lo que puede provocar un rendimiento deficiente si lo usamos masivamente.

En algunos casos extremos, puede que desees evitar el uso de la reflexión e implementar tu propia lógica de configuración de parámetros manualmente. Esto puede ser útil cuando se dé alguna de estas cosas, y desde luego todas al mismo tiempo:

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

En casos extremos, puedes invalidar el método virtual SetParametersAsync del componente e implementar tu propia lógica específica. 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 este fragmento, al devolver el SetParametersAsync de la clase base, se ejecutan los métodos de ciclo de vida normales del componente, sin asignar los parámetros de nuevo.

Como puedes observar, invalidar SetParametersAsync y proporcionar lógica personalizada es complicado y laborioso, por lo que normalmente no se recomienda adoptar este enfoque. Pero, en casos extremos, puede mejorar el rendimiento del renderizado en un 20 o 25%, pero solo deberías considerar este enfoque en esos escenarios extremos, una combinación de lo mencionado al principio de este apartado.

3.- No lances eventos demasiado rápido

Algunos eventos del explorador se lanzan con demasiada frecuencia. Por ejemplo, onmousemove y onscroll se pueden lanzar 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 tanta frecuencia. Si los eventos se lanzan demasiado rápido pueden 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 y se notifican en el servidor, deberías usar la interoperabilidad con JavaScript para registrar una función de callback que se notifique con menos frecuencia. Por ejemplo, el siguiente componente muestra la posición del ratón, pero solo la actualiza como máximo una vez cada 500 ms:

@inject IJSRuntime JS
@implements IDisposable

<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 un manejador para el evento de movimiento del ratón. En este ejemplo, el manejador del evento utiliza la función throttle de la biblioteca JavaScript 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>

4.- Evitar un nuevo renderizado tras eventos sin cambios de estado

Por defecto, los componentes heredan de ComponentBase, que invoca automáticamente al evento StateHasChanged tras procesar los manejadores de eventos de un componente. En algunos casos, puede que no sea necesario, o no sea deseado, lanzar un nuevo renderizado tras llamar a un manejador de eventos. Por ejemplo, si un manejador de eventos sabemos que no va a modificar el estado del componente. En estos casos, la aplicación puede aprovechar la interfaz IHandleEvent para controlar el comportamiento del control del evento Blazor.

Para evitar volver a renderizar el componente tras cada ejecución de un manejador de eventos, implementa la interfaz IHandleEvent y proporciona una tarea IHandleEvent.HandleEventAsync que llame al manejador de eventos sin llamar a StateHasChanged.

En el siguiente ejemplo, ningún controlador de eventos agregado al componente desencadena su renderizado, por lo que HandleSelect no da como resultado un renderizado cuando se invoca:

@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 que se repita el renderizado, es posible evitarlo también para un solo manejador de eventos mediante el siguiente método de utilidad.

Añade la siguiente clase, EventUntil, a una aplicación Blazor. Las funciones y acciones estáticas de la parte superior de la clase EventUtil proporcionan manejadores que cubren varias combinaciones de argumentos y tipos devueltos que usa Blazor al controlar eventos (guárdalo en un archivo, por ejemplo, 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);
    }
}

Ahora puedes llamar a EventUtil.AsNonRenderingEventHandler para ejecutar un manejador de eventos que no desencadene un renderizado cuando se invoque.

Así, en el ejemplo siguiente:

  • Al seleccionar el primer botón, que llama a HandleClick1, se lanza un renderizado repetido.
  • Al seleccionar el segundo botón, que llama a HandleClick2, no se lanza un nuevo renderizado.
  • Al seleccionar el tercer botón, que llama a HandleClick3, no se lanza un nuevo renderizado y además se usan unos argumentos de evento (MouseEventArgs en este caso).
@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 te puede ayudar a reducir los renderizados no deseados una vez que se controlen los eventos. Por ejemplo, se puede usar también la invalidación de ShouldRender en componentes secundarios del componente de destino para controlar la repetición del renderizado.

5.- Evita volver a crear delegados cuando se repiten muchos componentes

Blazor recrea los delegados para expresiones lambda cuando se reutiliza un componente (por definición son funciones anónimas y por lo tanto no se reutilizan). Cuando un componente se renderiza muchas veces en un bucle, este hecho puede dar lugar a un mal rendimiento.

El componente siguiente representa un conjunto de botones. Cada botón asigna un delegado a su evento @onclick mediante una expresión lambda, lo que está bien si no hay muchos botones que renderizar:

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

Pero, si se renderizan un gran número de botones de esta manera, la velocidad de renderizado se verá afectada negativamente, y dará lugar a una mala experiencia de usuario. Para renderizar un gran número de botones con un manejador para el evento de pulsación, en el ejemplo siguiente hemos usado una colección de botones que asignan al delegado para @onclick a un elemento Action. El enfoque mostrado en el siguiente fragmento de código, no requiere que Blazor recompile todos los delegados de botón cada vez que se renderizan:

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

6.- Optimización de la velocidad de interoperabilidad con JavaScript

Las llamadas entre .NET y JavaScript implican una sobrecarga adicional porque, por defecto:

  • Las llamadas son asíncronas.
  • 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 de .NET.

Además, en Blazor Server, estas llamadas se trasiegan a través de la red.

6.1.- Evita las llamadas excesivamente específicas

Puesto que cada llamada implica cierta sobrecarga, en ocasiones puede ser útil reducir el número de llamadas que se realizan. Considera por ejemplo el siguiente código, que almacena una colección de elementos en el almacén local (localStorage) del navegador:

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

En este código 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);
}

Esta función de JavaScript almacena toda la colección de elementos en el cliente en una sola llamada desde Blazor, por lo que se reduce el número de llamadas a una sola:

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

En el caso de las aplicaciones Blazor WebAssembly, la reducción de llamadas de interoperabilidad de JS normalmente solo mejoraría significativamente el rendimiento si se realizan un número muy elevado de ellas.

6.2.- Considera el uso de llamadas síncronas

👁 Este apartado sólo se aplica a las aplicaciones Blazor WebAssembly

Por defecto, las llamadas de interoperabilidad de JS son asíncronas, independientemente de si el código al que se llama es síncrono o asíncrono. Esto es así para asegurarse de que los componentes son compatibles en ambos modelos de hospedaje: Blazor Server y Blazor WebAssembly. En Blazor Server, todas las llamadas de interoperabilidad de JS deben ser asíncronas porque se envían a través de una conexión de red.

Si sabes con certeza que la aplicación solo se va a ejecutar en Blazor WebAssembly, puedes optar por realizar llamadas de interoperabilidad de JS de manera síncrona. Esto tiene una sobrecarga ligeramente menor que la realización de llamadas asíncronas y puede dar lugar a menos ciclos de renderizado porque no existe ningún estado intermedio mientras se esperan los resultados (pero la interfaz se bloquea al mismo tiempo).

Para hacer una llamada asíncrona de .NET a JavaScript en una aplicación Blazor WebAssembly, convierte IJSRuntime en IJSInProcessRuntime a la hora de realizarla:

@inject IJSRuntime JS

...

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

Al trabajar con IJSObjectReference, puedes realizar una llamada síncrona convirtiéndola en un IJSInProcessObjectReference.

Para hacer una llamada síncrona desde JavaScript a .NET, utiliza DotNet.invokeMethod en lugar de DotNet.invokeMethodAsync.

Las llamadas asíncronas funcionan si:

  • La aplicación se ejecuta en Blazor WebAssembly, no en Blazor Server.
  • La función llamada devuelve un valor de forma síncrona. O sea, la función no es un método async y no devuelve un valor Task de .NET o Promise de JavaScript.

6.3.- Considera usar llamadas deserializadas

👁 Este apartado sólo se aplica a las aplicaciones Blazor WebAssembly

Cuando la aplicación se ejecuta en el modelo Blazor WebAssembly, es posible realizar llamadas deserializadas de .NET a JavaScript. Se trata de llamadas síncronas que no realizan la serialización a JSON de argumentos o valores de retorno. Todos los aspectos de la administración de la memoria y las "traducciones" entre las representaciones de .NET y JavaScript quedan en manos del desarrollador.

⚠ Atención: aunque el uso de IJSUnmarshalledRuntime es el método de interoperabilidad de JavaScript 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");
    }
}

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

8.- Minimización del tamaño de descarga de la aplicación

8.1.- Revinculación en tiempo de ejecución

Para obtener información sobre cómo la revinculación en tiempo de ejecución minimiza el tamaño de descarga de una aplicación, lee Hospedaje e implementación de ASP.NET Core Blazor WebAssembly.

8.2.- Utiliza System.Text.Json

La implementación de la interoperabilidad de JavaScript de Blazor se basa en el uso de System.Text.Json, que es una biblioteca de serialización de JSON de alto rendimiento con una asignación de memoria reducida. Te recomendamos el uso de esta biblioteca nativa frente al uso de otras bibliotecas JSON alternativas, especialmente JSON.NET de Newtonsoft que es la que se venía usando tradicionalmente.

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

8.3.- Recorte de lenguaje intermedio (IL)

👁 Este apartado sólo se aplica a las aplicaciones Blazor WebAssembly

Cuando una aplicación Blazor WebAssembly se recorta (proceso conocido como Trimming de ensamblados), el tamaño de la aplicación se reduce quitando de los archivos binarios de la aplicación el código que no se utiliza. Para más información, lee: Configuración del recortador de ASP.NET Core Blazor.

8.4.- Ensamblados de carga diferida (lazy loading)

👁 Este apartado sólo se aplica a las aplicaciones Blazor WebAssembly

Carga los ensamblados en tiempo de ejecución solo cuando una ruta los necesite. Para más información, lee: Ensamblados de carga diferida en Blazor WebAssembly de ASP.NET Core.

8.5.- Compresión

👁 Este apartado sólo 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 los ensamblados 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, comprueba que proporcione archivos comprimidos. Inspecciona la pestaña Red de las herramientas de desarrollo del navegador y comprueba 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.

8.6.- Deshabilitar las características sin usar

👁 Este apartado sólo se aplica a las aplicaciones Blazor WebAssembly

El runtime de Blazor WebAssembly incluye las siguientes características de .NET, que se pueden deshabilitar para conseguir un tamaño de descarga más pequeño:

  • 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 en false la propiedad de MSBuild BlazorEnableTimeZoneSupport del archivo de proyecto de la aplicación:

    <PropertyGroup
        <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport
    </PropertyGroup>
    
  • De forma predeterminada, 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 de los Estados Unidos de América.

 

campusMVP campusMVP es la mejor forma de aprender a programar online y en español. En nuestros cursos solamente encontrarás contenidos propios de alta calidad (teoría+vídeos+prácticas) creados y tutelados por los principales expertos del sector. Nosotros vamos mucho más allá de una simple colección de vídeos colgados en Internet porque nuestro principal objetivo es que tú aprendas. Ver todos los posts de campusMVP
Archivado en: Lenguajes y plataformas

¿Te ha gustado este post?
Pues espera a ver nuestro boletín mensual...

Suscríbete a la newsletter

La mejor formación online para desarrolladores como tú

Agregar comentario

Los datos anteriores se utilizarán exclusivamente para permitirte hacer el comentario y, si lo seleccionas, notificarte de nuevos comentarios en este artículo, pero no se procesarán ni se utilizarán para ningún otro propósito. Lee nuestra política de privacidad.