Many applications have functionality that is used infrequently.
So why should your application load all the code for this feature if it is not being used?
In that case you should use lazy loading.
With lazy loading you can have your Blazor application load the code when it is actually needed.
And enabling lazy loading is not hard at all!
Let us have a look at an example. The sources for this example is on my Github.
This single page application has two pages which are used infrequently.
First step
The first thing you need to do is to move the components you want to lazy load to a razor component library. For the demo I have created a new Razor Component Library called LazyLoadingComponents
, and I moved the LazyDog.razor
and FetchData.razor
component into it.
Once you have moved your components, test everything to see if it still works.
The Blazor runtime will still load this assembly on startup. Let us fix this.
Second step
Next step is to tell the compiler to use lazy loading for your library project.
In your main project add the following ItemGroup
, this will tell the runtime
not to load the LazyLoadedLibrary.dll
file when it starts:
<!-- List of lazy loaded assemblies -->
<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="LazyLoadedLibrary.dll" />
<BlazorWebAssemblyLazyLoad Include="ServiceProxies.dll" />
</ItemGroup>
This tells the runtime not to load these assemblies when the application starts.
If this assembly has dependencies you can also mark them for lazy loading, here for example
that would be the ServiceProxies
class library.
Do not run the application. It will not function as expected.
There is one more step to do:
Third step
When the user navigates to your lazy loaded component you should load the assembly dynamically.
Who handles the navigation in your application? The Router
of course!
Open App.razor
(or wherever your router is).
Use dependency injection to get a reference to the LazyAssemblyLoader
:
@inject Microsoft.AspNetCore.Components.WebAssembly.Services.LazyAssemblyLoader lazyAssemblyLoader;
Implement the OnNavigateAsync
event on the Router
component:
<Router AppAssembly="@typeof(Program).Assembly"
OnNavigateAsync="@OnNavigateAsync">
The router triggers this event whenever the user wants to navigate to another page, or when
you call the NavigateTo
method on the NavigationManager
.
Implement this event to check the route, and if it uses a lazy loaded component, load the assembly. This example will load a razor component library, and a simple class library:
if (ctxt.Path == "lazy-loaded-component" || ctxt.Path == "fetchdata")
{
var assemblies = await lazyAssemblyLoader.LoadAssembliesAsync(new[] { "LazyLoadedLibrary.dll", "ServiceProxies.dll" });
}
The router will inspect all Blazor components for your route at startup. But we just added a bunch of new components at runtime, so the router will not know about them.
We need to tell the router about this, by giving it a collection of assemblies to inspect.
Add a List<Assembly>
collection to your main component (normally App
)
private List<Assembly> lazyLoadedAssemblies = new List<Assembly>()
{
};
Tell the router to check this collection for new assemblies using the AdditionalAssemblies
parameter.
<Router AppAssembly="@typeof(Program).Assembly"
AdditionalAssemblies="@lazyLoadedAssemblies"
OnNavigateAsync="@OnNavigateAsync">
And when you load a new assembly add it to the collection:
if (ctxt.Path == "lazy-loaded-component" || ctxt.Path == "fetchdata")
{
var assemblies = await lazyAssemblyLoader.LoadAssembliesAsync(new[]
{
"LazyLoadedLibrary.dll", "ServiceProxies.dll"
});
lazyLoadedAssemblies.AddRange(assemblies);
}
Run the application. Everything should work as before! Or will it?
Lazy Loading and Dependency Injection
Lazy loading works very well for simple components.
But what if your component uses a dependency injected service
and you want that service to be lazy loaded as well??
Let us look at an example:
The FetchData
component of the example uses the IWeatherForecastService
service.
public partial class FetchData
{
[Inject]
public IWeatherForecastService forecastService;
private IEnumerable<WeatherForecast> forecasts;
protected override async Task OnInitializedAsync()
{
forecasts = await forecastService.GetWeatherForecasts();
}
}
As we have seen in the Lazy Loading section, we can move this component to our lazy loaded class library.
The WeatherForecastService
is implemented in the ServiceProxies
class library. Since we want to lazy load the WeatherForecastService
dependency, we add this assembly to the list of lazy loaded assemblies:
<!-- List of lazy loaded assemblies -->
<ItemGroup>
<BlazorWebAssemblyLazyLoad Include="LazyLoadedLibrary.dll" />
<BlazorWebAssemblyLazyLoad Include="ServiceProxies.dll" />
</ItemGroup>
And we should not forget to add it in routing:
if (ctxt.Path == "lazy-loaded-component" || ctxt.Path == "fetchdata")
{
var assemblies = await lazyAssemblyLoader.LoadAssembliesAsync(new[]
{
"LazyLoadedLibrary.dll",
"ServiceProxies.dll
});
lazyLoadedAssemblies.AddRange(assemblies);
}
When you run this project, it will fail. This is because when we configure DependencyInjection in our Program.cs
this line fails:
builder.Services.AddScoped< IWeatherForecastService, WeatherForecastService>();
The error tells us it cannot find the ServiceProxies
assembly. This should not come
as a surprise... We told the Blazor Runtime NOT to load it yet.
System.IO.FileNotFoundException: Could not load file or assembly 'ServiceProxies, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies
But now we are in a pickle... We cannot lazy load the assembly with the dependency
if we want to configure dependency injection. DI become immutable after it has been
configured, so we cannot postpone the configuration of the dependency...
The way to fix this is to use a class that is not lazy loaded,
and that will create the dependency after we have loaded the assembly.
Start by creating a new library project (in my code called LazyLoaded
).
Then add a reference to the actual dependency (ServiceProxies
).
Next add a class LazyLoadedWeatherForecastService
that will function as a Factory:
public class LazyLoadedWeatherForecastService
{
private readonly IServiceProvider services;
public LazyLoadedWeatherForecastService(IServiceProvider services)
{
this.services = services;
}
public IWeatherForecastService Create()
{
HttpClient http = this.services.GetRequiredService<HttpClient>();
return new WeatherForecastService(http);
}
}
This class will create the WeatherForecastService
instance (and all its dependencies)
when the Create
method gets called. Please note that the Create
method will cause the runtime to locate the WeatherForecastService
type. The idea is that we will call this
method after we have lazy-loaded the ServiceProxies
assembly.
Now add a reference to this project (LazyLoaded
) in the lazy-loaded libary.
<ProjectReference Include="..\LazyLoaded\LazyLoaded.csproj" />
Replace the dependency in your Program.cs
// builder.Services.AddScoped< IWeatherForecastService, WeatherForecastService>();
builder.Services.AddScoped<LazyLoadedWeatherForecastService>();
Replace the IWeatherForecastService dependency with the factory class:
@page "/fetchdata"
@inject LazyLoaded.LazyLoadedWeatherForecastService factory
And use it to create the actual dependency
protected override async Task OnInitializedAsync()
{
forecasts = await factory.Create().GetWeatherForecasts();
}
Now when you navigate to fetchdata
, the router will lazy-load the two assemblies (one with the razor components, one with the WeatherForecastService
), create the component, run it.
This will inject the LazyLoadedWeatherForecastService
into the FetchData
component, which
uses it to create the actual dependency.
That all folks!