Maximize Performance in Blazor WebAssembly

Blazor WebAssembly brings the power of .NET to client-side development, offering a moderninteractive, and feature-rich framework for building Single Page Applications (SPAs). Ensuring high performance is critical for delivering a seamless user experience, especially given its dependency on downloading WebAssembly (.WASM) files and other resources. This article explores essential optimization techniques, supported by practical examples, to help you maximize performance in Blazor WebAssembly applications.

1. Reduce Application Size

Reducing application size minimizes the time required to download and load your app. Key strategies include:

AOT Compilation

Ahead-of-Time (AOT) compilation converts .NET assemblies into WebAssembly for faster execution.

dotnet publish -c Release -p:RunAOTCompilation=true

The command dotnet publish -c Release -p:RunAOTCompilation=true publishes a .NET application in Release mode with Ahead-of-Time (AOT) compilation enabled. This process converts .NET Intermediate Language (IL) code into WebAssembly, improving runtime performance by reducing execution overhead on the client.

Trimming

Enable trimming to remove unused code during publishing:

dotnet publish -c Release -p:PublishTrimmed=true

The command dotnet publish -c Release -p:PublishTrimmed=true publishes a .NET application in Release mode with code trimming enabled. This removes unused code and assemblies from the final build, reducing the size of the published application and improving performance by ensuring only necessary code is included.

Lazy Loading

Load non-critical assemblies only when needed.

@page "/lazy"
@if (module == null)
{
<button @onclick="LoadModule">Load Module</button>
}
else
{
<module-component />
}

@code {
private Type module;
private async Task LoadModule()
{
var assembly = await Assembly.LoadFromStreamAsync(stream);
module = assembly.GetType("LazyLoadedModule");
}
}

This Blazor code implements lazy loading for a component. Initially, when the page is loaded, the module variable is null, and a button is displayed to the user with the text “Load Module”. When the button is clicked, the LoadModule method is triggered, which asynchronously loads a module (an assembly) and sets the module variable to the loaded type. Once the module is loaded, it dynamically renders the <module-component /> on the page.

In essence, the code defers loading the module until it is explicitly requested by the user, improving initial load performance.

2. Optimize Data Handling

Efficient data handling reduces load times and memory usage.

Efficient API Calls

Use server-side filtering and pagination:

public async Task<IActionResult> GetProducts(string category, int pageSize, int pageNumber)
{
var productsQuery = _context.Products
.Where(p => p.Category == category) // Filter by category
.OrderBy(p => p.Name) // Order by product name
.Skip((pageNumber - 1) * pageSize) // Skip records for the current page
.Take(pageSize); // Take the number of records defined by pageSize

var products = await productsQuery.ToListAsync(); // Execute the query asynchronously

return Ok(products);
}

Explanation:

  1. Where: Filters products based on the category.
  2. OrderBy: Orders the products by their name.
  3. Skip: Skips the records based on the current page and page size.
  4. Take: Takes only the number of records that fit the page size.

This code ensures that only the relevant subset of data is retrieved from the database, minimizing memory usage and improving performance, especially for large datasets.

Caching

Cache data in the browser for quicker access:

protected override async Task OnInitializedAsync()
{
if (!localStorage.Exists("productData"))
{
var products = await Http.GetFromJsonAsync<List<Product>>("api/products");
await localStorage.SetItemAsync("productData", products);
}
}

This code block checks if the “productData” is already stored in the local storage; if not, it fetches the product data from the API, stores it in the local storage for future use, and avoids making repeated API calls.

localStorage is a web API that allows websites to store data persistently in the browser. It provides a simple key-value storage mechanism, where data is stored as strings and persists across page reloads and sessions. Unlike cookies, localStorage has a larger storage capacity (usually 5-10 MB) and does not send data with every HTTP request, making it more efficient for storing data like user preferences or cached content. The data stored in localStorage remains even after the browser is closed, until it is manually cleared or expires.

3. Optimize Components

Ensure components only render what’s necessary.

Virtualization

Render large lists efficiently using Virtualize:

<Virtualize Items="@items" Context="item">
<div>@item.Name</div>
</Virtualize>
@code {
private List<Item> items = GetItems();
}

This code uses Blazor’s Virtualize component to efficiently render a large list of items by only displaying the visible items on the screen, improving performance. It binds the items list to the Virtualize component and displays each item’s name in a div.

State Management

Minimize unnecessary state updates:

if (currentValue != newValue)
{
currentValue = newValue;
StateHasChanged();
}

This code minimizes unnecessary state updates by only updating the component’s state and triggering a re-render if the new value is different from the current value, preventing redundant renders and improving performance. This ensures that the UI is only updated when there is an actual change in the state.

4. Optimize UI

Lean UI frameworks and smarter loading improve responsiveness.

Content Delivery Networks

Host CSS and JavaScript libraries on a CDN:

<link href="https://cdn.jsdelivr.net/npm/tailwindcss" rel="stylesheet">

Responsive Design

Leverage lightweight frameworks like Tailwind CSS for modern, responsive design.

5. Improve Startup Time

Faster startup times improve the perceived performance.

Pre-rendering

Enable pre-rendering for server-side rendering:

builder.Services.AddServerSideBlazor().AddCircuitOptions(options => { options.DetailedErrors = false; });

This code block optimizes performance by enabling prerendering for a Blazor Server application. By adding AddServerSideBlazor() and configuring AddCircuitOptions with options.DetailedErrors = false, the server can render the initial page content before it’s sent to the client, reducing the perceived load time. Prerendering allows the user to see the page’s UI faster while the Blazor Server connection is being established, thus improving the overall user experience by decreasing the initial wait time for content to appear.

Static File Hosting

Host .wasm and other static assets on a CDN for faster delivery.

6. Manage Resources Effectively

Prevent resource leaks by disposing of unused resources.

Dispose Unused Resources

protected override void OnInitialized()
{
timer = new Timer(UpdateTime, null, 0, 1000);
}

public void Dispose()
{
timer?.Dispose();
}

This code optimizes resource management by properly disposing of the timer when the component is no longer needed. The OnInitialized method sets up a timer to update every second, while the Dispose method ensures that the timer is disposed of when the component is destroyed, releasing resources and preventing potential memory leaks. This ensures efficient use of resources and reduces unnecessary background operations.

7. Optimize Debugging and Development

Release builds significantly enhance app performance.

Release Mode

dotnet publish -c Release

Disable Debugging in Production

{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
}
}

The command dotnet publish -c Release publishes a .NET application in Release configuration, which optimizes the code for production by enabling compiler optimizations and reducing the size of the output. This ensures that the application is optimized for performance and efficiency when deployed, as opposed to the Debug configuration, which is typically used during development.

8. Leverage Browser Capabilities

Utilize browser-native features for better performance.

Blazor Compression

Enable Brotli or Gzip compression to reduce the size of the .wasm files downloaded by the browser. Configure this in your web server settings, e.g., for Nginx:

gzip on;
gzip_types application/wasm;

This configuration enables Gzip compression for WebAssembly (.wasm) files on a server (likely in an Nginx or similar web server).

  • gzip on; turns on Gzip compression for responses.
  • gzip_types application/wasm; specifies that WebAssembly (.wasm) files should be compressed with Gzip before being sent to the client.

This optimization reduces the size of the .wasm files, improving load times and reducing bandwidth usage for clients accessing Blazor WebAssembly applications or similar content.

9. Monitor and Profile

Identify bottlenecks with the right tools.

Browser DevTools

Profile network requests and inspect application performance.

Application Insights

Track user interactions and diagnose issues:

services.AddApplicationInsightsTelemetry("your-instrumentation-key");

The code services.AddApplicationInsightsTelemetry("your-instrumentation-key"); configures Application Insights for telemetry in a .NET application. It adds the necessary services to monitor application performance, track exceptions, and collect other telemetry data, with the specified instrumentation key linking the application to a specific Application Insights resource in Azure. This helps developers monitor the application’s health and diagnose issues in real-time, optimizing both performance and troubleshooting processes.

Blazor WebAssembly enables developers to build powerful, feature-rich Single Page Applications (SPAs) with .NET. By adopting these performance optimization strategies, you can enhance application load times, improve responsiveness, and ensure scalability. From reducing application size to leveraging AOT compilation, every optimization contributes to delivering a seamless user experience. Begin applying these techniques today to fully harness the potential of Blazor WebAssembly.