For years now, if you wanted to write code to run in a browser, your choices were JavaScript or JavaScript. For a couple of brief periods on certain browsers, there were other languages you could use, but they weren’t significant: VBScript on IE and Dart on a special build of Chrome.
There are also languages that compile down to JavaScript (TypeScript, CoffeeScript, …), but they were still really JavaScript under the covers. The JavaScript monoculture’s days are numbered with the advent of WebAssembly (Wasm). For .NET developers, Wasm is arriving in the form of Blazor.
WebAssembly is a specification for a virtual machine in the browser that supports running Wasm bytecode. This binary format allows for faster execution than typical JavaScript so the performance can be better than pure JavaScript. There are a number of compilers that can output this format including LLVM. So it is possible now to write code in C++, compile it to Wasm assembly, send it to a browser, and run it directly.
The browser support for Wasm is quite broad. Even on older browsers, support is available via asm.js, all be it at a lower performance threshold.
A number of languages have started projects to bring their languages to WebAssembly by outputting Wasm assembly. The approach that Microsoft has taken is a little bit different from most of the other platforms. Typically you would compile your output binaries to Wasm and then load them directly in the browser. However, .NET binaries are already in a generic language designed to run on the .NET Framework: Intermediate Language. So if instead of compiling your code to Wasm, they compiled the framework itself to Web Assembly, they could interpret the same binaries they have already using this Wasm version of the framework.
There are, of course, a few different flavors of the .NET Framework unified by .NET Standard. The version that runs as WebAssembly is actually Mono. There has been some concern voiced about using Mono over .NET Core, which I can kind of understand. It is slightly annoying to use several .NET frameworks, but the standardization efforts between the various frameworks are very good.
I like to tell people that Blazor and compiling the entire .NET Framework to Wasm is totally bonkers. The thing is that Blazor is a project of Steve Sanderson’s, who has previously created some great bonkers projects in the past: xVal, Knockout, JavaScript services. So as bonkers projects go, we’re in excellent hands.
Blazor is a highly experimental project to bring an ASP.NET feel to Wasm. You write your code in C# using all the tools you recognize and remember. You can still unit test using the same tools you have in the past. You can also still use the same logging tools like Retrace. In effect, you can take all of your C# knowledge and just write web applications.
There are currently two models for Blazor: a client-side and server-side model. The client-side version actually runs in the browser through Wasm and updates to the DOM are done there, while the server-side model retains a model of the DOM on the server and transmits diffs back and forth between the browser and the server using a SignalR pipeline. Server-side is more likely to become a real product and has, in fact, been promised for ASP.NET Core 3, which sounds like it is about a year away.
There are a number of factors which make it seem like the performance of an application deployed in this fashion would be terrible. The first is the large size of the .NET Framework. Bundling the loading of the .NET framework into each website load is large. However, there are already technologies that make this more manageable. Browsers can cache the framework and even reuse it from site to site, if the framework is delivered from a CDN. A slightly more exotic approach is to employ tree shaking to remove the huge swaths of the framework that aren’t being used by an application.
The largest asset in the project is the mono.wasm file, which is 869KB. The various DLLs that are used in the project add up to almost a megabyte.
Name | Size |
---|---|
Microsoft.AspNetCore.Blazor.Browser.dll | 14.7KB |
Microsoft.AspNetCore.Blazor.dll | 44.0KB |
Microsoft.AspNetCore.Blazor.TagHelperWorkaround.dll | 2.5KB |
Microsoft.Extension.DependencyInjection.Abstractions.dll | 11.9KB |
Microsoft.Extension.DependencyInjection.dll | 20.4KB |
Microsoft.JSInterop.dll | 20.8KB |
Mono.WebAssembly.Interop.dll | 3.2KB |
mscorlib.dll | 670KB |
System.dll | 42.2KB |
System.Core.dll | 142KB |
System.Net.Http.dll | 31.7KB |
Total | 1003.4KB |
This is a lot of framework code to download, almost 2MB without any actual functionality so that is indeed a concern.
Next, can .NET code run in a performant fashion when delivered out to the browser and compiled down to a weird assembly language? Currently, C projects compiled to Wasm seem to have a slowdown on the order of 50%. Obviously, there is an overhead to running the framework in the browser, but we’re not really sure where this is going to land yet. Work is being done to improve and optimize the speed, and by the time it hits production performance will likely be reasonable. A lot of this performance simply comes from the fact that Wasm is, in general, more efficient than JavaScript, although such benchmarks are notoriously difficult.
The end result is that Wasm seems to have a larger startup cost that a typical JavaScript web application, but that once you’re up and running any sort of computationally complex operations are faster.
The first step is to ensure that everything you have in terms of Visual Studio and .NET Core is up to date. At the time of writing, I’m on .NET Core tools version 2.1.500. Next, there are some tools that will make working with Blazor more pleasant. The first is the collection of templates that can be installed by running
dotnet new -i Microsoft.AspNetCore.Blazor.Templates
from the command line. We’ll make use of full Visual Studio in this article, but you can use VS Code or vi or whatever you want. In Visual Studio, install the Blazor extension, the official Blazor docs suggest running at least VS 15.9, but I’ve run on 15.8 without issue.
With all of that in place, we can start a brand new Blazor project. In the new project dialog, select a new ASP.NET Core project. The template selection dialog there are 3 Blazor-based templates
For our purposes, we’re going to make use of the ASP.NET Core hosted variant. Remember this version which ships DLLs to the browser isn’t going to be in the official support in ASP.NET Core 3, at least not at this juncture.
The solution, which is created from this template, has 3 projects.
The most interesting code is in the Client project, as the Server project is mostly just a standard ASP.NET Core application. The first thing in the client structure you’ll notice is that it is pretty similar to the structure of an ASP.NET Core project.
There is a Program.cs that serves as the bootstrapper, just like you’d see in an ASP.NET Core application. The Startup.cs contains code that sets up services and configures the application. This similarity allows you to carry over even more of your ASP.NET Core knowledge to the Blazor world. The HTML files all also have the same .cshtml extension you know and love. Opening one of these files reveals that they use the same Razor syntax you know from MVC way back to 2008.
The most interesting of the cshtml files is the FetchData.cshtml.
@using Blazor2.Shared @page "/fetchdata" @inject HttpClient Http <h1>Weather forecast</h1> <p>This component demonstrates fetching data from the server.</p> @if (forecasts == null) { <p><em>Loading...</em></p> } else { <table class="table"> <thead> <tr> <th>Date</th> <th>Temp. (C)</th> <th>Temp. (F)</th> <th>Summary</th> </tr> </thead> <tbody> @foreach (var forecast in forecasts) { <tr> <td>@forecast.Date.ToShortDateString()</td> <td>@forecast.TemperatureC</td> <td>@forecast.TemperatureF</td> <td>@forecast.Summary</td> </tr> } </tbody> </table> } @functions { WeatherForecast[] forecasts; protected override async Task OnInitAsync() { forecasts = await Http.GetJsonAsync<WeatherForecast[]>("api/SampleData/WeatherForecasts"); } }
In this file, you’ll notice a few interesting pieces. The @page directive is used to provide a location the router can use to direct people to the page. The @inject allows injecting services. The HttpClient is registered by default and doesn’t require an explicit entry in Startup.cs. What’s interesting here is that the HttpClient used here isn’t the same one as you use on the server side because access to raw sockets isn’t permitted in the Wasm sandbox just like it isn’t permitted in JavaScript. Finally, you’ll notice that there is a branch based on if forecasts is null. This property is actually monitored by Blazor, and when it changes, the page rendering will be rerun. Change detection is built right in.
In order to extend the project out to a fully-fledged application, you can simply add services and cshtml pages. So long as you pay attention to the structure and match how you would extend an ASP.NET Core application, you should be in good standing. You can add references to external libraries via NuGet, most of which will just work. However, you should keep in mind that every time you add a package that increases the size of the payload, that needs to make it to the client. Since we’re at near 2MB for the base package, every little bit more than you have to download hurts all that much more.
Obviously, there are some limitations to what you can do inside a browser. Everything that is run in Wasm is run inside of the JavaScript sandbox, which means that you can’t directly access things like a disk or the network. So a lot of functionality like using SQLClient to talk directly to a database won’t work (also that’s a terrible idea anyway). Libraries may contain functionality that tries to write temp files too, which won’t work. Keep these limitations in mind when you’re planning how you test the application.
One of the really nice parts about Blazor and Wasm, in general, is that it is possible to interact with the JavaScript APIs. So if you want to do geolocation through the geolocation API you can install a package like Blazor.Geolocation (freshly updated for Blazor 0.7, BTW) and it will just work. The complex marshaling is done to map JavaScript and .NET data types back and forth is all done under the hood by Blazor. For the most part, you can easily call .NET methods from JavaScript and JavaScript methods from .NET code. Mind blown!
In the example of Blazor.Geolocation to get the location information from the browser’s JavaScript context, you need only inject a location service into your cshtml and run
location = await locationService.GetLocationAsync();
The package takes care of dealing with the asynchronous nature of getting the location information from the browser. Other JavaScript APIs can be similarly wrapped.
You can read about how to interop with JavaScript in great detail in the Blazor documentation.
The debugging story isn’t fantastic at the moment. With server-side Blazor, everything is F5-debuggable in Visual Studio, but once you come over to client-side, the debugging story isn’t fully fleshed out. The sources tab in Chrome contains a baffling array of files and Wasm functions, which cannot be easily debugged at this juncture. Eventually, there may be source mapping support for all this and the experience should be much better. However, for the time being, the compiler is not able to output maps.
Network traffic can be debugged just as you would any other network traffic, for instance in Prefix. Equally, the server side remains a standard ASP.NET Core application, so it can benefit from instrumentation with Retrace or Prefix. In that regard, at least the debugging and analytics story is quite good.
Only fools make statements about the future, so here I go: I think that WebAssembly has a bright future. Unlocking web development for a myriad of languages and programming paradigms will result in some quite interesting new frameworks. The speed of WebAssembly is also promising for all manner of code from games to AI.
Run Keras, TensorFlow, PyTorch and ONXX models in a browser using the ONNXJS engine – to make this fast, it uses a WebAssembly-powered engine, is multi-threaded and can use your GPU (via WebGL): https://t.co/6AIdJ6z2Ph
— Miguel de Icaza (@migueldeicaza) November 30, 2018
If you would like to be a guest contributor to the Stackify blog please reach out to stackify@stackify.com