Lazy load assemblies in ASP.NET Core Blazor WebAssembly

By Safia Abdalla and Luke Latham

Blazor WebAssembly app startup performance can be improved by deferring the loading of some application assemblies until they are required, which is called lazy loading. For example, assemblies that are only used to render a single component can be set up to load only if the user navigates to that component. After loading, the assemblies are cached client-side and are available for all future navigations.

Blazor's lazy loading feature allows you to mark app assemblies for lazy loading, which loads the assemblies during runtime when the user navigates to a particular route. The feature consists of changes to the project file and changes to the application's router.

Note

Assembly lazy loading doesn't benefit Blazor Server apps because assemblies aren't downloaded to the client in a Blazor Server app.

Project file

Mark assemblies for lazy loading in the app's project file (.csproj) using the BlazorWebAssemblyLazyLoad item. Use the assembly name with the .dll extension. The Blazor framework prevents the assemblies specified by this item group from loading at app launch. The following example marks a large custom assembly (GrantImaharaRobotControls.dll) for lazy loading. If an assembly that's marked for lazy loading has dependencies, they must also be marked for lazy loading in the project file.

<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="GrantImaharaRobotControls.dll" />
</ItemGroup>

Router component

Blazor's Router component designates which assemblies Blazor searches for routable components. The Router component is also responsible for rendering the component for the route where the user navigates. The Router component supports an OnNavigateAsync feature that can be used in conjunction with lazy loading.

In the app's Router component (App.razor):

  • Add an OnNavigateAsync callback. The OnNavigateAsync handler is invoked when the user:
    • Visits a route for the first time by navigating to it directly from their browser.
    • Navigates to a new route using a link or a NavigationManager.NavigateTo invocation.
  • If lazy-loaded assemblies contain routable components, add a List<Assembly> (for example, named lazyLoadedAssemblies) to the component. The assemblies are passed back to the AdditionalAssemblies collection in case the assemblies contain routable components. The framework searches the assemblies for routes and updates the route collection if any new routes are found.
@using System.Reflection

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

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

    private async Task OnNavigateAsync(NavigationContext args)
    {
    }
}

If the OnNavigateAsync callback throws an unhandled exception, the Blazor error UI is invoked.

Assembly load logic in OnNavigateAsync

OnNavigateAsync has a NavigationContext parameter that provides information about the current asynchronous navigation event, including the target path (Path) and the cancellation token (CancellationToken):

  • The Path property is the user's destination path relative to the app's base path, such as /robot.
  • The CancellationToken can be used to observe the cancellation of the asynchronous task. OnNavigateAsync automatically cancels the currently running navigation task when the user navigates to a different page.

Inside OnNavigateAsync, implement logic to determine the assemblies to load. Options include:

  • Conditional checks inside the OnNavigateAsync method.
  • A lookup table that maps routes to assembly names, either injected into the component or implemented within the @code block.

LazyAssemblyLoader is a framework-provided singleton service for loading assemblies. Inject LazyAssemblyLoader into the Router component:

...
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@inject LazyAssemblyLoader assemblyLoader

...

The LazyAssemblyLoader provides the LoadAssembliesAsync method that:

  • Uses JS interop to fetch assemblies via a network call.
  • Loads assemblies into the runtime executing on WebAssembly in the browser.

The framework's lazy loading implementation supports lazy loading with prerendering in a hosted Blazor solution. During prerendering, all assemblies, including those marked for lazy loading, are assumed to be loaded. Manually register LazyAssemblyLoader in the Server project's Startup.ConfigureServices method (Startup.cs):

services.AddScoped<LazyAssemblyLoader>();

User interaction with <Navigating> content

While loading assemblies, which can take several seconds, the Router component can indicate to the user that a page transition is occurring:

...
@using Microsoft.AspNetCore.Components.Routing
...

<Router ...>
    <Navigating>
        <div style="...">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
</Router>

...

Handle cancellations in OnNavigateAsync

The NavigationContext object passed to the OnNavigateAsync callback contains a CancellationToken that's set when a new navigation event occurs. The OnNavigateAsync callback must throw when this cancellation token is set to avoid continuing to run the OnNavigateAsync callback on a outdated navigation.

If a user navigates to Route A and then immediately to Route B, the app shouldn't continue running the OnNavigateAsync callback for Route A:

@inject HttpClient Http
@inject ProductCatalog Products

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

@code {
    private async Task OnNavigateAsync(NavigationContext context)
    {
        if (context.Path == "/about") 
        {
            var stats = new Stats = { Page = "/about" };
            await Http.PostAsJsonAsync("api/visited", stats, context.CancellationToken);
        }
        else if (context.Path == "/store")
        {
            var productIds = [345, 789, 135, 689];

            foreach (var productId in productIds) 
            {
                context.CancellationToken.ThrowIfCancellationRequested();
                Products.Prefetch(productId);
            }
        }
    }
}

Note

Not throwing if the cancellation token in NavigationContext is canceled can result in unintended behavior, such as rendering a component from a previous navigation.

OnNavigateAsync events and renamed assembly files

The resource loader relies on the assembly names that are defined in the blazor.boot.json file. If assemblies are renamed, the assembly names used in OnNavigateAsync methods and the assembly names in the blazor.boot.json file are out of sync.

To rectify this:

  • Check to see if the app is running in the Production environment when determining which assembly names to use.
  • Store the renamed assembly names in a separate file and read from that file to determine what assembly name to use in the LazyLoadAssemblyService and OnNavigateAsync methods.

Complete example

The following complete Router component demonstrates loading the GrantImaharaRobotControls.dll assembly when the user navigates to /robot. During page transitions, a styled message is displayed to the user.

@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@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.EndsWith("/robot"))
            {
                var assemblies = await assemblyLoader.LoadAssembliesAsync(
                    new List<string>() { "GrantImaharaRobotControls.dll" });
                lazyLoadedAssemblies.AddRange(assemblies);
            }
        }
        catch (Exception ex)
        {
            ...
        }
    }
}

Troubleshoot

  • If unexpected rendering occurs (for example, a component from a previous navigation is rendered), confirm that the code throws if the cancellation token is set.
  • If assemblies are still loaded at application start, check that the assembly is marked as lazy loaded in the project file.

Additional resources