Ahead-Of-Time Compilation for Blazor Wasm

Blazor Wasm applications are being interpreted in the browser by default. The idea behind Wasm is to be able to compile any language to Wasm and in this way have a common platform that all code can be run on. Instead of doing this directly Blazor Wasm first bootstrapped this approach by making an IL interpreter in Wasm and with this made it possible to run .NET code compiled to IL instructions in the browser. In this article, we will look at how you can compile your code directly to Wasm Ahead-Of-Time (AOT) and what pros and cons this brings.

Prerequisites

AOT compilation for Blazor Wasm was first introduced in .NET 6 preview 4 so to use this feature you need to install this version of .NET 6 or newer. It can be downloaded following this link: Download .NET 6. The tools used for this were further polished in .NET 6 preview 7 so this version is recommended to be used, but not a requirement.

For the best developer experience, you also need the newest preview version of Visual Studio. Visual Studio Preview can be downloaded here: Download Visual Studio Preview

Minimal example

Now that we have the newest version of .NET 6, let's get started with a sample project that we will enable AOT compilation for. So we start by creating a new Blazor Wasm project using this command in some empty directory.

dotnet new blazorWasm

This will make the standard template Blazor Wasm project that we know.
In the csproj file we will change the first PropertyGroup to the following.

<PropertyGroup>
  <TargetFramework>net6.0</TargetFramework>
  <RunAOTCompilation>true</RunAOTCompilation>
</PropertyGroup>

We simply added a new tag that specifies that we will use AOT compilation.

Next, we need a special tool or more specifically a workload to be able to do AOT compilation. We add this by running the following command.

dotnet workload install wasm-tools

For .NET 6 Preview 6 and earlier you should use the following command instead.

dotnet workload install microsoft-net-sdk-blazorWasm-aot

Troubleshooting

The Workload command can be very unstable depending on what preview version of .NET you have, what operating system you run this on, and how much you have played with workloads previously. And this is only a preview feature so it should be expected to have some changes over time between versions. A common error is that you don't have the references for the workloads. To fix this we can add the following argument to the end of the previous command.

--source https://pkgs.dev.azure.com/dnceng/public/_packaging/6.0.100-preview.7.21379.14-shipping-1/nuget/v3/index.json

but switching this version number 7.21379.14 to that of the preview that you use. In Preview 6 and earlier the argument is --add-source instead. If you don't use a preview version of .NET 6 then this should not be necessary. Sometimes it might also fail on updating manifests before installing workloads. To fix this and skip the manifest update add the following to the end as well.

--skip-manifest-update

Compiling AOT

Now we are ready to do the AOT compilation. For this, we simply do a normal Release publish.

dotnet publish -c Release

It took 4 minutes and 42 seconds to compile this template project with AOT. The total published folder is 30.4 MB and on the first load, the page is 8.1 MB.

Compared to a normal publish that takes 11 seconds to compile. Has a published folder size of 25.7 MB and has a load size of 3.3 MB

The most notable difference is that it takes a lot more time to compile the different assemblies and that the compiled assemblies are bigger than the IL code. And for this example, there aren't really any pros. For us to gain something from doing AOT compilation we will have to use some computation heavy work in our application. AOT compilation can potentially make tasks written in C# faster than the same tasks written in JavaScript.

Example: Conway's Game of Life

So let's try to make a page that does a lot of work and see it do the work faster with AOT Compilation.

I have made a very simple implementation of Conway's Game of Life here:

Screenshot for implementation of Conway's Game of Life
@page "/"
<button @onclick="() => Start()">Start</button> @Status
<table border="1">
    @for (int i = 0; i < BoardSize; i++)
    {
        <tr>
            @for (int j = 0; j < BoardSize; j++)
            {
                var row = i;
                var col = j;
                <td @onclick="() => Board[row, col] = !Board[row, col]" style="width:10px;height:10px;background-color:@(Board[row, col] ? "black" : "white")">
                </td>
            }
        </tr>
    }
</table>

@code {
    private int BoardSize = 100;

    protected bool[,] Board { get; set; }

    protected string Status = "Not Started";

    protected override void OnInitialized()
    {
        Board = new bool[BoardSize, BoardSize];
    }

    protected void Start()
    {
        var watch = System.Diagnostics.Stopwatch.StartNew();
        for (int iteration = 0; iteration < 300; iteration++)
        {
            var NewBoard = new bool[BoardSize, BoardSize];
            for (int row = 0; row < BoardSize; row++)
            {
                for (int col = 0; col < BoardSize; col++)
                {
                    if (Board[row, col])
                    {
                        if (GetNeighbors(row, col) is 2 or 3)
                        {
                            NewBoard[row, col] = true;
                        }
                    }
                    else
                    {
                        if (GetNeighbors(row, col) is 3)
                        {
                            NewBoard[row, col] = true;
                        }
                    }
                }
            }
            Board = NewBoard;
        }
        watch.Stop();
        Status = $"Miliseconds: {watch.ElapsedMilliseconds}";
    }

    private int GetNeighbors(int row, int col)
    {
        var res = 0;
        for (int i = row - 1; i < row + 2; i++)
        {
            for (int j = col - 1; j < col + 2; j++)
            {
                if (i >= 0 && i < BoardSize && j >= 0 && j < BoardSize && Board[i, j])
                {
                    res++;
                }
            }
        }
        if (Board[row, col]) res--;
        return res;
    }
}

You don't have to know what Conway's Game of Life is to see that this contains a lot of nested for loops. These are used to change the state of the Board that is displayed in a table. Every cell is either alive or dead and some rules based on the number of neighbors decide if they are alive after each iteration. Together cells make complex machines that can move across the Board and construct other machines.

If we run this example in an app that has the normal Release build then it takes 1705 milliseconds but if we run this on a AOT compiled version then it only takes 361 milliseconds.

This means that this runs about 4.72 times as fast when AOT compiled. Others have reported gains of up to 8 times as good in certain projects. This is a great increase and we could imagine that this would be very useful if applied to more complex examples.

Conclusion

In this article, we have seen how to add AOT Compilation to a Blazor Wasm project. We have seen the difference in size of the published projects with and without AOT. And in the end, we have seen how well a simple implementation of Conway's Game of Life performs with AOT.

elmah.io: Error logging and Uptime Monitoring for your web apps

This blog post is brought to you by elmah.io. elmah.io is error logging, uptime monitoring, deployment tracking, and service heartbeats for your .NET and JavaScript applications. Stop relying on your users to notify you when something is wrong or dig through hundreds of megabytes of log files spread across servers. With elmah.io, we store all of your log messages, notify you through popular channels like email, Slack, and Microsoft Teams, and help you fix errors fast.

See how we can help you monitor your website for crashes Monitor your website