top of page

Advanced Techniques for Optimizing Blazor WebAssembly Applications

Blazor WebAssembly is a revolutionary framework that allows developers to build interactive web applications using C# and .NET, instead of JavaScript. This opens up new possibilities for web development but also introduces new concepts and practices for hosting and deploying these applications. In this article, we will delve into advanced techniques for optimizing Blazor WebAssembly applications.


Optimizing Blazor  WebAssembly Applications

Advanced Techniques for Optimizing Blazor WebAssembly Applications

Here are the top advanced techniques you can use to optimize your Blazor WebAssembly application:

  1. Webcil Packaging Format: This technique wraps .NET assemblies in a WebAssembly binary format, making them more web-friendly.

  2. Ahead-of-Time (AOT) Compilation: AOT compilation compiles .NET code directly into WebAssembly, potentially leading to significant runtime performance improvements.

  3. Trimming .NET Intermediate Language (IL): Trimming .NET IL after AOT compilation can reduce the size of the deployed application, leading to faster load times and a better user experience.

  4. Runtime Relinking: Runtime relinking optimizes the .NET runtime by removing unused code, resulting in a smaller and faster-loading application.


1. Webcil Packaging Format for .NET Assemblies

Webcil is a packaging format for .NET assemblies that are designed to be web-friendly. It wraps .NET assemblies in a WebAssembly binary format, which allows them to be loaded and executed in the browser just like any other WebAssembly module.


The Webcil packaging format uses a standard WebAssembly wrapper, where the assemblies are deployed as WebAssembly files that use the standard .wasm file extension. This means that the .NET assemblies are wrapped in a WebAssembly binary format, which allows them to be loaded and executed in the browser.


Benefits and Use cases of Webcil in Blazor WebAssembly:

There are several benefits and use cases of using the Webcil packaging format in Blazor WebAssembly:

  1. Network Restrictions: Some network environments have restrictions on the types of files that can be downloaded. For example, they might block the download of DLL files. In such cases, the Webcil packaging format can be used to bypass these restrictions, as the .NET assemblies are deployed as .wasm files, which are typically allowed.

  2. Performance: Webcil files are smaller than their corresponding DLL files. This means that they can be downloaded faster, leading to improved performance.

  3. Security: The Webcil packaging format provides an additional layer of security, as the .NET assemblies are not exposed directly to the client. Instead, they are wrapped in a WebAssembly binary format, which is more difficult to reverse-engineer.


Example of using Webcil Packaging Format:

Let’s say you have a .NET project named MyProject that you’ve written in C#. The project structure might look something like this:

MyProject/
├── Program.cs
├── MyProject.csproj
└── bin/
    └── Debug/
        ├── netstandard2.0/
        │   ├── MyProject.dll
        │   ├── MyProject.pdb
        │   └── MyProject.deps.json
        └── ...

When you compile this project, the C# code in Program.cs is transformed into a .NET assembly, which is a binary file with a .dll extension (MyProject.dll).


Now, you want to deploy your project to the web using Blazor WebAssembly. To do this, you’ll need to package the .NET assembly in a way that it can be loaded in the browser. This is where Webcil comes in.


The Webcil packaging process takes your .NET assembly and wraps it in a WebAssembly binary format. The resulting file has a .wasm extension. After the Webcil packaging process, your project structure might look something like this:

MyProject/
├── Program.cs
├── MyProject.csproj
└── bin/
    └── Release/
        ├── netstandard2.0/
        │   ├── publish/
        │   │   ├── MyProject.wasm
        │   │   ├── MyProject.pdb
        │   │   └── MyProject.deps.json
        └── ...

Now, you can deploy these .wasm files to your web server. When a user navigates to your website, their browser will download these .wasm files, just like it would download any other WebAssembly module.


This is a simplified example and the actual process may vary depending on the specifics of your project and deployment environment.


Lazy Loading:

Webcil supports lazy loading, which can improve the startup performance of Blazor WebAssembly apps. Lazy loading delays the loading of certain assemblies until they are required, which can be beneficial in large applications with many assemblies.


Disabling Webcil:

There might be scenarios where you want to disable the use of Webcil. For instance, you might be debugging your application and want to inspect the original .NET assemblies, or you might be facing compatibility issues with certain environments or versions of Blazor WebAssembly.


To disable the use of Webcil, you can set the WasmEnableWebcil property to false in your project file. Here’s how you can do it:


STEP 1: Open your .csproj file. This file defines the settings and dependencies of your project.


STEP 2: Look for the <PropertyGroup> element. This element is used to define properties that control the build process.


STEP 3: Inside the <PropertyGroup> element, add the following line:

<WasmEnableWebcil>false</WasmEnableWebcil>

STEP 4: Save your changes and close the file.


Here’s what your .csproj file might look like after you’ve made these changes:

<Project Sdk="Microsoft.NET.Sdk.Web">

  <PropertyGroup>
    <TargetFramework>netstandard2.1</TargetFramework>
    <RazorLangVersion>3.0</RazorLangVersion>
    <WasmEnableWebcil>false</WasmEnableWebcil>
  </PropertyGroup>

  <!-- ... other settings and dependencies ... -->

</Project>

Now, when you build your project, the .NET assemblies will not be wrapped in the Webcil packaging format. Instead, they will be deployed as regular .DLL files.


Disabling Webcil might have implications on the performance, security, and compatibility of your application.


File Structure:

The Webcil module stores the payload size in data segment 0, and the payload content in data segment. The payload content in data segment 1 must be aligned on a 4-byte boundary within the web assembly module.


2. Ahead-of-Time (AOT) Compilation

Ahead-of-time (AOT) Compilation is a process that compiles a higher-level programming language such as C# into a lower-level language or machine code before it is executed by the browser. This is in contrast to Just-in-Time (JIT) compilation, where the compilation happens at runtime.


In Blazor WebAssembly (WASM), AOT compilation means that your C# code is precompiled into WebAssembly bytecode during the build process, before it is sent to the client’s browser.


How AOT Improves Runtime Performance

AOT compilation can significantly improve the runtime performance of Blazor WASM applications in several ways:

  1. Faster Startup Time: Since the code is already compiled into machine code before execution, the browser can start executing it immediately upon loading, leading to faster startup times.

  2. Optimized Code Execution: AOT compilers can apply optimizations during the compilation process that make the resulting machine code run more efficiently.

  3. Improved Performance for CPU-Intensive Tasks: For applications that perform CPU-intensive tasks, AOT compilation can yield dramatic performance improvements. This is because these tasks can take full advantage of the optimizations provided by the AOT compiler.


Trade-offs of Using AOT Compilation

While AOT compilation has several benefits, there are also trade-offs to consider:

  1. Larger App Size: AOT compilation can result in a larger app size because it includes both the original .NET assemblies and the resulting WebAssembly bytecode. This could lead to longer download times for the user.

  2. Build Time: AOT compilation can increase the build time of your application because the compiler needs to process all of the code at build time.

  3. Less Flexibility: With JIT compilation, you can take advantage of certain runtime features such as dynamic code generation. These features are not available with AOT compilation.


Enabling AOT compilation in Blazor WebAssembly

To use AOT compilation you need to enable Ahead-of-Time (AOT) compilation in your Blazor WebAssembly app. Follow these steps to do so:


STEP 1: Open Your Project File:

Open your Blazor WebAssembly app’s project file (usually named .csproj).


STEP 2: Add the AOT Compilation Property:

Inside the <PropertyGroup> section of your project file, add the following line to enable AOT compilation:

<RunAOTCompilation>true</RunAOTCompilation>

This line enables AOT compilation for your project.


STEP 3: Publish Your App:

After you’ve made the changes, save the project file and publish your project. You can do this from the command line with the following command:

dotnet publish -c Release

This command builds the project in Release mode (which enables AOT compilation) and publishes the output to the publish directory.


After these steps, your Blazor WebAssembly project will be using AOT compilation. This means that the .NET code will be precompiled into WebAssembly bytecode during the build process, which can lead to improved performance at runtime. However, please note that enabling AOT compilation can result in a larger output size, so it’s a trade-off that you need to consider.


Debugging AOT Compilation

Debugging applications that use AOT Compilation can be more challenging than those that use Just-in-Time (JIT) Compilation. This is because the code that is running in the browser is not the source code, but the compiled machine code.


As of now, debugging AOT-compiled code in Blazor WebAssembly applications is a work in progress. The .NET team is working on enabling debugging with AOT-compiled code1. This involves generating and using source maps that allow the browser’s debugger to map the compiled code back to the original C# code.


However, you can use traditional debugging techniques such as

  1. Logging: A technique where you write log statements in your code to record the flow of execution and the state of variables at various points. In AOT, logging can help you understand how your pre-compiled code is behaving at runtime.

  2. Exception Handling: A technique where you write code to catch and handle exceptions - errors that occur during the execution of your program. In AOT, exception handling can help you identify issues that only occur at runtime.

  3. Conditional Compilation: A technique where certain blocks of code are only compiled and included in the final binary under certain conditions. In AOT, you can use conditional compilation to disable AOT during development and testing. This allows you to debug with the source code, while still benefiting from the performance improvements of AOT in production.


Updates and Maintenance:

When you use Ahead-of-Time (AOT) Compilation, any updates to the application require a full recompilation and redeployment of the binary. This is because the application’s code is compiled into machine code before it is deployed, and any changes to the code would necessitate a new compilation.


Here’s what you have to practice:

  • Recompilation: If you make changes to your application’s code, you’ll need to recompile the entire application. This is different from Just-in-Time (JIT) Compilation, where new code can be compiled and added at runtime.

  • Redeployment: After recompiling your application, you’ll need to redeploy the new binary. This means replacing the current version of your application on the server or servers where it’s hosted.


This process can have several implications:

  • Time and Resources: Recompiling and redeploying your application can take time and computational resources, especially for larger applications. This could slow down your development and deployment cycles.

  • Downtime: Depending on your deployment strategy and infrastructure, you might need to have some downtime while you deploy the new version of your application.

  • Versioning: You’ll need to have a good versioning and rollback strategy in place. If there’s a problem with a new version of your application, you should be able to quickly roll back to a previous version.

  • Testing: Because each update requires a full redeployment, thorough testing becomes even more important. You’ll want to catch any issues before you deploy the update, rather than having to fix them in a live environment.



3. Trimming .NET Intermediate Language (IL) After AOT Compilation

.NET Intermediate Language (IL) is a low-level programming language used as the output of compilation of .NET programming languages like C# and F#. The IL code is then just-in-time (JIT) compiled and executed by the .NET runtime. However, when we perform AOT compilation, the IL code is precompiled into WebAssembly bytecode, which can be directly executed by the browser.


After AOT compilation, the original IL code becomes redundant because the browser executes the WebAssembly bytecode, not the IL code. Therefore, we can remove or “trim” the IL code to reduce the size of the deployed application.


How Trimming .NET IL Reduces App Download Size and Load Times

Trimming the IL code after AOT compilation can significantly reduce the size of the deployed application. This is because the IL code can be quite large, especially for complex applications with many dependencies. By removing this redundant code, we can make the application package smaller.


A smaller application package has several benefits:

  1. Faster Download Times: A smaller package means there’s less data to download, so the application can be downloaded faster.

  2. Faster Load Times: Less data also means the application can load and start faster.

  3. Less Memory Usage: By removing unnecessary code, we can reduce the memory footprint of the application, which can lead to better performance, especially on devices with limited memory.


Example of Trimming .NET IL After AOT Compilation

To enable IL trimming in a Blazor WebAssembly project, you can add the WasmStripILAfterAOT property to the project file.


Here’s an example:

<PropertyGroup>
  <RunAOTCompilation>true</RunAOTCompilation>
  <WasmStripILAfterAOT>true</WasmStripILAfterAOT>
</PropertyGroup>

In this example, the RunAOTCompilation property enables AOT compilation, and the WasmStripILAfterAOT property enables IL trimming after AOT compilation. With these settings, the .NET build will perform AOT compilation and then strip away the IL code, resulting in a smaller and faster-loading application.


Note:

While IL trimming can reduce the size of the application, it can also increase the build time, as the build process needs to analyze the application to determine which IL code can be safely removed.


So, the trade-off here is between the size of the deployed application (and the associated benefits in terms of download times, load times, and memory usage) and the time it takes to build the application.


Depending on your specific requirements and constraints, you might prioritize one over the other. For example, if you’re developing a large, complex application where performance is critical, you might decide that the benefits of trimming .NET IL outweigh the increased build time.


On the other hand, if you’re working on a smaller project or if quick build times are more important to you (for example, in a rapid prototyping or agile development environment), you might choose to skip IL trimming.


Impact on Debugging

However, when you use Ahead-of-Time (AOT) compilation, the IL code is precompiled into WebAssembly bytecode before the application is deployed. If you then trim away the IL code to reduce the application size, the original IL code is no longer available in the deployed application.


This can make debugging more challenging for a couple of reasons:

  1. Lack of Source Code Mapping: Without the IL code, you lose the close representation of your source code in the deployed application. This can make it harder to map any issues or errors back to your source code.

  2. Limited Runtime Information: The trimmed IL code also means there’s less information available at runtime about the program’s structure and behavior. This can make it harder to understand what the program is doing at the point where an error occurs.


So, while trimming IL code can help reduce the size of your application, it’s a trade-off that can make debugging more challenging. It’s important to be aware of this when deciding whether to use IL trimming in your AOT-compiled application.


Compatibility with Reflection

The .NET Reflection API relies heavily on metadata that is stored in the Intermediate Language (IL) code. This metadata includes information about the types, methods, properties, fields, and events in your code, as well as their attributes, visibility, relationships, and more.


When you use Ahead-of-Time (AOT) compilation, the IL code is precompiled into WebAssembly bytecode. If you then trim the IL code to reduce the size of the application, some of this metadata may be removed. This is because the trimming process tries to remove anything that is not directly used by your code, and some metadata might appear to be unused even though it’s needed for reflection.


It means:

  • Missing Metadata: If you’re using reflection to access metadata about types or members that have been trimmed, those reflection calls will fail at runtime. For example, if you’re using Type.GetType to get a reference to a type, and that type has been trimmed, Type.GetType will return null.

  • Limited Functionality: Some reflection-based operations might still work, but with limited functionality. For example, you might be able to get a Type object for a trimmed type, but some of its methods or properties might be missing.

  • Runtime Errors: In some cases, the lack of metadata can cause runtime errors. For example, if you’re using reflection to invoke a method that has been trimmed, you’ll get a MissingMethodException at runtime.


To mitigate these issues, .NET provides options to control the trimming process. You can specify which assemblies, types, or members should be preserved during trimming by using TrimmerRootAssembly, DynamicDependencyAttribute, or PreserveDependencyAttribute. However, using these options increases the size of the output, so it’s a trade-off between application size and reflection compatibility.


4. Runtime Relinking

Runtime relinking is a process that optimizes the .NET runtime used by Blazor WebAssembly by removing unused runtime code. This process is performed during the build process when you publish your Blazor WebAssembly app.


The .NET runtime used by Blazor WebAssembly is contained in a file named dotnet.wasm. This file includes a lot of functionality to support a wide range of .NET features. However, not all apps use all these features. Runtime relinking takes advantage of this by analyzing your app and determining which parts of the runtime are used. It then creates a new dotnet.wasm file that only includes the used parts, effectively “relinking” the runtime.


Role of Runtime Relinking in Blazor WebAssembly

Runtime relinking plays a crucial role in optimizing Blazor WebAssembly apps:

  1. Reducing App Size: By trimming unused runtime code, runtime relinking can significantly reduce the size of the dotnet.wasm file, and therefore the overall size of the app. This can lead to faster download times, which is especially beneficial for users with slower internet connections.

  2. Improving App Performance: A smaller dotnet.wasm file means there’s less data for the browser to download and parse, which can lead to faster app load times. Additionally, by removing unused code, runtime relinking can also reduce the memory footprint of the app, potentially leading to better runtime performance.


Enable Runtime Relinking

Here are the steps to enable runtime relinking in the Blazor WebAssembly project:


STEP 1: Install .NET WebAssembly Build Tools:

First, you need to install the .NET WebAssembly build tools. You can do this by adding a PackageReference to Microsoft.NET.Runtime.WebAssembly.Sdk in your project file:

<ItemGroup> <PackageReference Include="Microsoft.NET.Runtime.WebAssembly.Sdk" Version="6.0.0" PrivateAssets="all" /> 
</ItemGroup>

Replace 6.0.0 with the version that matches your project’s .NET version.


STEP 2: Enable Runtime Relinking: Runtime relinking is enabled by default when you publish your Blazor WebAssembly app in the “Release” configuration. If you want to disable it, you can add the following property to your project file:

<PropertyGroup> <BlazorWebAssemblyEnableDebugging>false</BlazorWebAssemblyEnableDebugging> 
</PropertyGroup>

STEP 3: Publish Your App: After you’ve made the changes, save the project file and publish your project. You can do this from the command line with the following command:

dotnet publish -c Release

This command builds the project in Release mode (which enables runtime relinking) and publishes the output to the publish directory.


After these steps, your Blazor WebAssembly project will be using runtime relinking. This means that the .NET runtime will be optimized by removing unused runtime code, which can lead to a smaller and faster-loading application.


However, please note that enabling runtime relinking can result in a longer build time, as the build process needs to analyze the application and the .NET runtime to determine which code can be safely removed.


Suppressing Runtime Relinking

If you’re investigating a problem with the publishing process or if the increased build time is a concern, you might want to suppress runtime relinking. This means you’re instructing the build process to skip the runtime relinking step.


To suppress runtime relinking, you can set the UsingBrowserRuntimeWorkload MSBuild property to false. Here’s how you can do it:


STEP 1: Open your .csproj file. This file defines the settings and dependencies of your project.


STEP 2: Look for the <PropertyGroup> element. This element is used to define properties that control the build process.


STEP 3: Inside the <PropertyGroup> element, add the following line:

<UsingBrowserRuntimeWorkload>false</UsingBrowserRuntimeWorkload>

STEP 4: Save your changes and close the file.


Now, when you build your project, the .NET runtime will not undergo the runtime relinking process. Instead, it will be deployed as it is, without any removal of unused code.


However, there’s an important caveat: this option to suppress runtime relinking is only available when the Blazor WebAssembly project doesn’t use the “Native Dependency” feature. The “Native Dependency” feature allows Blazor WebAssembly apps to use native dependencies, and if your project uses this feature, you won’t be able to suppress runtime relinking.


Continuous Integration/Continuous Deployment (CI/CD) Considerations

When you’re using a CI/CD pipeline to build and deploy your application, there are several considerations to keep in mind if you’re also using runtime relinking:

  1. Increased Build Time: Runtime relinking can increase the build time because it involves analyzing the application and the .NET runtime to determine which code can be safely removed. Your CI/CD pipeline needs to be configured to handle this increased build time. This might involve adjusting build timeouts or configuring build servers to have sufficient resources.

  2. Build Failures: If the relinking process encounters an error, it could cause the build to fail. You’ll need to have a strategy for handling build failures, such as notifications to developers or automatic rollback to a previous build.

  3. Testing: Because runtime relinking can change the behavior of your application, it’s important to have thorough testing as part of your CI/CD pipeline. This can help catch any issues introduced by the relinking process before they affect the production environment.

  4. Deployment: The deployment process might also need to be adjusted to handle the smaller application size resulting from runtime relinking. For example, if you’re using blue-green deployment, you’ll need to ensure that both environments have the same version of the runtime.


Conclusion

These techniques can significantly enhance the performance of your Blazor WebAssembly applications. However, they require a deep understanding of both the Blazor framework and the underlying .NET and WebAssembly technologies. Always test thoroughly when implementing these optimizations to ensure they are beneficial for your specific application.

bottom of page