cs11dotnet7/docs/bonus/improving-wasm-apps.md
2022-10-19 12:32:54 +01:00

19 KiB

Improving Blazor WebAssembly apps

This is an optional bonus section for Chapter 16. It is not required to complete the rest of the book.

There are common ways to improve Blazor WebAssembly apps. We'll look at some of the most popular ones now.

Enabling Blazor WebAssembly AOT

By default, the .NET runtime used by Blazor WebAssembly is doing IL interpretation using an interpreter written in WebAssembly. Unlike other .NET apps, it does not use a just-in-time (JIT) compiler, so the performance of CPU-intensive workloads is lower than you might hope for.

In .NET 6 and later, Microsoft added support for ahead-of-time (AOT) compilation, but you must explicitly opt-in because although it can dramatically improve runtime performance, AOT compilation can take several minutes on small projects like the ones in this book and potentially much longer for larger projects. The size of the compiled app is also larger than without AOT—typically twice the size. The decision to use AOT is therefore based on a balance of increased compile and browser download times with potentially much faster runtimes.

AOT was the top requested feature in a Microsoft survey, and the lack of AOT was cited as a primary reason why some developers had not yet adopted .NET for developing single-page applications (SPAs).

Let's install the additional required workload for Blazor AOT named .NET WebAssembly build tools and then enable AOT for our Blazor WebAssembly project:

  1. In the command prompt or terminal with admin rights, install the Blazor AOT workload, as shown in the following command:
dotnet workload install wasm-tools
  1. Note the messages, as shown in the following partial output:
...
Installing pack Microsoft.NET.Runtime.MonoAOTCompiler.Task version 7.0.0...
Installing pack Microsoft.NETCore.App.Runtime.AOT.Cross.browser-wasm version 7.0.0...
Successfully installed workload(s) wasm-tools.
  1. Modify the Northwind.BlazorWasm.Client project file to enable AOT, as shown highlighted in the following markup:
<PropertyGroup>
  <TargetFramework>net7.0</TargetFramework>
  ...
  <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>
  1. Publish the Northwind.BlazorWasm.Client project, as shown in the following command:
dotnet publish -c Release

Be patient. Wait for the process to finish. The process can take at least 20 minutes even on a modern multi-core CPU.

  1. Note that more than 70 assemblies have AOT applied, as shown in the following partial output:
  Northwind.BlazorWasm.Shared -> C:\cs11dotnet7\PracticalApps\Northwind.BlazorWasm\Shared\bin\Release\net7.0\Northwind.BlazorWasm.Shared.dll
  Northwind.Common.EntityModels.Sqlite -> C:\cs11dotnet7\PracticalApps\Northwind.Common.EntityModels.Sqlite\bin\Release\net7.0\Northwind.Common.EntityModels.Sqlite.dll
  Northwind.BlazorWasm.Client -> C:\cs11dotnet7\PracticalApps\Northwind.BlazorWasm\Client\bin\Release\net7.0\Northwind.BlazorWasm.Client.dll
  Northwind.BlazorWasm.Client (Blazor output) -> C:\cs11dotnet7\PracticalApps\Northwind.BlazorWasm\Client\bin\Release\net7.0\wwwroot
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
  Optimizing assemblies for size. This process might take a while.
  Northwind.Common.EntityModels.Sqlite -> C:\cs11dotnet7\PracticalApps\Northwind.Common.EntityModels.Sqlite\bin\Release\net7.0\Northwind.Common.EntityModels.Sqlite.dll
  Northwind.BlazorWasm.Shared -> C:\cs11dotnet7\PracticalApps\Northwind.BlazorWasm\Shared\bin\Release\net7.0\Northwind.BlazorWasm.Shared.dll
  Optimizing assemblies for size may change the behavior of the app. Be sure to test after publishing. See: https://aka.ms/dotnet-illink
  AOT'ing 73 assemblies
  [1/72] System.ComponentModel.dll -> System.ComponentModel.dll.bc
  ...
  [72/72] Microsoft.EntityFrameworkCore.Abstractions.dll -> Microsoft.EntityFrameworkCore.Abstractions.dll.bc
  Compiling native assets with emcc with -Oz. This may take a while ...
  [1/3] pinvoke.c -> pinvoke.o [took 0.78s]
  [2/3] corebindings.c -> corebindings.o [took 0.98s]
  [3/3] driver.c -> driver.o [took 1.15s]
  Compiling assembly bitcode files with -O2 ...
  [1/72] System.ComponentModel.dll.bc -> System.ComponentModel.dll.o [took 2.61s]
  ...
  Linking with emcc. This may take a while ...
  ...
  Optimizing dotnet.wasm ...
  Compressing Blazor WebAssembly publish artifacts. This may take a while...
  1. Navigate to the Northwind.BlazorWasm\Client\bin\release\net7.0\publish folder and note the increased size of the downloaded files. Without AOT, the downloaded Blazor WebAssembly app took up about 10.65 MB of space. With AOT, it takes about 95.9 MB. This increase in size will affect a website visitor's experience.

Good Practice: The use of AOT is a balance between a larger initial download (and therefore slower start) and faster potential execution after that. Depending on the specifics of your app, AOT might not be worth it.

Exploring Progressive Web App support

Progressive Web App (PWA) support in Blazor WebAssembly projects means that the web app gains the following benefits:

  • It acts as a normal web page until the visitor explicitly decides to progress to a full app experience, with better integrations with native platform features like notifications, full access to the filesystem, and so on.
  • After the app is installed, it can be launched from the OS's start menu or desktop.
  • It visually appears in its own app window instead of a browser tab.
  • It works offline.
  • It automatically updates.

Let us see PWA support in action:

  1. Start the Northwind.BlazorWasm.Server project.
  2. Start Chrome and navigate to https://localhost:5007/.
  3. In Chrome, in the address bar on the right, click the icon with the tooltip Install Northwind.BlazorWasm, as shown in Figure 16.11:

Figure 16.11: Installing Northwind.BlazorWasm as an app Figure 16.11: Installing Northwind.BlazorWasm as an app

  1. Click the Install button.
  2. Close the app window and close Chrome.
  3. Launch the Northwind.BlazorWasm app from your Windows Start menu or macOS Launchpad and note that it has a full app experience. Search for northwind if it does not appear in the recent apps list.
  4. On the right of the title bar, click the three dots menu and note that you can uninstall the app, but do not do so yet.
  5. Navigate to Developer Tools. On Windows, you can press F12 or Ctrl + Shift + I. On macOS, you can press Cmd + Shift + I.
  6. Select the Network tab and then, in the Throttling dropdown, select the Offline preset.
  7. In the left navigation menu, click Home and then click Customers Worldwide, and note the failure to load any customers and the error message at the bottom of the app window, as shown in Figure 16.12:

Figure 16.12: Failure to load any customers when the network is offline Figure 16.12: Failure to load any customers when the network is offline

  1. In Developer Tools, set Throttling back to Disabled: No throttling.
  2. Click the Reload link in the yellow error bar at the bottom of the app and note that functionality returns.
  3. You could now uninstall the PWA app or just close it.

Implementing offline support for PWAs

We could improve the experience by caching HTTP GET responses from the Web API service locally, storing new, modified, or deleted customers locally, and then synchronizing with the server later by making the stored HTTP requests once network connectivity is restored. But that takes a lot of effort to implement well, so it is beyond the scope of the 2022 editions of my books. I do plan to add coverage of offline support in the second edition of my book, Apps and Services with .NET 8, planned to be published in November 2023, if enough readers tell me they want that.

Understanding the browser compatibility analyzer for Blazor WebAssembly

With .NET 6 and later, Microsoft has unified the .NET library for all workloads. However, although, in theory, this means that a Blazor WebAssembly app has full access to all .NET APIs, in practice, it runs inside a browser sandbox so there are limitations. If you call an unsupported API, this will throw a PlatformNotSupportedException.

To be forewarned about unsupported APIs, you can add a platform compatibility analyzer that will warn you when your code uses APIs that are not supported by browsers.

Blazor WebAssembly App and Razor Class Library project templates automatically enable browser compatibility checks.

To manually activate browser compatibility checks, for example, in a Class Library / classlib project, add an entry to the project file, as shown in the following markup:

<ItemGroup>
  <SupportedPlatform Include="browser" />
</ItemGroup>

Microsoft decorates unsupported APIs, as shown in the following code:

[UnsupportedOSPlatform("browser")]
public void DoSomethingOutsideTheBrowserSandbox()
{
  ...
}

Good Practice: If you create libraries that should not be used in Blazor WebAssembly apps, then you should decorate your APIs in the same way.

Interop with JavaScript

By default, Blazor components do not have access to browser capabilities like local storage, geolocation, and media capture, or any JavaScript libraries like React or Vue. If you need to interact with them, you can use JavaScript Interop.

Let's see an example that uses the browser window's alert box and local storage that can persist up to 5 MB of data per visitor indefinitely:

  1. In the Northwind.BlazorServer project, in the wwwroot folder, add a folder named scripts.
  2. In the scripts folder, add a file named interop.js, and then modify its contents to define some functions to show an alert message, and to set and get a color in local storage, as shown in the following code:
function messageBox(message) {
  window.alert(message);
}

function setColorInStorage() {
  if (typeof (Storage) !== "undefined") {
    localStorage.setItem("color", 
      document.getElementById("colorBox").value);
  }
}

function getColorFromStorage() {
  if (typeof (Storage) !== "undefined") {
    document.getElementById("colorBox").value = 
      localStorage.getItem("color");
  }
}
  1. In the Pages folder, in _Layout.cshtml, after the <script> element that adds Blazor Server support, add a <script> element that references the JavaScript file that you just created, as shown in the following code:
<script src="scripts/interop.js"></script>
  1. In the Pages folder, in Index.razor, delete the two Customers component instances, and then add a button and a code block that uses the Blazor JavaScript runtime dependency service to call a JavaScript function, as shown in the following code:
<button type="button" class="btn btn-info" @onclick="AlertBrowser">
  Poke the browser</button>
<hr />
<input id="colorBox" />
<button type="button" class="btn btn-info" @onclick="SetColor">
  Set Color</button>
<button type="button" class="btn btn-info" @onclick="GetColor">
  Get Color</button>
@code {
  [Inject]
  public IJSRuntime JSRuntime { get; set; } = null!;

  public async Task AlertBrowser()
  {
    await JSRuntime.InvokeVoidAsync(
      "messageBox", "Blazor poking the browser");
  }

  public async Task SetColor()
  {
    await JSRuntime.InvokeVoidAsync("setColorInStorage");
  }

  public async Task GetColor()
  {
    await JSRuntime.InvokeVoidAsync("getColorFromStorage");
  }
}
  1. Start the Northwind.BlazorServer project.
  2. Start Chrome and navigate to https://localhost:5005/.
  3. On the home page, in the textbox, enter red and then click the Set Color button.
  4. Show Developer Tools, select the Application tab, expand Local Storage, select https://localhost:5005, and note the key-value pair color-red, as shown in Figure 16.13:

Figure 16.13: Storing a color in browser local storage using JavaScript Interop Figure 16.13: Storing a color in browser local storage using JavaScript Interop

  1. Close Chrome and shut down the web server.
  2. Start the Northwind.BlazorServer project.
  3. Start Chrome and navigate to https://localhost:5005/.
  4. On the home page, click the Get Color button and note that the value red is shown in the textbox, retrieved from local storage between visitor sessions.
  5. Click the Poke the browser button and note the message that appears.
  6. Close Chrome and shut down the web server.

Enabling location change event handling

ASP.NET Core 7 introduces support for handling location-changing events within Blazor apps. You can use this to warn users about unsaved data changes when the user performs a page component navigation.

You register a location-changing event handler with the NavigationManager service using the RegisterLocationChangingHandler method. Your handler can then choose to either automatically save the changes or cancel the navigation.

Warning! Your handler code only executes during navigation within your Blazor app. If you need to handle navigating away from the app, then you must use JavaScript.

Even easier is the use of the NavigationLock component that you can put in a component, and it has an event that you can easily handle to prevent navigation. It also has an extra option named ConfirmExternalNavigation that will prevent external navigation using some JavaScript.

Let's prevent the visitor from navigating away from the EditCustomer page component if they have made changes that are unsaved:

  1. In the Northwind.BlazorWasm.Client project, in the Shared folder, in CustomerDetail.razor, add statements to execute event handlers when text is input into any of the four textboxes, and to set a parameter named HasChanges, as shown highlighted in the following partial markup:
<EditForm Model="@Customer" OnValidSubmit="@OnValidSubmit">
...
        <InputText @bind-Value="@Customer.CustomerId" 
                   @oninput="OnInputCustomerId" />
...
        <InputText @bind-Value="@Customer.CompanyName" 
                   @oninput="OnInputCompanyName" />
...
        <InputText @bind-Value="@Customer.Address" 
                   @oninput="OnInputAddress" />
...
        <InputText @bind-Value="@Customer.Country" 
                   @oninput="OnInputCountry" />
...
</EditForm>
@code {
...
  [Parameter]
  public bool HasChanges { get; set; } = false;

  private void OnInputCustomerId(ChangeEventArgs args)
  {
    if (Customer.CustomerId != args.Value?.ToString()) HasChanges = true;
  }

  private void OnInputCompanyName(ChangeEventArgs args)
  {
    if (Customer.CompanyName != args.Value?.ToString()) HasChanges = true;
  }

  private void OnInputAddress(ChangeEventArgs args)
  {
    if (Customer.Address != args.Value?.ToString()) HasChanges = true;
  }

  private void OnInputCountry(ChangeEventArgs args)
  {
    if (Customer.Country != args.Value?.ToString()) HasChanges = true;
  }

  public void ClearChanges()
  {
    HasChanges = false;
  }
}
  1. In the Pages folder, in EditCustomer.razor, add statements to prevent navigation away from the page if the customer has changes, as shown highlighted in the following markup:
@page "/editcustomer/{customerid}" 
@inject INorthwindService service 
@inject NavigationManager navigation
@inject IJSRuntime jsRuntime
<h3>Edit Customer</h3>

<CustomerDetail ButtonText="Update"
                Customer="@customer" 
                @ref="customerDetail"
                OnValidSubmit="@Update" />

<NavigationLock OnBeforeInternalNavigation="ConfirmNavigation" 
                ConfirmExternalNavigation />
@code {
...
  private CustomerDetail? customerDetail;

  async Task ConfirmNavigation(LocationChangingContext context)
  {
    if (customerDetail is null) return;

    if (customerDetail.HasChanges)
    {
      bool leave = await jsRuntime.InvokeAsync<bool>(
        "window.confirm",
        "You will lose changes if you leave. OK to leave. Cancel to stay.");

      if (leave)
      {
        customerDetail.ClearChanges();
      }
      else
      {
        context.PreventNavigation();
      }
    }
  }
}
  1. Start the Northwind.BlazorWasm.Server project.
  2. Start Chrome and navigate to https://localhost:5007/.
  3. Select Customers in France, and then in the BLONP row, click the Edit button.
  4. Click in any textbox but do not make any changes.
  5. Click Counter and note that you are not prevented from navigating to it.
  6. Select Customers in France, and then in the BLONP row, click the Edit button.
  7. Change the Address to 42, place Kléber.
  8. Click Counter and note that you are prevented from navigating to it, as shown in Figure 16.14:

Figure 16.14: Preventing a visitor from navigating away when there are unsaved changes Figure 16.14: Preventing a visitor from navigating away when there are unsaved changes

  1. Click OK to leave.
  2. Select Customers in France, and then in the BLONP row, click the Edit button.
  3. Note that you lost the previous change.
  4. Change the Address to 24, place Kléba`.
  5. Click Counter and note that you are prevented from navigating to it.
  6. Click Cancel to stay.
  7. Click Update.
  8. Click Counter and note that you are prevented from navigating to it.
  9. Click OK to leave.
  10. Select Customers in France, and then in the BLONP row, note that your change was saved.

Libraries of Blazor components

There are many libraries of Blazor components. Paid component libraries are from companies like Telerik, DevExpress, and Syncfusion.

Open-source Blazor component libraries include the following:

My companion book, Apps and Services with .NET 7, covers more about Blazor in its Chapter 16, Building Web Components Using Blazor WebAssembly. That book also covers Radzen Blazor components in its Chapter 17, Leveraging Open-Source Blazor Component Libraries.