Melhores práticas de desempenho Blazor do ASP.NET Core

Observação

Esta não é a versão mais recente deste artigo. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, confira .NET e a Política de Suporte do .NET Core. Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Importante

Essas informações relacionam-se ao produto de pré-lançamento, que poderá ser substancialmente modificado antes do lançamento comercial. A Microsoft não oferece nenhuma garantia, explícita ou implícita, quanto às informações fornecidas aqui.

Para informações sobre a versão vigente, confira a Versão do .NET 8 deste artigo.

Blazor é otimizado para alto desempenho em cenários mais realistas de interface do usuário de aplicativo. No entanto, o melhor desempenho depende de os desenvolvedores adotarem os padrões e recursos corretos.

Observação

Os exemplos de código neste artigo adotam tipos de referência anuláveis (NRTs) e análise estática de estado nulo do compilador .NET, que são compatíveis com ASP.NET Core no .NET 6 ou posterior.

Otimizar a velocidade de renderização

Otimize a velocidade de renderização para minimizar a carga de trabalho de renderização e melhorar a capacidade de resposta da interface do usuário, o que pode produzir uma melhoria de dez vezes ou mais na velocidade de renderização da interface do usuário.

Evitar a renderização desnecessária das subárvores de componente

Você pode remover a maior parte do custo de renderização de um componente pai ignorando a nova renderização das subárvores de componente filho, quando ocorre um evento. Você só deve se preocupar em ignorar a nova renderização de subárvores que são particularmente caras de renderizar e estão causando o atraso na interface do usuário.

No runtime, os componentes existem em uma hierarquia. Um componente raiz (o primeiro componente carregado) tem componentes filho. Por sua vez, os filhos da raiz têm seus próprios componentes filho e assim por diante. Quando ocorre um evento, como um usuário selecionar um botão, o processo a seguir determina quais componentes devem ser renderizados novamente:

  1. O evento é expedido para o componente que renderiza o manipulador do evento. Depois de executar o manipulador de eventos, o componente é renderizado novamente.
  2. Quando um componente é renderizado novamente, ele fornece uma nova cópia dos valores de parâmetro para cada um de seus componentes filho.
  3. Depois que um novo conjunto de valores de parâmetro é recebido, cada componente decide se deve ser renderizado novamente. Os componentes são renderizados novamente se os valores do parâmetro forem alterados, por exemplo, se forem objetos mutáveis.

As duas últimas etapas da sequência anterior continuam recursivamente abaixo da hierarquia de componentes. Em muitos casos, toda a subárvore é renderizada novamente. Eventos direcionados a componentes de alto nível podem causar uma nova renderização cara, pois cada componente abaixo do componente de alto nível deve ser renderizado novamente.

Para evitar a recursão de renderização em uma subárvore específica, use uma das seguintes abordagens:

  • Verifique se os parâmetros de componente filho são de tipos imutáveis primitivos, como string, int, bool, DateTime e outros tipos semelhantes. A lógica interna para detectar alterações ignora automaticamente a nova renderização, se os valores de parâmetro imutáveis primitivos não foram alterados. Se você renderizar um componente filho com <Customer CustomerId="item.CustomerId" />, onde CustomerId é um tipo int, o componente Customer não será renderizado novamente, a menos que item.CustomerId seja alterado.
  • Substituir ShouldRender:
    • Para aceitar valores de parâmetro não primitivos, como tipos de modelo personalizado complexos, retornos de chamada de evento ou valores RenderFragment.
    • Se estiver criando um componente somente de interface do usuário que não seja alterado após a renderização inicial, independentemente das alterações de valor do parâmetro.

O exemplo de ferramenta de pesquisa da versão de pré-lançamento a seguir usa campos privados para acompanhar as informações necessárias para detectar alterações. O identificador da versão de pré-lançamento de entrada anterior (prevInboundFlightId) e o identificador da versão de pré-lançamento de saída anterior (prevOutboundFlightId) acompanham as informações para a próxima atualização potencial do componente. Se um dos identificadores da versão de pré-lançamento for alterado quando os parâmetros do componente forem definidos em OnParametersSet, o componente será renderizado novamente porque shouldRender está definido como true. Se shouldRender for avaliado como false depois de verificar os identificadores da versão de pré-lançamento, uma nova renderização cara será evitada:

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

Um manipulador de eventos também pode definir shouldRender como true. Para a maioria dos componentes, determinar a nova renderização no nível dos manipuladores de eventos individuais geralmente não é necessário.

Para saber mais, consulte os recursos a seguir:

Virtualização

Ao renderizar grandes volumes de interface do usuário em um loop, por exemplo, uma lista ou grade com milhares de entradas, o grande volume de operações de renderização pode levar a um atraso na renderização da interface do usuário. Dado que o usuário só pode ver um pequeno número de elementos de uma só vez sem rolagem, muitas vezes é um desperdício gastar tempo renderizando elementos que não estão visíveis no momento.

Blazor fornece o componente Virtualize<TItem> para criar os comportamentos de aparência e rolagem de uma lista arbitrariamente grande, renderizando apenas os itens de lista que estão dentro do visor de rolagem atual. Por exemplo, um componente pode renderizar uma lista com 100.000 entradas, mas pagar apenas o custo de renderização de 20 itens visíveis.

Para obter mais informações, confira Virtualização de componentes Razor do ASP.NET Core.

Criar componentes leves e otimizados

A maioria dos componentes Razor não exige esforços agressivos de otimização, pois a maioria dos componentes não se repete na interface do usuário e não é renderizada novamente em alta frequência. Por exemplo, os componentes roteáveis com uma diretiva @page e componentes usados para renderizar partes de alto nível da interface do usuário, como caixas de diálogo ou formulários, provavelmente aparecem apenas um de cada vez e só são renderizados novamente em resposta a um gesto do usuário. Esses componentes geralmente não criam uma carga de trabalho de renderização alta. Portanto, você pode usar livremente qualquer combinação de recursos de estrutura, sem muita preocupação com o desempenho de renderização.

No entanto, há cenários comuns em que os componentes são repetidos em escala e geralmente resultam em um desempenho insatisfatório da interface do usuário:

  • Formulários aninhados grandes com centenas de elementos individuais, como entradas ou rótulos.
  • Grades com centenas de linhas ou milhares de células.
  • Gráficos de dispersão com milhões de pontos de dados.

Se estiver modelando cada elemento, célula ou ponto de dados como uma instância de componente separada, muitas vezes há tantos deles que o desempenho de renderização se torna crítico. Esta seção fornece avisos sobre como tornar esses componentes leves para que a interface do usuário permaneça rápida e responsiva.

Evitar milhares de instâncias de componente

Cada componente é uma ilha separada que pode renderizar independentemente dos pais e filhos. Ao escolher como dividir a interface do usuário em uma hierarquia de componentes, você está assumindo o controle sobre a granularidade da renderização da interface do usuário. Isso pode resultar em um desempenho satisfatório ou insatisfatório.

Ao dividir a interface do usuário em componentes separados, você pode ter partes menores da nova renderização da interface do usuário, quando ocorrem eventos. Em uma tabela com muitas linhas que têm um botão em cada linha, você pode ter apenas uma única linha renderizada usando um componente filho, em vez de toda a página ou tabela. No entanto, cada componente requer memória adicional e sobrecarga de CPU para lidar com o estado independente e ciclo de vida de renderização.

Em um teste executado pelos engenheiros da unidade de produto do ASP.NET Core, uma sobrecarga de renderização de cerca de 0,06 ms por instância de componente foi vista em um aplicativo Blazor WebAssembly. O aplicativo de teste renderiza um componente simples que aceita três parâmetros. Internamente, a sobrecarga se deve em grande parte à recuperação do estado por componente dos dicionários e à passagem e ao recebimento de parâmetros. Por multiplicação, você pode ver que adicionar mais 2.000 instâncias de componente acrescentaria 0,12 segundos ao tempo de renderização e a interface do usuário começaria a ficar lenta para os usuários.

É possível tornar os componentes mais leves para que você possa ter mais deles. No entanto, uma técnica mais eficaz é muitas vezes evitar ter tantos componentes para renderizar. As seções a seguir descrevem duas abordagens que você pode adotar.

Para obter mais informações sobre o gerenciamento de memória, consulte Hospedar e implantar aplicativos do ASP.NET Core Blazor no lado do servidor.

Componentes filho embutidos nos pais

Considere a seguinte parte de um componente pai que renderiza componentes filho em um loop:

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

O exemplo anterior terá um bom desempenho, se milhares de mensagens não forem mostradas de uma só vez. Para mostrar milhares de mensagens de uma só vez, não considere o componente ChatMessageDisplay separado. Em vez disso, embuta o componente filho no pai. A abordagem a seguir evita a sobrecarga por componente de renderizar tantos componentes filho às custas de perder a capacidade de renderizar novamente a marcação de cada componente filho de forma independente:

<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>
Definir RenderFragments reutilizável no código

Você pode considerar os componentes filho puramente como uma forma de reutilizar a lógica de renderização. Se esse for o caso, você pode criar uma lógica de renderização reutilizável, sem implementar componentes adicionais. No bloco @code de qualquer componente, defina um RenderFragment. Renderize o fragmento em qualquer local quantas vezes forem necessárias:

@RenderWelcomeInfo

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

@RenderWelcomeInfo

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

Para tornar o código RenderTreeBuilder reutilizável em vários componentes, declare o RenderFragment public e static:

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

SayHello no exemplo anterior pode ser invocado a partir de um componente não relacionado. Essa técnica é útil para criar bibliotecas de snippets de marcação reutilizáveis renderizados sem sobrecarga por componente.

Os delegados RenderFragment podem aceitar parâmetros. O seguinte componente passa a mensagem (message) para o 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>;
}

A abordagem anterior reutiliza a lógica de renderização sem sobrecarga por componente. No entanto, a abordagem não permite atualizar a subárvore da interface do usuário de forma independente, nem tem a capacidade de ignorar a renderização da subárvore da interface do usuário quando o pai é renderizado, pois não há limite de componente. A atribuição a um delegado RenderFragment só é compatível com arquivos de componente Razor (.razor) e não há suporte para retornos de chamada de evento.

Para um campo, método ou propriedade não estático que não pode ser referenciado por um inicializador de campo, como TitleTemplate no exemplo a seguir, use uma propriedade em vez de um campo para o RenderFragment:

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

Não receber muitos parâmetros

Se um componente se repetir com muita frequência, por exemplo, centenas ou milhares de vezes, a sobrecarga de passar e receber cada parâmetro será acumulada.

É raro que muitos parâmetros restrinjam severamente o desempenho, mas pode ser um fator. Para um componente TableCell que renderiza 4.000 vezes em uma grade, cada parâmetro passado para o componente adiciona cerca de 15 ms ao custo total de renderização. A passagem de dez parâmetros requer cerca de 150 ms e causa um atraso na renderização da interface do usuário.

Para reduzir a carga do parâmetro, agrupe vários parâmetros em uma classe personalizada. Por exemplo, um componente de célula de tabela pode aceitar um objeto comum. No exemplo a seguir, Data é diferente para cada célula, mas Options é comum em todas as instâncias de célula:

@typeparam TItem

...

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

No entanto, considere que pode ser uma melhoria não ter um componente de célula de tabela, conforme mostrado no exemplo anterior e, em vez disso, embutir a lógica no componente pai.

Observação

Quando várias abordagens estão disponíveis para melhorar o desempenho, o parâmetro de comparação das abordagens geralmente é necessário para determinar qual abordagem produz os melhores resultados.

Para obter mais informações sobre parâmetros de tipo genérico (@typeparam), confira os seguintes recursos:

Garantir que os parâmetros em cascata sejam corrigidos

O componente CascadingValue tem um parâmetro IsFixed opcional:

  • Se IsFixed for false (o padrão), cada destinatário do valor em cascata configurará uma assinatura para receber as notificações de alteração. Cada [CascadingParameter] é substancialmente mais caro do que um [Parameter] normal, devido ao acompanhamento da assinatura.
  • Se IsFixed for true (por exemplo, <CascadingValue Value="someValue" IsFixed="true">), os destinatários receberão o valor inicial, mas não configurarão uma assinatura para receber as atualizações. Cada [CascadingParameter] é leve e não é mais caro do que um [Parameter] normal.

Configurar IsFixed para true melhora o desempenho, se houver um grande número de outros componentes que recebem o valor em cascata. Sempre que possível, defina IsFixed como true nos valores em cascata. Você pode definir IsFixed como true quando o valor fornecido não é alterado ao longo do tempo.

Quando um componente passa this como um valor em cascata, IsFixed também pode ser definido como true:

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

Para obter mais informações, confira Valores em cascata e parâmetros Blazor do ASP.NET Core.

Evitar o splatting de atributos com CaptureUnmatchedValues

Os componentes podem optar por receber valores de parâmetro "incompatíveis" usando o sinalizador CaptureUnmatchedValues:

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

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

Essa abordagem permite passar atributos adicionais arbitrários para o elemento. No entanto, essa abordagem é cara porque o renderizador deve:

  • Corresponder a todos os parâmetros fornecidos com o conjunto de parâmetros conhecidos para criar um dicionário.
  • Acompanhar como várias cópias do mesmo atributo substituem umas às outras.

Use CaptureUnmatchedValues onde o desempenho de renderização de componentes não é crítico, como componentes que não são repetidos com frequência. Para componentes que são renderizados em escala, como cada item em uma lista grande ou nas células de uma grade, tente evitar o splatting de atributos.

Para obter mais informações, confira Nivelamento de atributos Blazor do ASP.NET Core e parâmetros arbitrários.

Implementar SetParametersAsync manualmente

Uma fonte significativa de sobrecarga de renderização por componente é gravar valores de parâmetro de entrada em propriedades [Parameter]. O renderizador usa reflexão para gravar os valores de parâmetro, o que pode levar a um desempenho insatisfatório em escala.

Em alguns casos extremos, talvez você queira evitar a reflexão e implementar sua própria lógica de configuração de parâmetro manualmente. Isso pode ser aplicável quando:

  • Um componente é renderizado com muita frequência, por exemplo, quando há centenas ou milhares de cópias do componente na interface do usuário.
  • Um componente aceita muitos parâmetros.
  • Você descobre que a sobrecarga de recebimento de parâmetros tem um impacto observável na capacidade de resposta da interface do usuário.

Em casos extremos, você pode substituir o método virtual SetParametersAsync do componente e implementar sua própria lógica específica do componente. O exemplo a seguir evita deliberadamente as pesquisas de dicionário:

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

No código anterior, retornar SetParametersAsync da classe base executa o método normal do ciclo de vida sem atribuir parâmetros novamente.

Como você pode ver no código anterior, substituir SetParametersAsync e fornecer a lógica personalizada é complicado e trabalhoso. Portanto, geralmente não recomendamos adotar essa abordagem. Em casos extremos, ela pode melhorar o desempenho de renderização em 20-25%, mas você só deve considerar essa abordagem nos cenários extremos listados anteriormente nesta seção.

Não dispare eventos muito rapidamente

Alguns eventos do navegador são disparados com extrema frequência. Por exemplo, onmousemove e onscroll podem disparar dezenas ou centenas de vezes por segundo. Na maioria dos casos, você não precisa executar atualizações de interface do usuário com tanta frequência. Se os eventos forem disparados muito rapidamente, você poderá prejudicar a capacidade de resposta da interface do usuário ou consumir tempo excessivo de CPU.

Em vez de usar eventos nativos que disparam rapidamente, considere o uso da interoperabilidade JS para registrar um retorno de chamada disparado com menos frequência. Por exemplo, o seguinte componente exibe a posição do mouse, mas só é atualizado no máximo uma vez a 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();
}

O código JavaScript correspondente registra o ouvinte de eventos do DOM para movimentação do mouse. Neste exemplo, o ouvinte de eventos usa a função throttle de Lodash para limitar a taxa de invocações:

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

Evite renderizar novamente após a manipulação de eventos sem alterações de estado

Os componentes herdam de ComponentBase, que invoca StateHasChanged automaticamente depois que os manipuladores de eventos do componente são invocados. Em alguns casos, pode ser desnecessário ou indesejável disparar uma nova renderização, depois que um manipulador de eventos é invocado. Por exemplo, um manipulador de eventos pode não modificar o estado do componente. Nesses cenários, o aplicativo pode aproveitar a interface IHandleEvent para controlar o comportamento da manipulação de eventos do Blazor.

Observação

A abordagem nesta seção não flui exceções aos limites de erro. Para obter mais informações e códigos de demonstração que dão suporte a limites de erro chamando ComponentBase.DispatchExceptionAsync, consulte AsNonRenderingEventHandler + ErrorBoundary = comportamento inesperado (dotnet/aspnetcore #54543).

Para evitar novas renderizações para todos os manipuladores de eventos de um componente, implemente IHandleEvent e forneça uma tarefa IHandleEvent.HandleEventAsync que invoque o manipulador de eventos sem chamar StateHasChanged.

No exemplo a seguir, os manipuladores de eventos adicionados ao componente não disparam uma nova renderização. Portanto, HandleSelect não resulta em uma nova renderização quando invocado.

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

Além de evitar novas renderizações depois que os manipuladores de eventos forem acionados a um componente de forma global, é possível evitar novas renderizações após um único manipulador de eventos, empregando o método de utilitário a seguir.

Adicione a seguinte classe EventUtil a um aplicativo Blazor. As ações e funções estáticas na parte superior da classe EventUtil fornecem manipuladores que abrangem várias combinações de argumentos e tipos de retorno que o Blazor usa ao manipular 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);
    }
}

Chame EventUtil.AsNonRenderingEventHandler para chamar um manipulador de eventos que não dispara uma renderização quando invocado.

No exemplo a seguir:

  • Selecionar o primeiro botão, que chama HandleClick1, dispara uma nova renderização.
  • Selecionar o segundo botão, que chama HandleClick2, não dispara uma nova renderização.
  • Selecionar o terceiro botão, que chama HandleClick3, não dispara uma nova renderização e usa 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);
    }
}

Além de implementar a interface IHandleEvent, aproveitar as outras práticas recomendadas descritas neste artigo também pode ajudar a reduzir as renderizações indesejadas depois que os eventos são manipulados. Por exemplo, a substituição de ShouldRender nos componentes filho do componente de destino pode ser usada para controlar a nova renderização.

Evite recriar delegados para muitos elementos ou componentes repetidos

A recriação do Blazor dos delegados de expressão lambda para elementos ou componentes em um loop pode levar a um desempenho insatisfatório.

O componente a seguir mostrado no artigo de manipulação de eventos renderiza um conjunto de botões. Cada botão atribui um delegado ao seu evento @onclick, o que é bom se não existirem muitos botões a serem renderizados.

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

Se um grande número de botões for renderizado usando a abordagem anterior, a velocidade de renderização será afetada negativamente, levando a uma experiência de usuário insatisfatória. Para renderizar um grande número de botões com um retorno de chamada para eventos de clique, o exemplo a seguir usa uma coleção de objetos de botão que atribuem o delegado @onclick de cada botão a um Action. A abordagem a seguir não requer Blazor para recriar todos os delegados de botão sempre que os botões são renderizados:

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

Otimizar a velocidade de interoperabilidade do JavaScript

As chamadas entre .NET e JavaScript exigem sobrecarga adicional porque:

  • As chamadas são assíncronas.
  • Os parâmetros e valores retornados são serializados por JSON para fornecer um mecanismo de conversão simples entre os tipos .NET e JavaScript.

Além disso, para aplicativos Blazor do lado do servidor, essas chamadas são transmitidas pela rede.

Evitar chamadas excessivamente refinadas

Como cada chamada envolve um pouco de sobrecarga, pode ser útil reduzir o número de chamadas. Considere o código a seguir, que armazena uma coleção de itens no localStorage do navegador:

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

O exemplo anterior faz uma chamada de interoperabilidade separada JS para cada item. Em vez disso, a abordagem a seguir reduz a interoperabilidade JS a uma única chamada:

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

A função JavaScript correspondente armazena toda a coleção de itens no cliente:

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

Para aplicativos Blazor WebAssembly, fazer a rolagem de chamadas de interoperabilidade JS individuais em uma única chamada geralmente só melhora significativamente o desempenho se o componente fizer um grande número de chamadas de interoperabilidade JS.

Considere o uso de chamadas síncronas

Chamar JavaScript do .NET

Esta seção se aplica somente a componentes do lado do cliente.

As chamadas de interoperabilidade de JS são assíncronas, independentemente do código chamado ser síncrono ou assíncrono. As chamadas são assíncronas para garantir que os componentes sejam compatíveis entre os modos de renderização do lado do servidor e do cliente. No servidor, todas as chamadas de interoperabilidade de JS devem ser assíncronas porque são enviadas por uma conexão de rede.

Se tiver certeza de que seu componente só é executado no WebAssembly, você pode optar por fazer chamadas de interoperabilidade de JS síncronas. Isso tem um pouco menos sobrecarga do que fazer chamadas assíncronas e pode resultar em menos ciclos de renderização, pois não há estado intermediário enquanto aguarda resultados.

Para fazer uma chamada síncrona do .NET para o JavaScript em um componente do lado do cliente, converta IJSRuntime em IJSInProcessRuntime para fazer a chamada de interoperabilidade de JS:

@inject IJSRuntime JS

...

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

Ao trabalhar com IJSObjectReference em componentes do lado do cliente do ASP.NET Core 5.0 ou posterior, você pode usar IJSInProcessObjectReference de forma síncrona. IJSInProcessObjectReference implementa IAsyncDisposable/IDisposable e deve ser descartado para coleta de lixo, para evitar uma perda de memória, como demonstra o exemplo a seguir:

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

Chamar .NET do JavaScript

Esta seção se aplica somente a componentes do lado do cliente.

As chamadas de interoperabilidade de JS são assíncronas, independentemente do código chamado ser síncrono ou assíncrono. As chamadas são assíncronas para garantir que os componentes sejam compatíveis entre os modos de renderização do lado do servidor e do cliente. No servidor, todas as chamadas de interoperabilidade de JS devem ser assíncronas porque são enviadas por uma conexão de rede.

Se tiver certeza de que seu componente só é executado no WebAssembly, você pode optar por fazer chamadas de interoperabilidade de JS síncronas. Isso tem um pouco menos sobrecarga do que fazer chamadas assíncronas e pode resultar em menos ciclos de renderização, pois não há estado intermediário enquanto aguarda resultados.

Para fazer uma chamada síncrona do JavaScript para o .NET em um componente do lado do cliente, use DotNet.invokeMethod em vez de DotNet.invokeMethodAsync.

As chamadas síncronas funcionarão se:

  • O componente só é renderizado para execução no WebAssembly.
  • A função chamada retorna um valor de forma síncrona. A função não é um método async e não retorna um Task do .NET ou um Promise do JavaScript.

Esta seção se aplica somente a componentes do lado do cliente.

As chamadas de interoperabilidade de JS são assíncronas, independentemente do código chamado ser síncrono ou assíncrono. As chamadas são assíncronas para garantir que os componentes sejam compatíveis entre os modos de renderização do lado do servidor e do cliente. No servidor, todas as chamadas de interoperabilidade de JS devem ser assíncronas porque são enviadas por uma conexão de rede.

Se tiver certeza de que seu componente só é executado no WebAssembly, você pode optar por fazer chamadas de interoperabilidade de JS síncronas. Isso tem um pouco menos sobrecarga do que fazer chamadas assíncronas e pode resultar em menos ciclos de renderização, pois não há estado intermediário enquanto aguarda resultados.

Para fazer uma chamada síncrona do .NET para o JavaScript em um componente do lado do cliente, converta IJSRuntime em IJSInProcessRuntime para fazer a chamada de interoperabilidade de JS:

@inject IJSRuntime JS

...

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

Ao trabalhar com IJSObjectReference em componentes do lado do cliente do ASP.NET Core 5.0 ou posterior, você pode usar IJSInProcessObjectReference de forma síncrona. IJSInProcessObjectReference implementa IAsyncDisposable/IDisposable e deve ser descartado para coleta de lixo, para evitar uma perda de memória, como demonstra o exemplo a seguir:

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

Considere o uso de chamadas com unmarshaling

Esta seção só se aplica a aplicativos Blazor WebAssembly.

Ao executar no Blazor WebAssembly, é possível fazer chamadas com unmarshaling do .NET para o JavaScript. Elas são chamadas síncronas que não executam a serialização por JSON de argumentos ou valores retornados. Todos os aspectos do gerenciamento de memória e traduções entre representações .NET e JavaScript são deixados para o desenvolvedor.

Aviso

Embora o uso de IJSUnmarshalledRuntime tenha a menor sobrecarga das abordagens de interoperabilidade JS, as APIs JavaScript necessárias para interagir com essas APIs não estão documentadas atualmente e estão sujeitas a alterações interruptivas nas versões 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");
    }
}

Usar interoperabilidade [JSImport]/[JSExport] JavaScript

A interoperabilidade [JSImport]/[JSExport] JavaScript para aplicativos Blazor WebAssembly oferece melhor desempenho e estabilidade em relação à API de interoperabilidade JS nas versões de estrutura antes do ASP.NET Core no .NET 7.

Para obter mais informações, consulte Interop de JSImport/JSExport do JavaScript com o ASP.NET Core Blazor.

Compilação AOT (Ahead Of Time)

A compilação AOT (Ahead Of Time) compila o código do .NET de um aplicativo Blazor diretamente no WebAssembly nativo para execução direta pelo navegador. Aplicativos com compilação AOT resultam em aplicativos maiores que levam mais tempo para serem baixados, mas aplicativos com complicação AOT geralmente fornecem melhor desempenho de runtime, especialmente para aplicativos que executam tarefas com uso intensivo de CPU. Para obter mais informações, confira Ferramentas de criação e compilação antecipada (AOT) do Blazor WebAssembly no ASP.NET Core.

Minimizar o tamanho de download do aplicativo

Nova vinculação de runtime

Para obter informações sobre como a revinculação do runtime minimiza o tamanho de download de um aplicativo, confira Ferramentas de criação e compilação antecipada (AOT) do Blazor WebAssembly no ASP.NET Core.

Use System.Text.Json.

A implementação de interop JS do Blazor conta com System.Text.Json, que é uma biblioteca de serialização por JSON de alto desempenho com baixa alocação de memória. O uso de System.Text.Json não deve resultar em tamanho de conteúdo de aplicativo adicional ao adicionar uma ou mais bibliotecas JSON alternativas.

Para obter diretrizes de migração, confira Como migrar de Newtonsoft.Json para System.Text.Json.

Filtragem de IL (Linguagem Intermediária)

Esta seção se aplica somente a cenários Blazor do lado do cliente.

Cortar assemblies não utilizados de um aplicativo Blazor WebAssembly reduz o tamanho do aplicativo, removendo o código não utilizado nos binários do aplicativo. Para obter mais informações, confira Configurar o filtro para Blazor do ASP.NET Core.

Vincular um aplicativo Blazor WebAssembly reduz o tamanho do aplicativo, cortando o código não utilizado nos binários do aplicativo. O Vinculador de IL (Linguagem Intermediária) só é habilitado ao criar na configuração Release. Para se beneficiar disso, publique o aplicativo para implantação usando o comando dotnet publish com a opção -c|--configuration definida como Release:

dotnet publish -c Release

Assemblies de carga lentos

Esta seção se aplica somente a cenários Blazor do lado do cliente.

Carregue os assemblies em runtime quando os assemblies forem exigidos por uma rota. Para obter mais informações, confira Assemblies de carregamento lentos no Blazor WebAssembly do ASP.NET Core.

Compactação

Esta seção só se aplica a aplicativos Blazor WebAssembly.

Quando um aplicativo do Blazor WebAssembly é publicado, a saída é compactada estaticamente durante a publicação para reduzir o tamanho do aplicativo e remover a sobrecarga para compactação de runtime. Blazor conta com o servidor para executar a negociação de conteúdo e fornecer arquivos compactados estaticamente.

Depois que um aplicativo for implantado, verifique se o aplicativo atende a arquivos compactados. Inspecione a guia Rede nas ferramentas de desenvolvedor de um navegador e verifique se os arquivos são atendidos com Content-Encoding: br (compactação Brotli) ou Content-Encoding: gz (compactação Gzip). Se o host não estiver atendendo aos arquivos compactados, siga as instruções em Hospedar e implantar Blazor WebAssembly do ASP.NET Core.

Desabilitar contas não utilizadas

Esta seção se aplica somente a cenários Blazor do lado do cliente.

O runtime do Blazor WebAssembly inclui os seguintes recursos do .NET que podem ser desabilitados para obter um tamanho de conteúdo menor:

  • O Blazor WebAssembly carrega recursos de globalização necessários para exibir valores, como datas e moeda, na cultura do usuário. Se o aplicativo não exigir localização, você pode configurar o aplicativo para aceitar a cultura invariável, o que geralmente se baseia na cultura en-US.
  • A adoção da globalização invariável resulta apenas no uso de nomes de fuso horário não localizados. Para aparar os dados e o código de fuso horário do aplicativo, aplique a propriedade MSBuild <InvariantTimezone> com um valor de true no arquivo de projeto do aplicativo:

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

    Observação

    <BlazorEnableTimeZoneSupport> substitui uma configuração <InvariantTimezone> anterior. Recomendamos remover a configuração <BlazorEnableTimeZoneSupport>.

  • Um arquivo de dados é incluído para corrigir as informações de fuso horário. Se o aplicativo não exigir esse recurso, desabilite-o definindo a propriedade MSBuild <BlazorEnableTimeZoneSupport> no arquivo de projeto do aplicativo como false:

    <PropertyGroup>
      <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
    </PropertyGroup>
    
  • As informações de ordenação são incluídas para fazer com que as APIs como StringComparison.InvariantCultureIgnoreCase funcionem corretamente. Se você tiver certeza de que o aplicativo não exige os dados de ordenação, desabilite-os definindo a propriedade MSBuild BlazorWebAssemblyPreserveCollationData no arquivo de projeto do aplicativo como false:

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