Skip to content

Add framework support for lazy-loading assemblies on route change #23290

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 9, 2020

Conversation

captainsafia
Copy link
Member

@captainsafia captainsafia commented Jun 24, 2020

This PR adds support for lazy loading assemblies to the Blazor framework.

In some scenarios, users might want to load assemblies dynamically at runtime based on certain constraints. For example, only load the assemblies needed to render the admin sales page if the user navigates to mysite.com/admin/sales.

This feature is particularly helpful for users operating in the WebAssembly hosting model with pages that require a large amount of assemblies. In this case, a user can conserve network calls and local resources by not loading the assemblies they do not need.

Note: This feature is not useful in the Server hosting model and won't do anything useful there. If you're running in a Server hosting model, you don't have the same concerns about conserving network requests and local resources.

A user starts using this functionality by labelling the assemblies that should be lazy loaded in their project file. Like so:

<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="Newtonsoft.Json.dll" />
  <BlazorWebAssemblyLazyLoad Include="MegaBig.dll" />
</ItemGroup>

At build time, these dependencies will be marked as lazy loaded. This will prevent them from being automatically loaded at app launch time by our startup logic.

Note: Only assemblies that are explicitly referenced from your app can be lazy loaded. If an assembly is not referenced in the app, it won't be lazily-loaded. In fact, it won't even be part of the published output as it will be stripped by the linker.

Once a user has designated the assemblies that should be lazily-loaded, they'll need to write some code to say when they should be loaded. Enter, this PR.

The user starts by adding a OnNavigateAsync callback to a router component in their page.

<Router Assembly="typeof(App)" AdditionalAssemblies="@lazyLoadedAssemblies" OnNavigateAsync="NavigateHandler">
...
</Router>

The OnNavigateAsync handler is a callback that gets invoked whenever the user:

  • Visits a route for the first time
  • Navigates from one route to another

This callback provides a NavigationContext parameter and expects a Task in return.

public EventCallback<NavigationContext> OnNavigateAsync();

The NavigationContext object is a sealed class that exposes the following properties:

public sealed class NavigationContext {
  string Path;
  CancellationToken CancellationToken;
}

The Path property is the route a user is navigating to, such as /blog/post/1 and the CancellationToken can be used to observe the cancellation of the async task.

Note: OnNavigateAsync automatically cancels the currently running navigation task when the user navigates away from a page.

Inside OnNavigateAsync, the user can implement whatever logic is needed to determine what assemblies should be loaded. Options include:

  • if statements inside the OnNavigateAsync
  • a lookup table that maps routes to assembly names that is injected into the application
  • a lookup table that is implemented within the @code fragment

This is an implementation detail that is left to the user based on their particular scenarios.

Once I've figured out what assemblies I need to load when a user navigates to a certain NavigationContext.Path I need to load them.

Enter LazyAssemblyLoader.

LazyAssemblyLoader is a singleton registered in the DI. To use it, a user can inject the dependency into their page.

@inject LazyAssemblyLoader lazyLoader

The LazyAssemblyLoader provides a single public API method LoadAssembliesAsync which has the following API interface.

public async Task<IEnumerable<Assembly>> LoadAssembliesAsync(IEnumerable<string> assembliesToLoad)

Assembly names in, assemblies out. Under the hood, this method makes some JS interop calls to fetch the assemblies via a network call and load them into the runtime running on WASM.

Note: The implementation is designed to support pre-rendering scenarios. When pre-rendering, there is no JS runtime available to interop with. In those scenarios, we're running on the server and can assume that all assemblies are already loaded (see note above about why lazy loading on the server doesn't make sense).

So now, my Router looks something like this.

@inject LazyAssemblyLoader lazyLoader

<Router Assembly="typeof(App)" AdditionalAssemblies="@lazyLoadedAssemblies" OnNavigateAsync="NavigateHandler">
...
</Router>

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

  private async Task NavigateHandler(NavigationContext args) {
    if (args.EndsWith("TheOneAndOnly") {
        var assemblies = await lazyLoader.LoadAssembliesAsync(new List<string>() { "MyPackage.dll" });
        lazyLoadedAssemblies.AddRange(assemblies);
    }
  }
}

Note: The lazy-loaded assemblies are passed back to AdditionalAssemblies in the event that the assemblies contain routeable components. In that case, we search the new set of assemblies for routes and update our routeable. This enables scenarios where users want to lazy-loaded a collection of pages onto their app.

While lazy-loading, the user might want to showcase a UI to their customer to indicate that a page transition is happening. This PR introduces support for a Navigating render fragment that is displayed when the OnNavigate async event is running.

<Router Assembly="typeof(App)" AdditionalAssemblies="@lazyLoadedAssemblies" OnNavigateAsync="NavigateHandler">
  <Navigating>
    Hang tight, we're bringing you a fancy new page...
  </Navigating>
</Router>

In summary, this PR introduces a few changes to support lazy-loading that include:

  • Support for a LazyAssemblyLoader service
  • API changes to the Router to enable lazy-loading

@ghost ghost added the area-blazor Includes: Blazor, Razor Components label Jun 24, 2020
@captainsafia captainsafia changed the base branch from master to release/5.0-preview7 June 24, 2020 08:48
@captainsafia captainsafia force-pushed the safia/lazy-load branch 2 times, most recently from 272caab to 44d10cc Compare June 24, 2020 23:49
Copy link
Contributor

@pranavkm pranavkm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I kinda like this implementation.

@captainsafia captainsafia force-pushed the safia/lazy-load branch 4 times, most recently from 36714be to d8f21e6 Compare June 29, 2020 22:47
@captainsafia captainsafia added the feature-blazor-wasm This issue is related to and / or impacts Blazor WebAssembly label Jun 29, 2020
@captainsafia captainsafia marked this pull request as ready for review June 29, 2020 22:51
@captainsafia captainsafia force-pushed the safia/lazy-load branch 2 times, most recently from 8cea3cf to fa9f6f0 Compare June 29, 2020 23:22
Copy link
Contributor

@pranavkm pranavkm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks pretty good!

@pranavkm pranavkm added this to the 5.0.0-preview7 milestone Jun 30, 2020
@captainsafia captainsafia force-pushed the safia/lazy-load branch 2 times, most recently from b71ee8d to 7ecc8f8 Compare July 8, 2020 23:32
@captainsafia
Copy link
Member Author

Thanks for the feedback everyone. I'll be merging this PR now since we have consensus on the overall approach.

There are follow-up items to address that are tracked under the lazy-loading label.

@captainsafia captainsafia merged commit bbc1162 into master Jul 9, 2020
@captainsafia captainsafia deleted the safia/lazy-load branch July 9, 2020 01:16
@mrpmorris
Copy link

mrpmorris commented Jul 9, 2020

@captainsafia on a related note, are there plans to let us cancel the navigation too? It would be good for confirming "Do you want to save your changes?" for example, and your changes should now make it possible.

@captainsafia
Copy link
Member Author

@captainsafia on a related note, are there plans to let us cancel the navigation too? It would be good for confirming "Do you want to save your changes?" for example, and your changes should now make it possible.

This seems like a good idea long-term to me. In fact, I originally started with the assumption that the NavigationContext object would provide a CTS that the user could use the cancel the navigation. However, I think we need to flesh out how we are handling the _onNavigateCts before adding support for cancellations coming from user code. The work to improve our CTS handling will be done in a follow-up to this PR.

@SteveSandersonMS
Copy link
Member

With cancellation, it's not totally clear to me what that means, since the browser's URL and history state has already changed. If there's clear prior art for how this is handled cleanly in other frameworks that would be interesting.

@captainsafia
Copy link
Member Author

With cancellation, it's not totally clear to me what that means, since the browser's URL and history state has already changed.

Yeah, this is a good point. Our cancellation would only support cancelling the on navigate task itself, not the navigation event. A more illustrative name for OnNavigateAsync would be something like AfterNavigateButBeforeRenderingRouteComponentAsync.

@guardrex I don't think I included this is as a note in my original documentation text but we might want to add an explicit call out for this to avoid user confusion. Something like:

Note: The OnNavigateAsync callback is invoked after the browser's navigation events (e.g. "popstate") have completed.

@mrpmorris
Copy link

@SteveSandersonMS I was under the impression that Blazor already intercepts all clicks on <a> tags and pushes them through the router? In which case wouldn't it be a case of the router not changing the url in the first place?

@mrpmorris
Copy link

mrpmorris commented Jul 9, 2020

@captainsafia Perhaps OnNavigated to illustrate past tense?

I think the current name + the presence of a cancellation token will lead people to ask how to set the cancellation token to cancelled to prevent the navigation.

It's the first thing I thought anyway :)

@SteveSandersonMS
Copy link
Member

When the user clicks back/forwards, Blazor isn’t the one changing the URL. Maybe it is possible to do something useful - I’m not sure - but it would be good to point to examples in other frameworks.

@javiercn
Copy link
Member

javiercn commented Jul 9, 2020

@SteveSandersonMS the url will have changed in SSB by the time the handler runs

@pranavkm pranavkm removed the api-ready-for-review API is ready for formal API review - https://github.com/dotnet/apireviews label Mar 22, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-blazor Includes: Blazor, Razor Components feature-blazor-wasm This issue is related to and / or impacts Blazor WebAssembly
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants