Assemblies de carregamento lento no Blazor WebAssembly do ASP.NET Core

Observação

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

Importante

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

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

O desempenho de inicialização do aplicativo Blazor WebAssembly pode ser aprimorado com a espera do carregamento de assemblies de aplicativo criados pelo desenvolvedor até que os assemblies sejam necessários, o que é chamado de carregamento lento.

As seções iniciais deste artigo abrangem a configuração do aplicativo. Para obter uma demonstração de trabalho, consulte a seção Exemplo completo no final deste artigo.

Este artigo é válido somente para aplicativos Blazor WebAssembly. O carregamento lento do assembly não beneficia aplicativos do lado do servidor porque os aplicativos renderizados pelo servidor não baixam assemblies para o cliente.

O carregamento lento não deve ser usado para assemblies de runtime principais, que podem ser cortados no momento da publicação e ficar indisponíveis no cliente quando o aplicativo é carregado.

Espaço reservado para extensão de nome de arquivo ({FILE EXTENSION}) para arquivos do assembly

Os arquivos do assembly usam o formato de empacotamento Webcil para assemblies do .NET com uma extensão de nome de arquivo .wasm.

Ao longo do artigo, o espaço reservado {FILE EXTENSION} representa "wasm".

Os arquivos do assembly são baseados em Bibliotecas de Link Dinâmico (DLLs) com uma extensão de nome de arquivo .dll.

Ao longo do artigo, o espaço reservado {FILE EXTENSION} representa "dll".

Configuração do arquivo de projeto

Marque assemblies para carregamento lento no arquivo de projeto do aplicativo (.csproj) usando o item BlazorWebAssemblyLazyLoad. Use o nome do assembly com a extensão de nome de arquivo. A estrutura Blazor impede que o assembly seja carregado na inicialização do aplicativo.

<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="{ASSEMBLY NAME}.{FILE EXTENSION}" />
</ItemGroup>

O espaço reservado {ASSEMBLY NAME} é o nome do assembly, e o espaço reservado {FILE EXTENSION} é a extensão de nome de arquivo. A extensão de arquivo é obrigatória.

Inclua um item BlazorWebAssemblyLazyLoad para cada assembly. Se um assembly tiver dependências, inclua uma entrada BlazorWebAssemblyLazyLoad para cada dependência.

Configuração do componenteRouter

A estrutura Blazor registra automaticamente um serviço singleton para assemblies de carregamento lento em aplicativos Blazor WebAssembly do lado do cliente, LazyAssemblyLoader. O método LazyAssemblyLoader.LoadAssembliesAsync:

  • Usa a interoperabilidade JS para buscar assemblies por meio de uma chamada de rede.
  • Carrega assemblies no runtime em execução no WebAssembly no navegador.

Observação

As diretrizes para soluçõesBlazor WebAssemblyhospedadas são abordadas na seção Assemblies de carga lenta em uma seção de solução Blazor WebAssembly hospedada.

O componente Router do Blazor designa os assemblies que pesquisam Blazor por componentes roteáveis e também é responsável por renderizar o componente para a rota em que o usuário navega. O método de OnNavigateAsync do componente Router é usado em conjunto com o carregamento lento para carregar os assemblies corretos para pontos de extremidade que um usuário solicita.

A lógica é implementada no interior de OnNavigateAsync para determinar os assemblies a serem carregados com LazyAssemblyLoader. As opções de como estruturar a lógica incluem:

  • Verificações condicionais no método OnNavigateAsync.
  • Uma tabela de pesquisa que mapeia rotas para nomes de assembly, injetadas no componente ou implementadas no bloco @code.

No exemplo a seguir:

  • O namespace para Microsoft.AspNetCore.Components.WebAssembly.Services é especificado.
  • O serviço LazyAssemblyLoader é injetado (AssemblyLoader).
  • O espaço reservado {PATH} é o caminho onde a lista de assemblies deve ser carregada. O exemplo usa uma verificação condicional para um único caminho que carrega um único conjunto de assemblies.
  • O espaço reservado {LIST OF ASSEMBLIES} é a lista separada por vírgulas de cadeias de caracteres de nome de arquivo de assembly, incluindo suas extensões de arquivo (por exemplo, "Assembly1.{FILE EXTENSION}", "Assembly2.{FILE EXTENSION}").

App.razor:

@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(App).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}
@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" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

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

Observação

O exemplo anterior não mostra o conteúdo da marcação Razor do componente Router (...). Para obter uma demonstração com código completo, consulte a seção Exemplo completo deste artigo.

Observação

Com a versão do ASP.NET Core 5.0.1 e para qualquer lançamento adicional do 5.x, o componente Router inclui o parâmetro PreferExactMatches definido como @true. Para obter mais informações, consulte Migrar do ASP.NET Core 3.1 para o 5.0.

Assemblies que incluem componentes roteáveis

Quando a lista de assemblies inclui componentes roteáveis, a lista de assembly de um determinado caminho é passada para a coleção AdditionalAssemblies do componente Router.

No exemplo a seguir:

  • A Lista<Assembly> em lazyLoadedAssemblies passa a lista de assembly para AdditionalAssemblies. A estrutura pesquisa os assemblies em busca de rotas e atualiza a coleção de rotas se novas rotas forem encontradas. Para acessar o tipo Assembly, o namespace para System.Reflection é incluído na parte superior do arquivo App.razor.
  • O espaço reservado {PATH} é o caminho onde a lista de assemblies deve ser carregada. O exemplo usa uma verificação condicional para um único caminho que carrega um único conjunto de assemblies.
  • O espaço reservado {LIST OF ASSEMBLIES} é a lista separada por vírgulas de cadeias de caracteres de nome de arquivo de assembly, incluindo suas extensões de arquivo (por exemplo, "Assembly1.{FILE EXTENSION}", "Assembly2.{FILE EXTENSION}").

App.razor:

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

<Router AppAssembly="typeof(App).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);
           }
    }
}
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

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

@code {
    private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();

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

Observação

O exemplo anterior não mostra o conteúdo da marcação Razor do componente Router (...). Para obter uma demonstração com código completo, consulte a seção Exemplo completo deste artigo.

Observação

Com a versão do ASP.NET Core 5.0.1 e para qualquer lançamento adicional do 5.x, o componente Router inclui o parâmetro PreferExactMatches definido como @true. Para obter mais informações, consulte Migrar do ASP.NET Core 3.1 para o 5.0.

Para obter mais informações, confira Roteamento e navegação do Blazor no ASP.NET Core.

Interação do usuário com conteúdo <Navigating>

Ao carregar assemblies, o que pode demorar vários segundos, o componente Router pode indicar ao usuário que uma transição de página está ocorrendo com a propriedade Navigating do roteador.

Para obter mais informações, confira Roteamento e navegação do Blazor no ASP.NET Core.

Manipular cancelamentos em OnNavigateAsync

O objeto NavigationContext transmitido ao retorno de chamada OnNavigateAsync contém um CancellationToken definido quando ocorre um novo evento de navegação. O retorno de chamada OnNavigateAsync precisa lançar quando esse token de cancelamento é definido a fim de evitar continuar a executar o retorno de chamada OnNavigateAsync em uma navegação desatualizada.

Para obter mais informações, confira Roteamento e navegação do Blazor no ASP.NET Core.

Eventos OnNavigateAsync e arquivos de assembly renomeados

O carregador de recursos depende dos nomes de assembly definidos no arquivo blazor.boot.json. Se os assemblies forem renomeados, os nomes de assembly usados em um retorno de chamada OnNavigateAsync e os nomes de assembly no arquivo blazor.boot.json estarão fora de sincronia.

Para corrigir isso:

  • Verifique se o aplicativo está em execução no ambiente Production ao determinar quais nomes de assembly devem ser usados.
  • Armazene os nomes de assembly renomeados em um arquivo separado e leia desse arquivo para determinar qual nome de assembly deve ser usado com o serviço LazyAssemblyLoader e o retorno de chamada OnNavigateAsync.

Assemblies de carregamento lentos em uma solução Blazor WebAssembly hospedada

A implementação de carregamento lento da estrutura dá suporte ao carregamento lento com pré-geração em uma Blazor WebAssemblysolução hospedada. Durante a pré-geração, todos os assemblies, incluindo aqueles marcados para carregamento lento, são considerados carregados. Registre manualmente o serviço LazyAssemblyLoader no projeto Server.

Na parte superior do arquivo Program.cs do projeto Server, adicione o namespace para Microsoft.AspNetCore.Components.WebAssembly.Services:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

No Program.cs do projeto Server, registre o serviço:

builder.Services.AddScoped<LazyAssemblyLoader>();

Na parte superior do arquivo Startup.cs do projeto Server, adicione o namespace para Microsoft.AspNetCore.Components.WebAssembly.Services:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

No Startup.ConfigureServices (Startup.cs) do projeto Server, registre o serviço:

services.AddScoped<LazyAssemblyLoader>();

Exemplo completo

A demonstração nesta seção:

  • Cria um assembly de controles de robô (GrantImaharaRobotControls.{FILE EXTENSION}) como uma RCL (biblioteca de classes) Razor que inclui um componente Robot (Robot.razor com um modelo de rota de /robot).
  • Carrega o assembly da RCL de modo preguiçoso para renderizar seu componente Robot quando a URL /robot é solicitada pelo usuário.

Crie um aplicativo autônomo Blazor WebAssembly para demonstrar o carregamento lento do assembly de uma biblioteca de classes Razor. Dê ao projeto o nome de LazyLoadTest.

Adicione um projeto de biblioteca de classes do ASP.NET Core à solução:

  • Visual Studio: clique com o botão direito do mouse no arquivo de solução no Gerenciador de Soluções e selecione Adicionar>Novo projeto. Na caixa de diálogo de novos tipos de projeto, selecione Biblioteca de Classes Razor. Dê ao projeto o nome de GrantImaharaRobotControls. Não marque a caixa de seleção Páginas de suporte e exibição.
  • CLI do Visual Studio Code/.NET: execute dotnet new razorclasslib -o GrantImaharaRobotControls de um prompt de comando. A opção -o|--output cria uma pasta e nomeia o projeto GrantImaharaRobotControls.

O componente de exemplo apresentado posteriormente nesta seção usa um formulário Blazor. No projeto da RCL, adicione o pacote Microsoft.AspNetCore.Components.Forms ao projeto.

Observação

Para obter diretrizes sobre como adicionar pacotes a aplicativos .NET, consulte os artigos em Instalar e gerenciar pacotes no Fluxo de trabalho de consumo de pacotes (documentação do NuGet). Confirme as versões corretas de pacote em NuGet.org.

Crie uma classe HandGesture na RCL com um método ThumbUp que hipoteticamente faz com que um robô execute um gesto de polegar para cima. O método aceita um argumento para o eixo, Left ou Right, como um enum. O método retorna true com êxito.

HandGesture.cs:

using Microsoft.Extensions.Logging;

namespace GrantImaharaRobotControls;

public static class HandGesture
{
    public static bool ThumbUp(Axis axis, ILogger logger)
    {
        logger.LogInformation("Thumb up gesture. Axis: {Axis}", axis);

        // Code to make robot perform gesture

        return true;
    }
}

public enum Axis { Left, Right }
using Microsoft.Extensions.Logging;

namespace GrantImaharaRobotControls
{
    public static class HandGesture
    {
        public static bool ThumbUp(Axis axis, ILogger logger)
        {
            logger.LogInformation("Thumb up gesture. Axis: {Axis}", axis);

            // Code to make robot perform gesture

            return true;
        }
    }

    public enum Axis { Left, Right }
}

Adicione o componente a seguir à raiz do projeto da RCL. O componente permite que o usuário envie uma solicitação de gesto de polegar para cima à esquerda ou direita.

Robot.razor:

@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm FormName="RobotForm" Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in Enum.GetValues<Axis>())
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

    <button type="submit">Submit</button>
</EditForm>

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new() { AxisSelection = Axis.Left };
    private string? message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}
@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in Enum.GetValues<Axis>())
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

    <button type="submit">Submit</button>
</EditForm>

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new() { AxisSelection = Axis.Left };
    private string? message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}
@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in (Axis[])Enum
            .GetValues(typeof(Axis)))
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

    <button type="submit">Submit</button>
</EditForm>

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new RobotModel() { AxisSelection = Axis.Left };
    private string message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}

No projeto LazyLoadTest, crie uma referência de projeto para a RCL GrantImaharaRobotControls:

  • Visual Studio: clique com o botão direito do mouse no projeto LazyLoadTest e selecione Adicionar>Referência do projeto para adicionar uma referência de projeto para a RCL GrantImaharaRobotControls.
  • CLI do Visual Studio Code/.NET: execute dotnet add reference {PATH} em um shell de comando da pasta do projeto. O espaço reservado {PATH} é o caminho para o projeto da RCL.

Especifique o assembly da RCL para carregamento lento no arquivo de projeto do aplicativo LazyLoadTest (.csproj):

<ItemGroup>
    <BlazorWebAssemblyLazyLoad Include="GrantImaharaRobotControls.{FILE EXTENSION}" />
</ItemGroup>

O componente Router a seguir demonstra o carregamento do assembly GrantImaharaRobotControls.{FILE EXTENSION} quando o usuário navega para /robot. Substitua o componente padrão App do aplicativo pelo componente a seguir App.

Durante as transições de página, uma mensagem com estilo é exibida para o usuário com o elemento <Navigating>. Para obter mais informações, consulte a seção Interação do usuário com conteúdo <Navigating>.

O assembly é atribuído a AdditionalAssemblies, o que resulta no roteador pesquisando componentes roteáveis no assembly, onde ele encontra o componente Robot. A rota do componente Robot é adicionada à coleção de rotas do aplicativo. Para obter mais informações, consulte o artigo Roteamento e navegação Blazor do ASP.NET Core e a seção Assemblies que incluem componentes roteáveis deste artigo.

App.razor:

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

<Router AppAssembly="typeof(App).Assembly"
        AdditionalAssemblies="lazyLoadedAssemblies" 
        OnNavigateAsync="OnNavigateAsync">
    <Navigating>
        <div style="padding:20px;background-color:blue;color:white">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if (args.Path == "robot")
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" });
                lazyLoadedAssemblies.AddRange(assemblies);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(Program).Assembly"
        AdditionalAssemblies="lazyLoadedAssemblies" 
        OnNavigateAsync="OnNavigateAsync">
    <Navigating>
        <div style="padding:20px;background-color:blue;color:white">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if (args.Path == "robot")
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" });
                lazyLoadedAssemblies.AddRange(assemblies);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}

Compile e execute o aplicativo.

Quando o componente Robot da RCL é solicitado em /robot, o assembly GrantImaharaRobotControls.{FILE EXTENSION} é carregado e o componente Robot é renderizado. Você pode inspecionar o carregamento do assembly na guia Rede das ferramentas de desenvolvedor do navegador.

Solucionar problemas

  • Se ocorrer uma renderização inesperada, como renderizar um componente de uma navegação anterior, confirme se o código será gerado se o token de cancelamento estiver definido.
  • Se os assemblies configurados para carregamento lento forem carregados inesperadamente no início do aplicativo, verifique se o assembly está marcado para carregamento lento no arquivo de projeto.

Observação

Existe um problema conhecido para carregar tipos de assembly carregado lentamente. Para obter mais informações, consulte Blazor WebAssembly lazy loading assemblies not working when using @ref attribute in the component (dotnet/aspnetcore #29342).

Recursos adicionais