Partilhar via


Práticas recomendadas de desempenho do ASP.NET Core Blazor

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Importante

Estas informações referem-se a um produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado comercialmente. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

Para a versão atual, consulte a versão .NET 9 deste artigo.

Blazor é otimizado para alto desempenho na maioria dos cenários realistas de interface do usuário do 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) ede análise estática de estado nulo do compilador .NET, que são suportados no ASP.NET Core no .NET 6 ou posterior.

Otimize 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 gerar uma melhoria de dez vezes ou mais na velocidade de renderização da interface do usuário.

Evite a renderização desnecessária de subárvores de componentes

Talvez seja possível remover a maior parte do custo de renderização de um componente pai ignorando a rerenderização das subárvores dos componentes filhos quando ocorre um evento. Você só deve se preocupar em ignorar a rerenderização de subárvores que são particularmente caras de renderizar e estão a causar lentidão na interface do utilizador.

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

  1. O evento é enviado para o componente que processou o manipulador do evento. Depois de executar o manipulador de eventos, o componente é rerenderizado.
  2. Quando um componente é renderizado novamente, fornece uma nova cópia dos valores de parâmetros para cada um de seus componentes filhos.
  3. Depois que um novo conjunto de valores de parâmetro é recebido, cada componente decide se deseja renderizar novamente. Os componentes são rerenderizados se os valores dos parâmetros podem ter sido alterados, por exemplo, se forem objetos mutáveis.

As duas últimas etapas da sequência anterior continuam recursivamente para baixo na hierarquia de componentes. Em muitos casos, toda a subárvore é rerenderizada. Eventos direcionados a componentes de alto nível podem causar rerenderização dispendiosa porque 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:

  • Certifique-se de que os parâmetros do componente filho sejam de tipos imutáveis primitivos, como string, int, bool, DateTimee outros tipos semelhantes. A lógica interna para detetar alterações ignora automaticamente a rerenderização se os valores de parâmetros imutáveis primitivos não tiverem sido alterados. Se renderizar um componente filho com <Customer CustomerId="item.CustomerId" />, onde CustomerId é um tipo int, então o componente Customer não será reprocessado, a menos que item.CustomerId seja modificado.
  • Substituir ShouldRender:
    • Para aceitar valores de parâmetros não primitivos, como tipos de modelo personalizados complexos, retornos de chamada de eventos ou valores RenderFragment.
    • Se a criação de um componente somente da interface do usuário não for alterado após a renderização inicial, independentemente das alterações no valor do parâmetro.

O exemplo de ferramenta de pesquisa de voos de companhias aéreas a seguir usa campos privados para rastrear as informações necessárias para detetar alterações. O identificador de voo de entrada anterior (prevInboundFlightId) e o identificador de voo de saída anterior (prevOutboundFlightId) rastreiam informações para a próxima atualização de componente potencial. Se qualquer um dos identificadores de voo mudar quando os parâmetros do componente forem definidos em OnParametersSet, o componente será reprocessado porque shouldRender está definido como true. Se shouldRender for avaliado como false após a verificação dos identificadores de voo, evita-se um novo processamento dispendioso.

@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 rerenderização no nível de manipuladores de eventos individuais geralmente não é necessário.

Para obter mais informações, consulte os seguintes recursos:

Virtualização

Ao renderizar grandes quantidades de interface do usuário dentro de um loop, por exemplo, uma lista ou grade com milhares de entradas, a grande quantidade 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 a aparência e os comportamentos de rolagem de uma lista arbitrariamente grande enquanto renderiza apenas os itens de lista que estão dentro do viewport 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, consulte ASP.NET Core Razor component virtualization.

Crie componentes leves e otimizados

A maioria dos componentes Razor não requer esforços de otimização agressivos porque a maioria dos componentes não se repete na interface de utilizador e não é redesenhada com alta frequência. Por exemplo, componentes roteáveis com uma diretiva @page e componentes usados para renderizar elementos de nível superior da interface do utilizador, como caixas de diálogo ou formulários, provavelmente aparecem apenas um de cada vez e só são renderizados novamente em resposta a uma ação do utilizador. Esses componentes geralmente não criam alta carga de trabalho de renderização, 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 baixo desempenho da interface do usuário:

  • Grandes formulários aninhados 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 modelar cada elemento, célula ou ponto de dados como uma instância de componente separada, geralmente há tantos deles que seu desempenho de renderização se torna crítico. Esta seção fornece conselhos sobre como tornar esses componentes leves para que a interface do usuário permaneça rápida e responsiva.

Evite milhares de instâncias de componentes

Cada componente é uma ilha separada que pode render independentemente de seus pais e filhos. Ao escolher como dividir a interface do usuário em uma hierarquia de componentes, você assume o controle sobre a granularidade da renderização da interface do usuário. Isso pode resultar em bom ou mau desempenho.

Ao dividir a interface do utilizador em componentes separados, é possível que partes menores da interface do usuário sejam atualizadas quando ocorrerem eventos. Numa tabela com muitas linhas, onde há um botão em cada linha, pode conseguir renderizar novamente apenas a linha específica utilizando 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 seu estado independente e ciclo de vida de renderização.

Em um teste realizado pelos engenheiros da unidade de produto ASP.NET Core, uma sobrecarga de renderização de cerca de 0,06 ms por instância de componente foi observada em um aplicativo Blazor WebAssembly. O aplicativo de teste renderizou um componente simples que aceita três parâmetros. Internamente, a sobrecarga é em grande parte devida à recuperação do estado de cada componente a partir de dicionários e à passagem e receção de parâmetros. Por multiplicação, você pode ver que adicionar 2.000 instâncias de componentes extras adicionaria 0,12 segundos ao tempo de renderização e a interface do usuário começaria a parecer 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 poderosa é 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 a gestão de memória, consulte Host e desenvolva aplicações Blazor ASP.NET Core do lado do servidor.

Componentes infantis embutidos em seus pais

Considere a seguinte parte de um componente pai que processa 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 funciona bem se milhares de mensagens não forem mostradas de uma só vez. Para mostrar milhares de mensagens de uma só vez, considere não extrair o componente ChatMessageDisplay separado. Em vez disso, incorpore o componente filho no pai. ** A abordagem a seguir evita a sobrecarga por componente de renderizar tantos componentes filhos, à custa de se perder a capacidade de rerenderizar o markup 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 como reutilizável no código

Você pode estar descontando componentes filho puramente como uma forma de reutilizar a lógica de renderização. Se esse for o caso, você pode criar lógica de renderização reutilizável sem implementar componentes adicionais. No bloco @code de qualquer componente, defina um RenderFragment. Renderize o fragmento de 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 RenderTreeBuilder código reutilizável em múltiplos componentes, declare o RenderFragmentpublic 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 trechos de marcação reutilizáveis que são renderizados sem sobrecarga por componente.

RenderFragment delegados podem aceitar parâmetros. O componente a seguir passa a mensagem (message) para o RenderFragment delegado:

<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 utilizador de forma independente, nem tem a capacidade de ignorar a renderização da subárvore da interface do utilizador quando o seu elemento pai é renderizado, porque não existe uma fronteira de componente. A atribuição a um delegado RenderFragment apenas é suportada em ficheiros de componentes Razor (.razor).

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 receba muitos parâmetros

Se um componente se repete com extrema frequência, por exemplo, centenas ou milhares de vezes, a sobrecarga de passar e receber cada parâmetro se acumula.

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

Para reduzir a carga de parâmetros, 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, tenha em mente que agrupar parâmetros primitivos em uma classe nem sempre é uma vantagem. Embora possa reduzir a contagem de parâmetros, também afeta como a deteção de alterações e a renderização se comportam. Passar parâmetros não primitivos sempre dispara uma rerenderização, porque Blazor não pode saber se objetos arbitrários têm estado internamente mutável, enquanto passar parâmetros primitivos só dispara uma rerenderização se seus valores tiverem sido realmente alterados.

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

Observação

Quando várias abordagens estão disponíveis para melhorar o desempenho, a avaliação comparativa das abordagens é geralmente necessária para determinar qual abordagem produz os melhores resultados.

Para obter mais informações sobre parâmetros de tipo genéricos (@typeparam), consulte 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 (padrão), cada destinatário do valor em cascata configura uma subscrição para receber notificações de alteração. Cada [CascadingParameter] é substancialmente mais caro do que um [Parameter] normal devido ao rastreamento 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 atualizações. Cada [CascadingParameter] é leve e não é mais caro do que um [Parameter]normal.

Definir IsFixed como 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 para true em valores em cascata. Você pode definir IsFixed como true quando o valor fornecido não muda ao longo do tempo.

Quando um componente passa this como um valor em cascata, IsFixed também pode ser definido como true, porque this nunca muda durante o ciclo de vida do componente:

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

Para obter mais informações, consulte ASP.NET Core Blazor valores e parâmetros em cascata.

Evite a dispersão de atributos com CaptureUnmatchedValues

Os componentes podem optar por receber valores de parâmetros "incompará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 todos os parâmetros fornecidos com o conjunto de parâmetros conhecidos para criar um dicionário.
  • Acompanhe como várias cópias do mesmo atributo se substituem.

Use CaptureUnmatchedValues em que 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 numa lista longa ou nas células de uma grelha, tente evitar o splatting de atributos.

Para obter mais informações, consulte ASP.NET Core Blazor atributo splatting e parâmetros arbitrários.

Implementar SetParametersAsync manualmente

Uma fonte significativa de sobrecarga de renderização por componente é gravar valores de parâmetros de entrada em propriedades [Parameter]. O renderizador utiliza a reflexão para escrever os valores dos parâmetros, o que pode levar a um desempenho ruim em grande escala.

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

  • Um componente é renderizado com extrema 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 dos parâmetros de recebimento tem um impacto observável na capacidade de resposta da interface do usuário.

Em casos extremos, pode-se substituir o método virtual SetParametersAsync do componente e implementar a sua própria lógica específica do componente. O exemplo a seguir evita deliberadamente 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 a classe base SetParametersAsync executa o método de ciclo de vida normal sem atribuir parâmetros novamente.

Como você pode ver no código anterior, substituir SetParametersAsync e fornecer lógica personalizada é complicado e trabalhoso, por isso geralmente não recomendamos a adoção dessa abordagem. Em casos extremos, ele 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 acione eventos muito rapidamente

Alguns eventos do navegador são acionados 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 da interface do usuário com frequência. Se os eventos forem acionados muito rapidamente, você poderá prejudicar a capacidade de resposta da interface do usuário ou consumir tempo excessivo da CPU.

Em vez de usar eventos nativos que disparam rapidamente, considere utilizar a interoperabilidade do JS para registar uma função de retorno que é acionada com menos frequência. Por exemplo, o componente a seguir exibe a posição do mouse, mas só atualiza 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 DOM para o movimento do mouse. Neste exemplo, o ouvinte de eventos usa a função do 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 rerenderização ao manipular eventos sem alterações de estado

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

Observação

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

Para evitar rerenderizaçõ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, nenhum manipulador de eventos adicionado ao componente dispara uma rerenderização, portanto, HandleSelect não resulta em uma rerenderizaçã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 rerenderizações depois que manipuladores de eventos são acionados em um componente de forma global, é possível evitar rerenderizações após um único manipulador de eventos empregando o seguinte método utilitário.

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 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 acione uma renderização quando invocado.

No exemplo a seguir:

  • Selecionar o primeiro botão, que chama HandleClick1, dispara uma re-renderização.
  • Selecionar o segundo botão, que chama HandleClick2, não provoca uma nova renderização.
  • Selecionar o terceiro botão, que chama HandleClick3, não aciona uma re-renderização e utiliza os 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 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 utilizada para controlar a re-renderização.

Evite recriar delegados para elementos ou componentes usados repetidamente

Blazorrecriação de expressão lambda delega para elementos ou componentes em um loop pode levar a um desempenho ruim.

O componente a seguir mostrado no artigo de manipulação de eventos renderiza um conjunto de botões. Cada botão aloca um delegado ao seu evento @onclick, o que não é problemático se não houver muitos botões para renderizar.

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 ruim. 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ões que atribuem o delegado @onclick de cada botão a um Action. A abordagem a seguir não exige que Blazor reconstrua todos os delegados de botão cada vez 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 => { };
    }
}

Otimize a velocidade de interoperabilidade do JavaScript

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

  • As chamadas são assíncronas.
  • Parâmetros e valores de retorno são serializados em JSON para fornecer um mecanismo de conversão fácil de entender entre os tipos .NET e JavaScript.

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

Evite chamadas excessivamente refinadas

Uma vez que cada chamada envolve alguma sobrecarga, pode ser valioso reduzir o número de chamadas. Considere o seguinte código, que armazena uma coleção de itens no localStoragedo 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 de JS separada para cada item. Em vez disso, a seguinte abordagem reduz o JS interop 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, rolar chamadas individuais de interoperabilidade de JS 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 a partir do .NET

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

JS chamadas de interoperabilidade são assíncronas, independentemente de o 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 lado do cliente. No servidor, todas as chamadas de interoperabilidade JS devem ser assíncronas porque são enviadas por uma conexão de rede.

Se tiveres a certeza de que o teu componente só é executado em WebAssembly, podes fazer chamadas de interoperabilidade síncronas JS. Isso tem um pouco menos de sobrecarga do que fazer chamadas assíncronas e pode resultar em menos ciclos de renderização porque não há nenhum estado intermediário enquanto aguarda resultados.

Para efetuar uma chamada síncrona do .NET para JavaScript num componente do lado do cliente, é necessário converter IJSRuntime para IJSInProcessRuntime de modo a realizar a chamada interop 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 no lado do cliente no ASP.NET Core 5.0 ou versões posteriores, pode-se usar IJSInProcessObjectReference de forma síncrona, em vez disso. IJSInProcessObjectReference implementa IAsyncDisposable/IDisposable e deve ser descartado para coleta de lixo para evitar um vazamento de memória, como demonstra o exemplo a seguir:

@inject IJSRuntime JS
@implements IDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var jsInProcess = (IJSInProcessRuntime)JS;
            module = await jsInProcess.Invoke<IJSInProcessObjectReference>("import", 
                "./scripts.js");
            var value = module.Invoke<string>("javascriptFunctionIdentifier");
        }
    }

    ...

    void IDisposable.Dispose()
    {
        if (module is not null)
        {
            await module.Dispose();
        }
    }
}

No exemplo anterior, um JSDisconnectedException não fica preso durante o descarte do módulo porque não há Blazor-SignalR circuito em um aplicativo Blazor WebAssembly a perder. Para obter mais informações, consulte ASP.NET Core Blazor interoperabilidade JavaScript (JS interop).

Chamar .NET a partir de JavaScript

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

JS chamadas de interoperação são assíncronas, independentemente de o 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 lado do cliente. No servidor, todas as chamadas de interoperabilidade JS devem ser assíncronas porque são enviadas por uma conexão de rede.

Se souberes com certeza que o teu componente funciona apenas em WebAssembly, podes optar por fazer chamadas de interoperabilidade JS síncronas. Isso tem um pouco menos de sobrecarga do que fazer chamadas assíncronas e pode resultar em menos ciclos de renderização porque não há nenhum estado intermediário enquanto aguarda resultados.

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

As chamadas síncronas funcionam se:

  • O componente só é renderizado para execução em 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 .NET ou JavaScript Promise.

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

JS chamadas de interoperabilidade são assíncronas, independentemente de o 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 lado do cliente. No servidor, todas as chamadas de interoperabilidade JS devem ser assíncronas porque são enviadas por uma conexão de rede.

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

Para fazer uma chamada síncrona do .NET para JavaScript num componente do lado do cliente, faça a conversão de IJSRuntime para IJSInProcessRuntime para realizar a chamada de interoperabilidade JS.

@inject IJSRuntime JS

...

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

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

@inject IJSRuntime JS
@implements IDisposable

...

@code {
    ...
    private IJSInProcessObjectReference? module;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            var jsInProcess = (IJSInProcessRuntime)JS;
            module = await jsInProcess.Invoke<IJSInProcessObjectReference>("import", 
                "./scripts.js");
            var value = module.Invoke<string>("javascriptFunctionIdentifier");
        }
    }

    ...

    void IDisposable.Dispose()
    {
        if (module is not null)
        {
            await module.Dispose();
        }
    }
}

No exemplo anterior, um JSDisconnectedException não fica preso durante o descarte do módulo porque não há circuito Blazor-SignalR numa aplicação Blazor WebAssembly a perder. Para obter mais informações, consulte ASP.NET Interoperabilidade do Core Blazor JavaScript (JS interoperabilidade).

Considere o uso de chamadas não processadas

Esta secção aplica-se apenas a aplicações Blazor WebAssembly.

Ao executar em Blazor WebAssembly, é possível fazer chamadas não organizadas do .NET para o JavaScript. Estas são chamadas síncronas que não realizam a serialização JSON dos argumentos ou dos valores de retorno. Todos os aspetos do gerenciamento de memória e traduções entre representações .NET e JavaScript são deixados para o desenvolvedor.

Advertência

Embora o uso do IJSUnmarshalledRuntime tenha a menor sobrecarga das abordagens de interoperabilidade JS, as APIs JavaScript necessárias para interagir com essas APIs estão atualmente não documentadas e sujeitas a alterações significativas em 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 a interoperabilidade JavaScript [JSImport]/[JSExport]

A interoperabilidade JavaScript [JSImport]/[JSExport] para aplicativos Blazor WebAssembly oferece melhor desempenho e estabilidade em relação à API de interoperabilidade JS em lançamentos anteriores ao ASP.NET Core no .NET 7.

Para mais informações, consulte a interoperabilidade do JSImport/JSExport JavaScript com ASP.NET Core Blazor.

Compilação Antecipada (AOT)

A compilação Ahead-of-Time (AOT) compila o código .NET de um aplicativo Blazor diretamente no WebAssembly nativo para execução direta pelo navegador. Os aplicativos compilados pela AOT resultam em aplicativos maiores que levam mais tempo para serem baixados, mas os aplicativos compilados pela AOT geralmente fornecem um melhor desempenho em tempo de execução, especialmente para aplicativos que executam tarefas com uso intensivo de CPU. Para obter mais informações, consulte ASP.NET Core Blazor WebAssembly ferramentas de construção e compilação antecipada (AOT).

Minimizar o tamanho do download do aplicativo

Revinculação de tempo de execução

Para obter informações sobre como a revinculação em tempo de execução minimiza o tamanho do download de um aplicativo, consulte ASP.NET Core Blazor WebAssembly build tools and ahead-of-time (AOT) compilation.

Utilize System.Text.Json

A implementação de interoperabilidade de JS de Blazordepende de System.Text.Json, que é uma biblioteca de serialização JSON de alto desempenho com baixa alocação de memória. O uso de System.Text.Json não deve resultar em um aumento do tamanho do pacote da aplicação em comparação com a adição de uma ou mais bibliotecas alternativas de JSON.

Para obter orientações sobre migração, consulte Como migrar do Newtonsoft.Json para o System.Text.Json.

Corte de linguagem intermediária (IL)

Esta seção só se aplica a cenários de 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, consulte Configurar o Trimmer para ASP.NET Core Blazor.

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 linguagem intermediária (IL) 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

Montagens de carga lenta

Esta seção só se aplica a cenários de Blazor do lado do cliente.

Carregue conjuntos de componentes em tempo de execução quando necessário por uma rota. Para obter mais informações, consulte Montar montagens de carga lenta no ASP.NET Core Blazor WebAssembly.

Compressão

Esta secção aplica-se apenas a aplicações Blazor WebAssembly.

Quando um aplicativo 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 em tempo de execução. Blazor depende do servidor para executar a negociação de conteúdo e servir arquivos compactados estaticamente.

Depois que um aplicativo for implantado, verifique se o aplicativo serve arquivos compactados. Inspecione a guia Network no de ferramentas de desenvolvedor de um navegador e verifique se os arquivos são servidos com (compactação Brotli) ou (compactação Gzip). Se o host não estiver servindo arquivos compactados, siga as instruções em Host e implante ASP.NET Core Blazor WebAssembly.

Desativar recursos não utilizados

Esta seção só se aplica a cenários de Blazor do lado do cliente.

O tempo de execução do Blazor WebAssemblyinclui os seguintes recursos .NET que podem ser desabilitados para um tamanho de carga útil menor:

  • A adoção da globalização inváriante resulta apenas no uso de nomes de fuso horário não localizados. Para cortar o código de fuso horário e os dados do aplicativo, aplique a propriedade <InvariantTimezone> MSBuild com um valor de true no arquivo de projeto do aplicativo:

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

    Observação

    <BlazorEnableTimeZoneSupport> substitui uma configuração de <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, considere desativá-lo definindo a propriedade <BlazorEnableTimeZoneSupport> MSBuild como false no arquivo de projeto do aplicativo:

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

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