Navigateur Web
Web Assembly avec JavaScript et AssemblyScript
Il est possible aujourd'hui de faire du code qui sera compilé en WebAssembly de plusieurs manières.
Les deux manières les plus évidentes sont :
- Coder dans un langage bas niveau existant (Go, Rust, C++, etc.) pour ensuite générer un binaire (
wasm
) à l'aide d'outils spécifiques à chaque langage. - Utiliser le langage « texte » du WebAssembly (
wat
) pour le compiler directement en binaire (wasm
).
Nous allons aborder une approche intermédiaire, car nous allons utiliser AssemblyScript, un langage basé sur TypeScript conçu pour être compilé en WebAssembly.
Présentation
L'AssemblyScript est un langage de programmation open source et communautaire qui offre une optimisation pour WebAssembly. Il est de typage fort et présente de nombreuses similitudes avec le TypeScript, ce qui en fait un outil très intuitif sans avoir besoin de maîtriser un nouveau langage de programmation.
AssemblyScript respecte les spécifications du WebAssembly et se base sur binaryen, un compilateur rapide et performant en WebAssembly codé en C++.
Il ne faut pas oublier que JavaScript demeure indispensable dans un navigateur, car même le WebAssembly a besoin de lui pour interagir.
Compatibilité
L'AssemblyScript est compatible avec la plupart des navigateurs, mais aussi avec Node.js, Wasmtime et Wasmer.
Premier projet
Compilation
Il faut bien comprendre que lors de la compilation, l’AssemblyScript ne se compile pas directement en wasm
. Il faut plutôt le voir comme une transpilation en code wat
(similaire à la commande tsc
, qui transpile le TypeScript en JavaScript), qui est ensuite compilé en wasm
.
Options de compilations
Les options de compilation sont variées et peuvent se passer soit en argument de la commande de compilation (asc
), soit dans un fichier asconfig.json
à la racine du projet.
Les principales options de compilation sont les suivantes :
Optimisation :
--optimizeLevel
- Niveau d’optimisation du code (0-3), indique le niveau d’optimisation du code (au détriment de la taille du binaire), 3 étant le niveau d’optimisation le plus haut.
--shrinkLevel
- Indique à quel point le compilateur va tenter de réduire la taille du binaire (0-2).
- ⚠️ Il et déconseillé de mettre une valeur haute à ce paramètre si vous avez également mis des options d’optimisation du code.
Sortie :
--outFile
- Chemin de sortie du fichier
wasm
- Chemin de sortie du fichier
--textFile
- Chemin de sortie du fichier
wat
- Chemin de sortie du fichier
Débogage :
--debug
- Ajoute des informations de debug dans la version compilée (peut entrainer une réduction des performances)
Fichier de configuration
Les options mentionnées ci-dessus peuvent être spécifiées directement dans le fichier asconfig.json
de la façon suivante :
Binding & typage
La commande asc
non seulement compile notre code, mais génère aussi du typage pour permettre d'utiliser le code AssemblyScript depuis du code JavaScript ou TypeScript avec de l'autocomplétion et une certaine sécurité. Cela facilite considérablement le transfert entre les deux mondes.
Limitations
Typage
Il n'y a pas de cast "intelligent", les types entiers et flottants sont donc tous convertis en number
pour le TypeScript ou le JavaScript. Même si la plupart des types du TypeScript sont supportés, certains ne le sont pas.
Effets de bords
Il n'est pas possible d'accéder au DOM ou au réseau.
Code synchrone
Les fonctions asynchrones, intervalles, timer, timeout et observables sont à oublier : l'AssemblyScript est un langage synchrone.
Langage statique
Ce n’est pas un langage dynamique comme le TypeScript, on veut, pour que le WebAssembly soit efficace, que tout, soit explicite (les type, les retours). any, undefined, unknown
On peut avoir du typage alternatif, dans la limite du raisonnable :
Performances
Nous ne faisons pas le choix d'utiliser le WebAssembly à la légère.
En général, nous recherchons des performances accrues ou une sécurisation de notre code pour notre application web.
Nous allons tenter de déterminer dans quels cas l'utilisation du WebAssembly pourrait s'avérer être judicieuse, ou pas.
Benchmark
Ensemble de Mandelbrot
L’ensemble de Mandelbrot est une fractale composée d’un ensemble de points du plan complexe qui a pour formule :
Cette formule permet de générer des images représentant le parcours des nombres complexes sur une région carrée de son plan. Ces images sont très élaborées et constituent un terrain de jeu parfait pour tester le calcul et le rendu de certains programmes.
Exemple d’un ensemble de Mandlebrot
Grâce au projet disponible sur “https://github.com/ColinEberhardt/wasm-mandelbrot”, nous avons différentes implémentations de ce rendu, dont une en JavaScript pur et une en AssemblyScript, avec le minimum d'adaptation.
Si nous testons le calcul de rendu x10 (pour mettre à l'épreuve notre PC Patate), nous observons les résultats suivants :
Rendu de l’ensemble de Mandelbrot AssemblyScript |
Rendu de l’ensemble de Mandelbrot JavaScript |
Et là, surprise, aucune différence notable n'apparaît.
L'explication vient du fait que l'ensemble de Mandelbrot se génère par des calculs avec essentiellement des nombres à virgule flottante. Il se trouve qu'en JavaScript, le type number
est un flottant et le JavaScript n'a aucune peine à faire des calculs sur ce type, car il est optimisé pour. Dans le cas d'AssemblyScript, comme le code utilisé est le même que celui en JavaScript avec le minimum d'adaptation, il n'y a pas de miracle : les opérations sur les nombres flottants en WebAssembly ne sont pas aussi optimisées.
Résultat : le JavaScript se débrouille mieux dans cette situation.
Il est possible de trouver des versions de l'ensemble de Mandelbrot plus performantes en AssemblyScript, si le code de ce dernier a été pensé pour le langage et n'est pas une simple "adaptation" d'un code existant dans un langage tel que JavaScript.
Tri des couleurs sur une image
Tournons-nous vers un autre projet, celui-ci : “https://github.com/manueldois/WebAssembly”.
Ce projet consiste à générer une image contenant des couleurs aléatoires, déterminées par une seed
donnée. Le test de performance va consister à observer le temps que prend le programme pour trier chaque pixel de l’image afin d'obtenir un rendu comme celui ci-dessous :
La seed
utilisée sera le mot “ESGI”, observons le temps que prend chaque implémentation pour trier les pixels de notre image :
|
|
Ici, le résultat est sans appel. L'implémentation en AssemblyScript est 4,5 fois plus rapide que celle en JavaScript.
Une image n'est, en fin de compte, qu'un tableau de pixels ayant une valeur représentant sa couleur. Le WebAssembly est tout à fait adapté à ce type d'utilisation.
Gestion des nombres et décalage de bits
Là où l'AssemblyScript se démarque, c'est dans la manipulation des nombres. En JavaScript, on utilise le type number
, qui est un nombre à virgule flottante sur 64 bits, ou bien bigint
(très rare).
En AssemblyScript, on a des types qui se calquent sur le WebAssembly, qui permettent de manipuler des nombres entiers sur 32, 64 bits, signés, non signés… (i32
, u32
, i64
, u64
…). Ces types sont bien gérés par le langage. On peut également utiliser des opérateurs logiques (bitwise operator).
Ce n'est pas quelque chose que l'on retrouve souvent dans du code métier, mais c'est un moyen d'optimiser son code. En JavaScript, cela fonctionne très mal (et c'est lent !)… La limitation vient du fait que cela ne fonctionne que sur les 32 premiers bits d'un nombre.
C'est juste inutilisable... À l'opposé de l'AssemblyScript qui gère ça très bien !
De nombreux algorithmes peuvent bénéficier de cette optimisation. Prenez l'exemple d'une IA.
Dans le cas d'un apprentissage d'un jeu d'échecs, il est possible de représenter en mémoire de manière très optimisée un échiquier, en utilisant les décalages de bits. On peut ainsi représenter l'état d'un échiquier avec seulement une douzaine d'entiers sur 64 bits.
Cette méthode s'appelle Bitboards. Vous pouvez trouver plus d'informations à ce sujet sur ce site : “https://www.chessprogramming.org/Bitboards”
En utilisant ce principe, nous pouvons effectuer un benchmark pour calculer le nombre de coups possibles sur un échiquier après N tours.
Si nous jouons 5 tours, le nombre de possibilités s’élève à 4 865 609 ! Pour calculer cela, nous obtenons les résultats suivants :
- Version JavaScript optimisée au mieux : 14 680 ms
- Version AssemblyScript utilisant les Bitboards : 1220 ms
CQFD.
Conclusion
AssemblyScript est super pour se mettre au WebAssembly, mais exige de revoir sa façon de coder pour obtenir des performances convaincantes et ne pas se contenter de « transposer » son code TypeScript en AssemblyScript. On y gagne rarement.
Il faut aussi savoir être pertinent ! Ce n'est pas parce qu'on va adapter son code (même bien) que cela sera forcément plus performant. Il faut bien étudier le besoin et ne pas hésiter à faire des benchmarks pour déterminer si le passage par le WebAssembly n'est pas une fausse bonne idée.
Ressources
https://www.assemblyscript.org/introduction.html
https://manueldois.github.io/WebAssembly/Sort Colors Benchmark/dist/index.html
youtube.com/watch?v=3KuDtqFxvRg
youtube.com/watch?v=gCT9ebtTzqw
youtube.com/watch?v=E0Z5oRq8APs
https://manueldois.github.io/WebAssembly/Sort Colors Benchmark/dist/index.html
https://colineberhardt.github.io/wasm-mandelbrot/#AssemblyScript
https://nischayv.github.io/as-benchmarks/graphics/fire/dist/index.html
https://www.chessprogramming.org/Bitboards
Framework Frontend
Comment lancer des applications Rust
Dans la vidéo suivante, nous allons utiliser des frameworks réalisés en Rust. Il est donc important d'installer le langage et d'en apprendre les bases. Attention, c'est un langage assez complexe. Ne vous inquiétez pas si vous ne comprenez pas tout : le compilateur vous expliquera vos erreurs lorsque vous écrirez du code.
Installer Rust
Ajouter la destination de compilation
Analysons le nom de cette destination de compilation :
- WASM32 : WASM pour Webassembly et 32 pour dire que l'espace mémoire est sur 32 bits.
- Le premier unknown de la target wasm32-**unknown-**unknown concerne la source de la compilation, la machine qui a généré le binaire. Dans notre cas, elle est inconnue, premièrement parce que toutes les architectures peuvent compiler vers le WASM et aussi parce que la compilation n'est pas signée. Une fois le binaire créé, il n'est pas possible de savoir depuis quelle architecture le binaire a été compilé.
- Le deuxième unknown : wasm32-unknown-unknown concerne la destination de compilation, ici encore une fois, on ne sait pas dans quel contexte le binaire va être exécuté. Cependant, il existe des discussions en cours. Premièrement, vu les différents usages du WebAssembly, il est pertinent de vouloir différencier les targets en fonction des usages. Et pour cela, certains voudraient ajouter des destinations de compilation :
- wasm32-npm-unknown (pour une version compatible avec npm)
- wasm32-node-unknown
- wasm32-web-unknown
À noter qu'il existe une destination de compilation wasm-wasi
qui est utilisée par le projet wasmtime. Elle est un standard du WASI et est adaptée à l'exécution côté serveur d'applications 100% WebAssembly. Elle a la particularité de pouvoir compiler en créant plusieurs fichiers binaires, contrairement à wasm32-unknown-unknown.
Installer trunk
Trunk est un outil de gestion d'applications très répandu sur le web. Il permet de lancer et d'orchestrer des outils qui interviennent pendant la création d'un projet web, allant de la compilation de SASS à la compression d'image et à la compilation de fichiers Rust en WebAssembly.
Il existe plusieurs méthodes pour l'installer. Maintenant que vous avez installé Rust, je vous recommande d'utiliser Cargo, car cette commande fonctionnera quel que soit votre système d'exploitation et, étant donné que Trunk est un outil spécifique à Rust, il est logique qu'il soit désinstallé lorsque vous désinstallerez Rust.
Spécificité Mac
Pour les utilisateurs de la plateforme Apple Silicon, le module wasm bindgen-cli
n'est pas installé correctement avec le tronc actuellement. Le paquet wasm bindgen
est un outil qui permet de générer des liens (des ponts) entre le code Rust et du code JavaScript qui servira de connexion avec le navigateur.
La commande pour installer wasm bindgen-cli est la suivante :
Instruction spécifique a la version nightly
Il existe plusieurs versions de Rust, également appelées canaux de diffusion. Il y en a 3 principaux :
- Stable
- Beta
- Nightly
Rust ajoute des fonctionnalités en continu et n'a pas prévu de régressions. Il utilise le sémantique versionning. Aujourd'hui, aucun changement de version majeur n'est prévu.
La version stable de Rust est mise à jour toutes les 6 semaines. La version bêta représente la prochaine version stable.
Rust utilise une technique appelée "feature flag" pour déterminer si une fonctionnalité est active sur une version donnée. Si vous souhaitez utiliser les fonctionnalités non stables de Rust, vous devrez utiliser le canal Nightly et activer la fonctionnalité. C'est ce que fait le framework Leptos.
Commande pour installer le channel Nightly
Commande pour activer Rust Nightly
Commande pour lister les channels installées
Commande pour lister les channels installées
En vérité, on liste les toolchains, mais je ne vais pas faire la distinction entre toolchain et channel.
Une fois passé à la version Nightly, je vous recommande de réinstaller la cible wasm32-unknown-unknown pour avoir la dernière version.
Démonstration
Type de rendu
Lors du développement d'une application frontend, il est important de prendre en compte ces différents enjeux lors du choix du type de rendu pour votre projet de développement afin de trouver l'équilibre approprié entre performance, charge sur les serveurs, répartition géographique des serveurs et SEO.
Le type de rendu désigne la façon dont les données sont présentées à l'utilisateur final, comment la page est construite et affichée. Il existe plusieurs solutions :
Le Client side rendering (CSR), dans lequel l'application est construite par le navigateur. Le serveur ne fait que renvoyer le code nécessaire au navigateur pour construire la page. C'est le code JavaScript qui va ensuite aller chercher la donnée et l'afficher dans la page.
Le Server side rendering (SSR) est un mode de rendu dans lequel le serveur va récupérer les données et construire la page avec ces données, puis l'envoyer au navigateur sous forme de package complet.
Le Static site generation (SSG) est le concept de générer des pages avec du rendu côté serveur pour des données peu souvent mises à jour, et de mettre ces pages en cache.
L'incremental static regeneration (ISR) est une forme de SSG dans laquelle on modifie les parties du cache sensibles aux changements plutôt que de régénérer tout le site.
Aujourd'hui, l'ISR est peu répandu et le CSR et le SSR ont des problèmes en termes de performance (SEO ou charge serveur). La génération d'un site statique est un processus qui peut être long, surtout si l'on veut construire des millions de pages. Heureusement, il existe Hugo (”https://gohugo.io/”) qui permet d'accélérer le processus de génération du site grâce à la gestion de la programmation concurrente du langage GO. Cependant, GO n'est pas un langage du web, et ici, on a dupliqué la manière de faire des rendus. Si cette logique pouvait être en WebAssembly, et donc disponible à la fois côté navigateur et côté serveur, on gagnerait énormément en performance et ouvrirait le web à de nouveaux usages. Pour cela, il faudrait néanmoins attendre que la gestion du DOM soit intégrée dans le WebAssembly. Ce qui n'est pas une priorité et qui risque de briser la nature du web et notamment la rétrocompatibilité. Cependant, peut-être verrons-nous arriver cette norme avec le HTML 7.
Ressources
https://docs.wasmtime.dev/wasm-rust.html
https://github.com/bytecodealliance/wasmtime/blob/main/docs/WASI-overview.md
https://github.com/rustwasm/team/issues/38