Démystifions Blazor et WebAssembly

Il y a quelques mois, Microsoft a exposé la première mouture de son implémentation de WebAssembly (wasm pour les intimes) en early preview (très) expérimentale : Blazor. Dans le jargon moderne, une preview expérimentale c’est une version alpha. C’est un peu comme si le petit lapin de la RATP te sussurait de ne pas mettre tes doigts trop près de la porte, car oui, tu peux te faire pincer très fort. Pas de Blazor en production, donc. En tout cas, pas encore. Ceci étant dit, voyons voir comment tout ça fonctionne.


WebAssembly

La genèse de WebAssembly est née des limites de Javascript. Les mauvaises langues diront qu’on aura beau compenser artificiellement des défauts structurels de Javascript avec de l’outillage, des moteurs JS sur-optimisés, des sur-ensembles et des grosses machines, la fuite en avant a ses limites. Mais l’idée est là. Javascript est né pour faire clignoter des balises dans Netscape, pas pour faire tourner des applications extrêmement complexes. C’est pourtant ce qu’on fait aujourd’hui tant bien que mal.

La documentation officielle propose la définition suivante :

WebAssembly (abbreviated Wasm) is a safe, portable, low-level code format designed for efficient execution and compact representation. Its main goal is to enable high performance applications on the Web, but it does not make any Web-specific assumptions or provide Web-specific features, so it can be employed in other environments as well.

WebAssembly est un standard proposé par le W3C pensé pour être l’assembleur du web. Son développement est réalisé par un comité du W3C composé de représentants des principaux acteurs du web actuel en tant que plateforme d’exécution : Mozilla, Google, Apple et enfin Microsoft.

WebAssembly répond à deux problématiques bien distinctes en proposant :

  1. une stack machine portable et abstraite de façon à pouvoir développer dans plusieurs langages haut niveau
  2. de meilleures performances grâce à une exécution plus efficace d’instructions compilées et donc plus légères dans un écosystème ou les applications web sont de plus en plus nombreuses et complexes et donc gourmandes en ressources client.

Pour fonctionner wasm repose sur un format binaire et une instruction homologue de type assembleur, sous forme de texte. Elles forment un langage intermédiaire (intermediary language, ou IL), à la façon du CIL du monde .NET. La page Wikipédia de wasm donne un exemple d’équivalence entre une série d’instructions en C et leur compilation en wasm. Le tout s’exécute dans une sandbox du navigateur. A ce jour les principaux navigateurs supportent nativement wasm : Chrome, Firefox, Safari, Opéra et Edge. On peut également utiliser un polyfill pour étendre ce support aux versions plus anciennes de ces navigateurs.

WebAssembly n’a pas vocation à remplacer le monde Javascript, mais plutôt à le compléter. En effet, il est parfaitement possible d’écrire des applications hybrides : en partie en Javascript/Typescript, en partie en wasm, les deux étant interopérables. On peut donc appeler des fonctions Javascript depuis wasm et appeler des fonctions wasm depuis Javascript.

Notons au passage que l’environnement d’exécution est totalement agnostique du navigateur et des particularités du web. Il pourra donc s’exécuter dans d’autres contextes que celui d’un navigateur.

L’être humain, de son côté, rechignant étrangement à coder en assembleur, il est possible de coder ses applications en langage de haut niveau, dont le code sera compilé en IL wasm. Les premiers langages proposant cette compilation sont C, C++ et Rust. La bonne nouvelle, c’est que cette liste s’allonge avec le temps. Une liste est maintenue à jour sur Github, et l’on peut voir que des langages managés tels que Java et C# ont fait leur apparition.

Le workflow ressemble donc énormément à celui qu’on peut connaître dans le monde .NET :

Le développeur développe dans un langage haut niveau, qui est compilé en IL. Cet IL est interprété via un compilateur JIT, compilé en binaire et exécuté par le navigateur.

La promesse est donc forte : des applications web plus rapides, moins consommatrices de ressources et développées dans le langage de votre choix. Le standard n’en est encore qu’à ses prémices et la route vers sa finalisation et surtout son adoption générale est encore très longue. En attestent la roadmap et les prochaines évolutions.

Pour en savoir plus sur ce qu’est WebAssembly, le meilleur article que j’ai vu sur le sujet est écrit par Milica Mihajlija, stagiaire de son état chez Mozilla. Vraiment, lisez-le.

Blazor

Blazor est l’implémentation wasm de Microsoft. Ou du moins son embryon. Le nom vient de la contraction de “browser” et “Razor”, le moteur de templating HTML que les développeurs ASP.NET connaissent bien.

Pour créer votre première application Blazor, plusieurs possibilités s’offrent à vous, sachant que dans tous les cas, vous aurez besoin d’une version à jour du SDK .NET Core (2.1.302 et plus récent) et des templates Blazor à jour (dotnet new -i Microsoft.AspNetCore.Blazor.Templates) :

  • Visual Studio 15.7+
  • la CLI dotnet

J’ai une préférence personnelle pour la CLI dotnet. Donc pour générer notre première application Blazor, on exécute la commande suivante :

dotnet new blazor

Cela nous génère la structure suivante :

Les développeurs .NET ne devraient pas se retrouver trop perdus. Le démarrage de l’application est assuré par la méthode Main() de Program.cs, qui appelle la classe Startup()

Les bonnes idées de .NET Core sont bien présentes : middleware, injection de dépendances, etc. Et le reste ressemble beaucoup aux récentes Razor Pages d’ASP.NET Core. Donc partage de composants graphiques communs dans Shared, layouts. Le code d’une application Blazor est compilé dans une assembly .NET Standard 2+ :

Et maintenant, vous vous demandez logiquement comment ce code .NET Standard est exécuté par le navigateur. Et bien, c’est Mono qui se charge de l’Interop entre le navigateur et les assemblies .NET Standard :

Jetons maintenant un œil à une des pages de l’application :

La première ligne contient le chemin relatif vers cette page. L’URL de cette page est donc [racine de mon application]/counter :

Ensuite, on retrouve la syntaxe Razor classique : du HTML et des éléments dynamiques préfixés par @. Les fonctions Blazor sont placées dans un bloc @functions, tout comme vous pourriez placer vos fonctions Javascript dans une page classique.

Ce qui donne le résultat suivant :

Dans cette autre page, on notera plusieurs éléments intéressants. Son fonctionnement est simple : à l’aide du HttpClient, on appelle une URL en GET pour récupérer un payload JSon à partir duquel on peuple un tableau de WeatherForecast. Tant que ce tableau est vide, on affiche “Loading…” et une fois que le tableau est peuplé, il est affiche dans une table HTML. Rien d’extraordinaire. La mécanique est très proche de ce qu’on retrouve dans une SPA Javascript. Néanmoins, on voit qu’on surcharge la méthode OnInitAsync de la classe Page. Les amateurs de WebForms y verront la résurgence du bon vieux page lifecycle. En effet, Blazor expose plusieurs hooks pour effectuer des actions pendant la génération de la page côté client. Et à chaque fois, les expressions sont réévaluées. Les différentes étapes du cycle de vie de la page sont :

  1. OnInit()
  2. OnInitAsync()
  3. OnParametersSet()
  4. OnParametersSetAsync()
  5. ShouldRender()
  6. OnAfterRender()
  7. OnAfterRenderAsync()

Interop

Comme dit plus haut, wasm n’a pas vocation à remplacer Javascript, mais à le compléter dans un certain nombre de scénarios. Il serait donc plutôt utile de pouvoir appeler des librairies JS depuis Blazor, et inversement. Eh bien, figurez-vous que c’est possible. Ce n’est pas encore bien sec, mais c’est autorisé par la machine virtuelle du navigateur. Il y a même un namespace dédié : Microsoft.JSInterop. Une fonction Blazor peut appeler une fonction JS à l’aide de la méthode JSRuntime.Current.InvokeAsync. Ex :

if (await JSRuntime.Current.InvokeAsync<bool>("say", "Hello"))         {             
// This line will be reached as our `say` function returns true
Console.WriteLine("Returned true");
}

A l’inverse, on peut appeler du code Blazor depuis JS. Pour cela, il faut déclarer cette méthode comme invoquable à l’aide de l’attribut [JSInvokable]

[JSInvokable]
public static Task<string> GetUserName(int id)
{
User user = GetUserById(id);
return user.Name;
}

Du côté Javascript, il suffit de déléguer l’appel à la librairie JS DotNet :

DotNet.invokeMethodAsync(assemblyName, 'GetUserName').then(data => ...)

La solution est plutôt simple si on se cantonne à appeler des méthodes statiques comme ci-dessus. Malheureusement (en tout cas, je l’espère), vous travaillez plus souvent avec des instances que des statiques. Et si c’est bien le cas, ça va vous demander un peu de travail. En effet, il faudra encapsuler votre instance à l’aide de la classe DotNetObjectRef. Dans tous les cas, la doc officielle est bien fichue. Et on trouve pas mal d’infos sur le sujet sur Learn Blazor aussi.

Déploiement et footprint

Pour manipuler une application Blazor, on suit la même logique qu’avec les applications .NET Core. On restore les packages Nuget avec dotnet restore, on compile avec dotnet build, on exécute en local avec dotnet run.

Pour déployer, on utilise la commande dotnet publish. Voici un exemple de commande pour déployer en configuration Release dans un répertoire donné :

dotnet publish -c Release -o ..\out

Cela génère le répertoire d’artefacts suivant :

Les choses intéressantes sont dans le répertoire dist :

  • A sa racine, on trouve les fichiers qui étaient dans le répertoire wwwroot de la solution
  • Le résultat de la compilation est dans dist/_framework

Le répertoire asmjs assure la rétrocompatibilité avec les navigateurs ne supportant pas wasm. Ce qui est bon signe, c’est de noter qu’il pèse 7Mo, alors que le répertoire wasm ne pèse que 2Mo. C’est ici qu’on voie effectivement que c’est Mono qui s’occupe de faire le pont entre la machine virtuelle du navigateur et notre code compilé.

Par contre, pour faire tourner du .NET dans le navigateur, il faut bien embarquer une partie du framework. C’est pour ça qu’on voit les bonnes vieilles assemblies dont le bien-connu mscorlib.dll.

Tout ceci nous donne une emprunte de plus de 12Mo pour un Hello World, que le navigateur doit télécharger à la première connexion à notre URL pour faire tourner notre application. C’est beaucoup. Un gros effort devra être fait d’ici la v1 de Blazor pour alléger cette emprunte. Parmi les pistes possibles : une cure d’amaigrissement du runtime, ou encore le faire télécharger par le navigateur via un Linker et du lazy loading comme peut le faire par exemple Webpack.

Maintenant que notre application est prête à être déployée, il ne reste plus qu’à lui trouver un nid douillet. Et n’importe quel hébergement sachant servir des fichiers statiques peut faire l’affaire. Même le Blob Storage sur Azure :

Le résultat est accessible à cette URL : https://dotnetconfblazor.z5.web.core.windows.net/

Conclusion

Le web bouge énormément. Malgré tout, les fondamentaux HTML, Javascript, CSS, eux, sont stables depuis 20 ans. On a évidemment connu des tentatives d’amener des langages de plus haut niveau dans le navigateur (coucou Flash et Silverlight). Mais elles ont échoué car elles étaient propriétaires et s’exécutaient à côté du navigateur via des plugins.

WebAssembly pourrait, et selon moi va, radicalement changer la face du web tel qu’on le connait. Il est déjà possible de faire tourner des applications complexes dans un navigateur. Mais à quel prix ? L’infrastructure n’est pas pensée dans ce sens. WebAssembly vient vraiment changer la donne. Un standard W3C, soutenu par les GAFAM pourrait réussir là où des expériences propriétaires plus ou moins malheureuses ont échoué sur le long terme.

Et Microsoft prend l’aspiration. Non seulement l’éditeur soutient et participe au développement du standard, mais il propose aussi logiquement son framework, en amenant plus de 20 ans d’expérience dans l’outillage des développeurs web, d’ASP 1.0 à ASP.NET Core. Blazor mérite donc une réelle attention. Evidemment, rien n’est sec. Chaque version mineure vient avec son lot de breaking changes. Mais c’est prometteur. On prend les bonnes pratiques du web moderne, on gomme certains défauts intrinsèques à Javascript. On ne sait pas vraiment où on va, mais ça pourrait bien nous donner des choses très chouettes.

Bonus track

La vidéo de ma session à la DotNetConf qui raconte grosso modo la même chose (avec du détail en moins et des tics de langage en plus)