Ajuste de desempenho do Blazor - Boas práticas e casos de uso avançados

Imagem de capa Ajuste de desempenho do Blazor - Boas práticas e casos de uso avançados

Ajuste de desempenho do Blazor - Boas práticas e casos de uso avançados

Índice

A postagem do blog mostrará o seguinte mecanismo:

  • Virtualizar
  • @Chave
  • Blazor WebAssembly: assemblies de carregamento lento
  • Blazor WebAssembly: Minimize o tamanho do download do aplicativo
  • Blazor WebAssembly: compilação AOT
  • Pré-renderize seu aplicativo Blazor
  • CascadingValue é fixo
  • Evite recriar delegados em loops
  • Caso de uso avançado: substituir ShouldRender
  • Caso de uso avançado: SetParametersAsync manualmente
  • Caso de uso avançado: número de sequência do RenderTreeBuilder
  • Caso de uso avançado: Blazor WebAssembly: compactar arquivos em soluções de hospedagem estática.

Virtualizar

O componente Virtualize permite que o Blazor renderize apenas componentes que estão visíveis no momento ou que serão visíveis imediatamente quando o usuário estiver rolando. A principal vantagem é que são alocados e renderizados apenas os recursos que estão no campo de visualização do usuário. Portanto, se você tiver muitos componentes ou se os componentes forem "pesados", você distribuirá essa carga de trabalho ao longo do tempo. Outra vantagem disso é que sua lógica não necessariamente sabe que foi inicializada como "preguiçosa". Há uma desvantagem: se você tiver usuários com deficiência visual que estão transmitindo no leitor de tela e outras tecnologias como essa, eles terão uma vida mais difícil. Existem algumas mitigações para isso, mas recursos como esse ou rolagem sem fim não são a melhor opção quando se trata de acessibilidade.

Aqui um pequeno exemplo:

<div  style="height:500px;overflow-y:scroll">
     <Virtualize  Items="@ImageUrls">
         <ImageCard ImageUrl="@context"></ImageCard>
      </Virtualize>
</div>

@chave

Este pequeno ajudante pode nos proteger precioso tempo de renderização / largura de banda em loops. Por quê? Normalmente, o mecanismo de comparação do Blazor usa o índice do elemento para comparar. Mas se você inserir um elemento no topo (ou não no final), isso não funcionará bem.

Você pode ver esse comportamento claramente nas ferramentas de desenvolvedor do seu navegador. Um clique em "Adicionar" e toda a lista é renderizada novamente. Agora, quando adicionamos a diretiva @chave, podemos dar a Blazor uma dica de como acompanhar nossa coleção. E tada em nosso segundo exemplo podemos ver que apenas "anexamos" os novos elementos < li> no topo ao invés de renderizar novamente a lista inteira.

O uso também é muito simples:

<ul>
   @foreach (var item in items)
   {
      @* With the @key attribute we define what makes the element unique
         Based on this Blazor can determine whether or not it has to re-render
         the element
       *@
       <li @key="item">@item</li>
   }
</ul>

Lembre-se de que seu valor @chave deve ser exclusivo (como uma PRIMARY KEY no SQL). Se você tiver vários com a mesma identidade, o Blazor lançará uma exceção.

Agora @chave vem com um pouco de penalidade de desempenho por si só. Mas em muitos cenários, você ganha tempo com menos renderização e, se usar o Blazor Server, precisará enviar menos Diff dentro de sua conexão SignalR.

Se você quiser saber mais sobre @chave, confira a documentação da Microsoft

Blazor WebAssembly: assemblies de carregamento lento

O carregamento lento significa que estamos inicializando nosso objeto apenas na primeira vez que alguém o solicita. E podemos fazer o mesmo com montagens no Blazor. O tempo de execução do cliente Blazor inicializa uma conexão na primeira solicitação e baixa todo o código .NET mais a lógica do aplicativo para o cliente. Podemos "adiar" alguns desses assemblies dizendo ao Blazor que queremos carregá-los posteriormente.

Em nosso csproj temos que dizer ao Blazor quais assemblies queremos carregar mais tarde:

<ItemGroup>
   <BlazorWebAssemblyLazyLoad  Include="MyCompany.Services.dll" />
</ItemGroup>

É importante que a extensão do arquivo faça parte do identificador. Portanto, não pule o .dll no final. Você pode ter várias propriedades BlazorWebAssemblyLazyLoad. Um para cada montagem que você deseja adiar o carregamento. Todas as dependências do seu assembly também devem ser carregadas com preguiça para obter o menor pacote inicial.

Agora isso não é tudo. Acabamos de dizer ao Blazor para adiar o carregamento desses assemblies, mas também temos que dizer ao Blazor quando realmente carregá-los. Isso é feito através do LazyAssemblyLoader. Mais especificamente, temos que estender nossa classe App. Temos que dizer ao Blazor em qual rota queremos carregar quais assemblies.

@using System.Reflection 
@using Microsoft.AspNetCore.Components.Routing 
@using Microsoft.AspNetCore.Components.WebAssembly.Services @using Microsoft.Extensions.Logging 
@inject LazyAssemblyLoader AssemblyLoader 
@inject ILogger<App> Logger

<Router AppAssembly="@typeof(Program).Assembly"
    AdditionalAssemblies="@lazyLoadedAssemblies"
    OnNavigateAsync="@OnNavigateAsync">
    ...
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new();
    
    private  async Task OnNavigateAsync(NavigationContext args)
    {
       try
         {
            if (args.Path == "{PATH}")
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { {LIST OF ASSEMBLIES} });
                lazyLoadedAssemblies.AddRange(assemblies);
             }
          }
          catch (Exception ex)
          {
              Logger.LogError("Error: {Message}", ex.Message);
          }
    }
}

Este exemplo é retirado diretamente daqui

A desvantagem óbvia dessa abordagem é que aumentamos nossa complexidade. Portanto, isso só faz sentido se você tiver grandes montagens que não são necessárias no carregamento inicial.

Blazor WebAssembly: Minimize o tamanho do download do aplicativo

Se você usa algumas bibliotecas, é muito normal que você não use todas as pequenas funções públicas expostas. Essa circunstância ajuda o Blazor a remover esses símbolos não utilizados ao publicar seu aplicativo. Como esse é o padrão para as versões mais recentes, você não precisará realizar nenhuma ação adicional. Você pode ver o resultado se apenas criar seu aplicativo versus publicá-lo: dotnet publish -c Release

No entanto, você pode ajudar o Blazor com algumas alternâncias:

<PropertyGroup>
        <BlazorEnableTimeZoneSupport>false</BlazorEnableTimeZoneSupport>
<PropertyGroup>

Isso removerá o suporte ao fuso horário do seu aplicativo Blazor. Você pode verificar a saída _framework/wasm/dotnet.timezones.dat após aparar e publicar. Deve ser menor no tamanho do arquivo.

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

Isso removerá as informações de agrupamento das APIs. Isso afetaria, por exemplo: StringComparison.InvariantCultureIgnoreCase. Se você não precisar de tais comparações, poderá removê-las e salvar alguns kB.

Blazor WebAssembly: compilação AOT

AOT compilará seu código C# diretamente no WebAssembly. Normalmente você terá uma camada intermediária: código IL. Para habilitar a compilação AOT, você deve adicionar o seguinte ao seu csproj:

<PropertyGroup>
    <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>

A principal vantagem disso é que seu código é executado muito mais rápido do que o equivalente de IL. Mas isso só faz sentido se você tiver tarefas com uso intensivo de CPU em seu aplicativo Blazor. A desvantagem dessa abordagem é que seu assembly .NET com AOT, que é enviado pela rede para seu cliente, é maior que o IL equivalente. Na verdade, pode ser até 2x maior. Agora, em vez de 2 MB, seu cliente precisa baixar 4 MB para o carregamento inicial. Portanto, meça primeiro se faz sentido no seu cenário. Se você quiser saber mais, dê uma olhada na documentação da Microsoft. Há uma mitigação para este hit de download inicial: Pré-renderização.

Pré-renderize seu aplicativo Blazor

"A pré-renderização significa apenas que aceitamos a primeira solicitação e fingimos ser como um site ASP.NET Core normal. Nós apenas renderizamos tudo e retornaremos o conteúdo (html estático) para o cliente (ao lado das outras coisas descritas acima)."

Para que isso funcione, temos que entregar nosso aplicativo Blazor por meio de um site ASP.NET Core. Se apenas hospedarmos, por exemplo, nosso aplicativo Blazor Client em um CDN, isso não funcionará.

Para habilitar a pré-renderização, basta estender seu _Layout.cshtml:

<component type="typeof(App)" render-mode="WebAssemblyPrerendered" />

Para o lado do cliente/WebAssembly

<component type="typeof(App)" render-mode="ServerPrerendered" />

Para o lado do servidor Blazor.`

O valor em cascata é fixo

CascadingValues são usados para, como o nome indica, a cascata de um parâmetro para baixo na hierarquia. O problema com isso é que cada componente filho ouvirá as alterações no valor original. Isso pode ficar caro. Caso seu componente não dependa de atualizações, você pode fixar/corrigir esse valor. Para isso você pode usar o parâmetro IsFixed.

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

Evite recriar delegados em loops

Imagine o seguinte código:

@code {
  @foreach(var item in items)
  {
    <button @onclick="((eventArgs) => DoSomething(eventArgs)">
  }
}

Obviamente, poderíamos fazer melhor, mas isso é apenas para fins de explicação. Agora imagine que os botões precisam ser renderizados novamente (por exemplo, devido a alterações nos itens). Como são lambdas, eles também precisam ser recriados. Para uma quantidade menor de controles, isso é obviamente bom, mas para listas maiores que podem ficar caras e diminuir seu desempenho. A solução óbvia no nosso caso:

@code {
   @foreach(var item in items)
   {
     <button @onclick="DoSomething">
   }
}

Agora, quando os itens mudam, não precisamos criar novos lambdas o tempo todo.

Se você quiser ver um caso mais avançado, confira a documentação da Microsoft.

Isenção de responsabilidade:

Como esses são casos de uso avançados, você não deve apenas implementá-los, pois isso pode economizar alguns por cento do tempo de renderização. As próximas abordagens definitivamente aumentarão a complexidade do seu componente Blazor. Considere fazer benchmarks primeiro e verifique se a solução fornecida atende às suas necessidades!

Caso de uso avançado: substituir ShouldRender

Cada componente Blazor deriva de ComponentBase. ComponentBase oferece a seguinte função protegida: ShouldRender. Em sua implementação normal, sempre retorna verdadeiro. Toda vez que o Blazor entra em um ciclo de renderização (por exemplo, porque um dos valores dos parâmetros foi alterado), o Blazor verifica via ShouldRender se deve invocar o pipeline de renderização. Aqui está uma boa melhoria potencial. Se soubermos que nosso componente não deve ser atualizado, podemos salvar o ciclo de renderização.

A documentação da Microsoft tem um bom exemplo para isso:

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

Só queremos renderizar quando o voo de entrada ou de saída mudou. Se forem iguais, podemos salvar a re-renderização.

Esteja ciente de que aumentamos a complexidade do nosso aplicativo para obter talvez um desempenho ligeiramente melhor. Em média, o Blazor pode detectar alterações por conta própria e pode lidar com essas re-renderizações de forma inteligente.

Caso de uso avançado: SetParametersAsync manualmente

Imagine que você tem um componente com alguns [Parameter]s

... Some HTML here ...
@code {
   [Parameter]
   public  string FirstName { get; set; }
   
   [Parameter]
   public  string LastName{ get; set; }
}

Agora, parte do tempo ao criar seu componente é usado mapeando os parâmetros de entrada de entrada para os componentes [Parameter]. Isso é feito por meio de reflexão. Em casos normais, isso não é grande coisa, mas se você tiver que renderizar centenas ou milhares de componentes com muitos parâmetros, a sobrecarga será perceptível. Para isso você pode implementar a lógica do mapeamento por conta própria. Para isso, você precisa substituir SetParametersAsync.

@code {
   [Parameter]
   public  string FirstName { get; set; }
   
   [Parameter] 
   public  string LastName { get; set; }
   
   public override Task SetParametersAsync(ParameterView parameters) 
   {
     foreach (var parameter in parameters) 
     {
       switch (parameter.Name) 
       {
         case  nameof(FirstName):
           FirstName = (string)parameter.Value;
           break;
         case  nameof(LastName):
           LastName= (string)parameter.Value;
           break;
         default:
           throw  new ArgumentException($"Couldn't map {parameter.Name}");
           break;
       }
       
       // This will tell Blazor that it doesn‘t have to
       // take care of parameters anymore
       return  base.SetParametersAsync(ParameterView.Empty);
    }
}

Se você quiser saber mais sobre, leia a Documentação oficial da Microsoft.

Caso de uso avançado: número de sequência do RenderTreeBuilder

Você pode ter coragem de usar o RenderTreeBuilder para criar sua própria marcação HTML. Para construir esses componentes, você deve fornecer um número de sequência. Esse número de sequência é muito importante para o Blazor detectar alterações (consulte @chave no topo desta postagem do blog, onde expliquei também que uma ordenação diferente pode causar renderização desnecessária).

@CreateComponent() 
@code {
    // Some code here ...
       private RenderFragment CreateComponent() => builder => 
       {
          var seqNumber = 0; 
          if (someFlag) 
          {
               builder.AddContent(seqNumber++, "First");
          }
          
          builder.AddContent(seqNumber++, "Second");
       }
}

Agora parece razoável pegar um seqNumber que aumenta dinamicamente. Mas imagine o seguinte cenário.

Na renderização inicial someFlag é verdade. Então temos algo assim:

Sequência 0 tem dados "Primeiro" Sequência 1 tem dados "Segundo"

Agora someFlag é falso. Agora vamos para nossa segunda renderização: Sequence 0 has Data "Second"

Hum. Isso é um problema porque o algoritmo diff vê duas mudanças:

  • Mudança de conteúdo para Sequência 0
  • Exclusão da Sequência 1

Em vez de alterar apenas um elemento, dois devem ser alterados porque introduzimos o número de sequência aumentado dinamicamente. Resultado: Use sempre o mesmo número para a mesma sequência. No nosso caso isso é simples.

private RenderFragment CreateComponent() => builder =>
{
    if (someFlag)
    {
         builder.AddContent(0, "First");
    }
    
    builder.AddContent(1, "Second");
}

Agora tudo está fixo independente da condição someFlag.

Caso de uso avançado: Blazor WebAssembly: compactar arquivos em soluções de hospedagem estática

Há casos em que seu aplicativo Blazor WebAssembly não é servido via ASP.NET Core, onde é super fácil habilitar a compactação. Se você quiser saber mais sobre isso, dê uma olhada aqui.

Há casos em que você deseja servir seu aplicativo de hosters estáticos. Um bom exemplo seria uma página do github. Aqui você não tem compressão. Você pode conseguir isso adicionando manualmente Brotli e sobrecarregando como a carga inicial do Blazor WebAssembly é tratada. Para todos os detalhes específicos, acesse a documentação oficial da Microsoft.

Conclusão

Espero poder fornecer uma boa visão geral sobre algumas técnicas para otimizar o desempenho do seu aplicativo Blazor. Se houver algo essencial faltando, por favor me avise e eu corrigirei esta postagem do blog (claro, com créditos para você).