Compartilhar via


Práticas recomendadas de desempenho de renderização do ASP.NET Core Blazor

Observação

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

Aviso

Esta versão do ASP.NET Core não tem mais suporte. Para obter mais informações, consulte a Política de Suporte do .NET e do .NET Core. Para informações sobre a versão vigente, confira a versão .NET 9 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 garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

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

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 renderização desnecessária de subárvores de componente

Você pode remover a maior parte dos custos de renderização de um componente pai evitando a re-renderização de subárvores de componentes filhos quando um evento ocorre. Você deve se preocupar apenas em evitar pular a nova renderização de subárvores que são particularmente caras para renderizar e estão causando atraso na interface do usuário.

Em runtime, 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 seus próprios componentes filho e assim por diante. Quando ocorre um evento, como um usuário selecionando um botão, o processo a seguir determina quais componentes gerar novamente:

  1. O evento é enviado 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 dos parâmetros para cada um de seus componentes filho.
  3. Depois que um novo conjunto de valores de parâmetro é recebido, Blazor decide se deseja rerender o componente. Os componentes serão rerenderizados se ShouldRender retornar true, que é o comportamento padrão, a menos que seja substituído, e os valores dos parâmetros podem ter mudado, 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 é rerenderizada. Eventos direcionados a componentes de alto nível podem causar uma re-renderização de alto custo porque cada componente abaixo dele deve ser re-renderizado.

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 específicos†, como string, int, bool e DateTime. A lógica embutida para detectar alterações ignora automaticamente a re-renderização caso os valores dos parâmetros imutáveis não tenham mudado. Se você renderizar um componente filho com <Customer CustomerId="item.CustomerId" />, onde CustomerId é um tipo int, o componente Customer não será rerenderizado, a menos que item.CustomerId seja alterado.
  • Substituir ShouldRender, retornando false:
    • Quando os parâmetros são tipos não primitivos ou tipos imutáveis sem suporte†, como tipos complexos de modelo personalizado ou valores RenderFragment, e os valores de parâmetro não foram alterados,
    • 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 no valor do parâmetro.

†Para obter mais informações, consulte a lógica de detecção de alterações na Blazororigem de referência (ChangeDetection.cs).

Observação

Os links de documentação para o código fonte de referência do .NET geralmente carregam a ramificação padrão do repositório, que representa o desenvolvimento atual para a próxima versão do .NET. Para selecionar uma tag para uma versão específica, use a lista suspensa Alternar entre branches ou tags. Para saber mais, confira Como selecionar uma marca de versão do código-fonte do ASP.NET Core (dotnet/AspNetCore.Docs #26205).

O exemplo da ferramenta de pesquisa de voo de companhia aérea a seguir usa campos privados para acompanhar as informações necessárias para detectar alterações. O identificador do voo de entrada anterior (prevInboundFlightId) e o identificador do voo de saída anterior (prevOutboundFlightId) rastreiam informações para a próxima possível atualização do componente. Se qualquer um dos identificadores de voo mudar quando os parâmetros do componente estiverem definidos em OnParametersSet, o componente será rerenderizado porque shouldRender está definido para true. Se shouldRender é avaliado como false depois de verificar os identificadores de voo, uma cara rerenderização 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 re-renderização no nível de manipuladores de eventos individuais não é geralmente necessária.

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

Virtualização

Ao renderizar grandes quantidades de interface do usuário em 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 rolar, 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 de tamanho arbitrário, renderizando apenas os itens de lista que estão dentro da área de visualização 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 não exige esforços agressivos de otimização, porque eles não se repetem na interface do usuário e não se renderizam em alta frequência. Por exemplo, componentes roteáveis com uma @page diretiva 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 re-renderizados 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 ruim da interface do usuário:

  • Formulários grandes e aninhados com centenas de elementos individuais, como campos de entrada ou etiquetas.
  • Grades com centenas de linhas ou milhares de células.
  • Gráficos de dispersão com milhões de pontos de dados.

Se cada elemento, célula ou ponto de dados for modelado como uma instância de componente separada, geralmente há tantos que o 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 componente

Cada componente é uma ilha separada que pode renderizar independentemente de seus 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 bom ou ruim desempenho.

Ao dividir a interface do usuário em componentes distintos, é possível que porções menores da interface sejam recarregadas quando eventos ocorrerem. Em uma tabela com muitas linhas, cada uma com um botão, é possível fazer com que apenas essa linha específica seja atualizada 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 seu estado independente e ciclo de vida de renderização.

Em um teste realizado 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 Blazor WebAssembly aplicativo. O aplicativo de teste renderiza um componente simples que aceita três parâmetros. Internamente, a sobrecarga deve-se, em grande parte, à recuperação do estado de cada componente a partir de dicionários e à passagem e recebimento de parâmetros. Por multiplicação, você pode ver que a adição de 2.000 instâncias de componentes adicionais adicionaria 0,12 segundos ao tempo de renderização e a interface do usuário começará 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 poderosa geralmente é 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 Gerenciar memória em aplicativos ASP.NET Core implantados no lado do servidorBlazor.

Componentes filho embutidos em seus 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 vez, considere não separar o componente ChatMessageDisplay. Em vez disso, incorpore o componente filho no componente pai. A abordagem a seguir evita a sobrecarga de renderização por componente individual ao custo de perder a capacidade de renderizar novamente, de forma independente, a marcação de cada componente filho.

<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 elementos reutilizáveis RenderFragments no código: você pode estar extraindo componentes filhos para reutilizar a lógica de renderização. Se esse for o caso, você poderá criar uma lógica de renderização reutilizável sem implementar componentes adicionais. Em qualquer bloco de @code de um componente, defina um RenderFragment. Renderize o fragmento de qualquer local quantas vezes for necessário:

@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 o código reutilizável em vários componentes, declare o RenderFragmentpublic e static:

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

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

RenderFragment os delegados podem aceitar parâmetros. O seguinte componente 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 de forma independente a subárvore da interface do usuário, nem tem a capacidade de ignorar a renderização da subárvore da interface do usuário quando seu pai é renderizado, porque não há fronteira de componente. A atribuição a um RenderFragment delegado só tem suporte nos Razor arquivos de componente (.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:RenderFragment

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

Não receba 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 TableCell componente 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. 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 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, tenha em mente que agrupar parâmetros primitivos em uma classe nem sempre é uma vantagem. Embora possa reduzir a contagem de parâmetros, ela também afeta o comportamento de detecção e renderização de alterações. A passagem de parâmetros não primitivos sempre dispara uma renderização novamente, porque Blazor não é possível saber se os objetos arbitrários têm um estado mutável internamente, enquanto a passagem de parâmetros primitivos só dispara uma renderização se seus valores realmente foram alterados.

Além disso, 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 sua lógica no componente pai.

Observação

Quando várias abordagens estão disponíveis para melhorar o desempenho, o benchmark 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), consulte os seguintes recursos:

Garanta que os parâmetros em cascata estejam corrigidos

O CascadingValue componente tem um parâmetro opcional IsFixed :

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

A configuração 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 em valores em cascata. Você pode definir IsFixedtrue 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, porque this nunca é alterado durante o ciclo de vida do componente:

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

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

Evite a disseminação de atributos com CaptureUnmatchedValues

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

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

  • Compare todos os parâmetros fornecidos com os parâmetros conhecidos para construir um dicionário.
  • Acompanhe 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 atributo splatting.

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 é escrever valores de parâmetros de entrada em propriedades [Parameter]. O renderizador usa reflexão para gravar os valores de parâmetro, o que pode levar a um desempenho em escala ruim.

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 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 SetParametersAsync base 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, portanto, geralmente não recomendamos adotar essa 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 acionem eventos muito rapidamente

Alguns eventos do navegador são disparados com extrema frequência. Por exemplo, onmousemove e onscroll pode 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 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 usar JS interop para registrar um callback que é acionado 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 DOM para movimento do mouse. Neste exemplo, o ouvinte de eventos usa a função de throttle 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 rerenderizar após a manipulação de eventos sem alterações de estado

Os componentes herdam de ComponentBase, que invoca StateHasChanged automaticamente após os manipuladores de eventos do componente serem acionados. Em alguns casos, pode ser desnecessário ou indesejável disparar uma rerender 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 utilizar a interface IHandleEvent para controlar o processamento de eventos de Blazor.

Observação

A abordagem nesta seção não propaga exceções para limites de erro. Para mais informações e código de demonstração que suporta limites de erro ao chamar ComponentBase.DispatchExceptionAsync, consulte AsNonRenderingEventHandler + ErrorBoundary = comportamento inesperado (dotnet/aspnetcore número 54543).

Para evitar re-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, nenhum manipulador de eventos adicionado ao componente dispara um rerender, portanto HandleSelect , não resulta em um rerender 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 prevenir re-renderizações após a execução dos manipuladores de eventos em um componente de maneira global, é possível evitar re-renderizações após um único manipulador de eventos, ao empregar o método utilitário a seguir.

Adicione a classe a seguir EventUtil a um Blazor aplicativo. As ações e funções estáticas na parte superior da EventUtil classe fornecem manipuladores que abrangem várias combinações de argumentos e tipos de retorno que Blazor usam ao lidar com 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:

  • Selecionando o primeiro botão, que chama HandleClick1, dispara uma re-renderização.
  • Selecionar o segundo botão, que chama HandleClick2, não aciona um rerender.
  • Selecionar o terceiro botão, que chama HandleClick3, não dispara um rerender 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 IHandleEvent interface, aproveitar as outras práticas recomendadas descritas neste artigo também pode ajudar a reduzir renderizações indesejadas depois que os eventos são tratados. Por exemplo, a substituição ShouldRender em componentes filho do componente de destino pode ser usada para controlar a rerendering.

Evite recriar delegados para muitos elementos ou componentes repetidos

BlazorA recriação de delegados de expressão lambda para elementos ou componentes dentro de 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 atribui um delegado ao evento @onclick , o que é bom se não houver 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 ruim do usuário. 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 de @onclick cada botão a um Action. A abordagem a seguir não requer Blazor reconstruir todos os delegados de botão sempre que os botões forem 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 => { };
    }
}