WASM côté serveur
Pendant longtemps, WebAssembly (WASM) a été perçu comme une technologie de navigateur, destinée à faire tourner des jeux ou des applications lourdes dans la page web. Mais depuis quelques années, une révolution plus discrète s’opère : WASM comme runtime serveur. L’idée est simple et radicale à la fois : compiler votre code une seule fois dans un format portable et sûr, puis l’exécuter n’importe où, y compris là où il n’y a pas de navigateur.
Dans cet article, je vais explorer pourquoi on voudrait exécuter du WebAssembly côté serveur, comment fonctionne le modèle d’exécution, où il brille, où il est encore immature, et comment l’utiliser concrètement. C’est le premier volet : je consacrerai un prochain article à WASI, l’interface système qui rend le WASM serveur vraiment utile.
Pourquoi exécuter du WASM côté serveur ?
L’argument “pourquoi se compliquer la vie avec WASM quand on a déjà Docker“ mérite une réponse. Les raisons sont multiples et sérieuses.
La portabilité de langage. Avec WASM, vous compilez une fois et vous exécutez partout. Que votre service soit écrit en Rust, C, Go, Zig ou même en Python ou TypeScript compilé, le résultat est un module .wasm binaire, portable, qui tourne sur n’importe quel runtime WASM, quel que soit le système d’exploitation sous-jacent. C’est un peu le rêve du compile once, run anywhere, mais au niveau du bytecode, pas du système.
La sandbox et la sécurité. Un module WASM s’exécute dans une sandbox avec un modèle de permissions strict. Par défaut, il ne peut pas accéder au système de fichiers, au réseau, à l’environnement, aux processus. Il ne fait que des calculs purs sur sa mémoire linéaire isolée. Pour lui donner accès à une ressource, l’hôte doit explicitement l’y autoriser. C’est une surface d’attaque beaucoup plus réduite qu’un container Docker, qui partage le noyau hôte.
La vitesse de démarrage, le cold start. C’est l’argument le plus important pour le edge computing. Un container peut mettre des centaines de millisecondes, parfois plusieurs secondes, à démarrer et à faire son healthcheck. Un module WASM démarre en quelques millisecondes. Dans un contexte serverless ou edge, où chaque requête peut tomber sur une instance au froid, cette différence est énorme.
La déterminisme. Un module WASM s’exécute de manière reproductible quel que soit l’hôte. Pas de différences subtiles entre environnements de dev et de prod, pas de surprises liées à la version du système. Vous testez le même binaire partout.
La portabilité entre edge providers. C’est peut-être la qualité la plus stratégique. Un module WASM compilé une fois tourne sur Cloudflare Workers, Fastly Compute, Fermyon Spin, ou dans votre propre runtime. Vous n’êtes plus verrouillé à un fournisseur edge : le même artefact se déploie partout. Avec le cloud, vous êtes souvent prisonnier ; avec WASM, le binaire vous suit.
Le modèle d’exécution : comment ça marche
Un module WASM n’est pas un exécutable autonome. C’est un bytecode compact, conçu pour une exécution rapide et prévisible, qui tourne dans une machine virtuelle. Je vais décrire le modèle en termes concrets.
- Mémoire linéaire. Le module expose une mémoire contiguë, adressable par des index. On y place les données (entrées, buffers, résultats). L’hôte et le module communiquent par cette mémoire.
- Fonctions exportées. Le module exporte des fonctions qui peuvent être appelées depuis l’hôte. C’est l’interface avec le monde extérieur.
- Fonctions importées. Le module peut importer des fonctions fournies par l’hôte (par exemple, une fonction de log, ou un appel système exposé par WASI). C’est par ces imports que le module accède aux ressources du système.
- Pas d’appels système directs. Le module ne voit pas le système d’exploitation. Il voit uniquement ce que l’hôte choisit de lui exposer. Tout est sous contrôle.
Ce modèle est fondamentalement différent de celui d’un container. Un container embarque tout le système de fichiers, les bibliothèques, un init, et partage le noyau de l’hôte avec des mécanismes d’isolation (namespaces, cgroups). Un module WASM n’embarque que le code et ses données, et ne voit jamais le noyau : il n’existe que la mémoire linéaire et les fonctions.
Cold start : comparons
La vitesse de démarrage est l’argument marketing le plus fort, mais il faut le quantifier pour être crédible. Établissons des ordres de grandeur réalistes en 2026.
| Technologie | Cold start typique | Modèle |
|---|---|---|
| Container prêt (déjà téléchargé) | 100 ms à 5 s | Image téléchargée, noyau + init lancés |
| Container à froid (image à tirer) | 5 s à 30 s+ | Pull de l’image + démarrage |
| Lambda / serverless | 100 ms à 1 s (selon langage) | Runtime plus ou moins cahud |
| Module WASM | 1 ms à 10 ms | Bytecode validé + interprété ou JIT |
La différence ne se joue pas sur des pourcentages, mais sur plusieurs ordres de grandeur. Pour un service edge qui reçoit des pics de requêtes et doit répondre en quelques millisecondes, un cold start de 5 ms est invisible, alors qu’un cold start de 1 seconde fait grimper la latence du 95e percentile.
C’est particulièrement précieux pour le serverless : le modèle de facturation au temps d’exécution récompense les runtimes qui démarrent vite et libèrent vite les ressources. WASM s’y prête à merveille.
Un exemple concret : compiler un module WASM
Rien ne vaut un exemple. Je vais écrire un petit bout de code dans ce qui est probablement le langage le plus naturel pour WASM, Rust, puis le compiler en module .wasm.
|
La fonction add est simple : elle additionne deux entiers et retourne le résultat. L’attribut #[no_mangle] et la convention extern "C" garantissent que le symbole reste exporté et accessible depuis l’hôte.
On compile pour la cible WASM :
rustup target add wasm32-unknown-unknown |
Le résultat est un fichier binaire lib.wasm. On peut le regarder d’un peu plus près, car c’est un format très lisible. Voici l’équivalent en texte .wat (WebAssembly Text format), qui montre la véritable structure :
(module |
On voit la structure : une fonction, deux paramètres i32, une valeur de retour i32, et une seule instruction i32.add. C’est un bytecode de plus bas niveau qu’une JVM, avec des types numériques explicites et une absence totale d’appels système.
Exécuter le module depuis Node.js
Maintenant, exécutons ce module depuis un hôte Node.js. Node.js embarque un moteur WASM intégré via l’API WebAssembly, ce qui nous évite d’installer un runtime externe.
import { readFile } from 'node:fs/promises'; |
Trois lignes essentielles. On lit le fichier binaire, on instancie le module via l’API native, puis on appelle la fonction exportée add. Node.js fournit tout, sans dépendance.
Pour des usages plus avancés, notamment l’accès aux ressources système via WASI, on basculera sur wasmtime (le runtime officiel de Bytecode Alliance) ou sur l’emballage Node de WASI. Voici un aperçu avec wasmtime :
wasmtime run lib.wasm |
Et côté Node, pour les cas qui nécessitent WASI, on utilisera le package @wasmer/wasi ou le runtime Wasmtime embeddable. J’y reviendrai en détail dans l’article dédié à WASI, car c’est là que les choses deviennent réellement intéressantes pour un serveur.
Comparaison : WASM vs containers
Le parallèle avec les containers est inévitable, car ils occupent le même créneau : empaqueter et exécuter du code de manière portable et isolée. Le tableau suivant met les différences en regard.
| Critère | Container (Docker) | Module WASM |
|---|---|---|
| Contenu empaqueté | OS + runtime + libs + code | Code + données uniquement |
| Taille typique | 50 Mo à 2 Go | 1 Ko à 10 Mo |
| Isolément | Namespaces + cgroups, partage du noyau | Sandbox, mémoire linéaire |
| Accès à l’OS | Par défaut large (root, mounts) | Par défaut aucun (explicite) |
| Démarrage | 100 ms à plusieurs s | 1 ms à 10 ms |
| Portabilité | Forte, mais dépend de l’image/hôte | Très forte, binaire universel |
| Langages | n’importe quel binaire | Ceux qui compilent vers WASM |
| Syscalls | Réels, directement au noyau | Aucuns, via imports/WASI |
La distinction cruciale est celle du modèle de confiance. Un container repose sur les mécanismes de sécurité du noyau Linux, qui ont régulièrement montré leurs failles (les fameux container escape). Un module WASM n’expose pas le noyau du tout : la surface d’attaque se réduit au runtime WASM lui-même, qui est bien plus petit et audité.
Pour autant, je ne dirais pas que WASM remplace les containers partout. Un container peut exécuter n’importe quel processus avec un accès complet au système, ce qui reste irremplaçable pour les workloads complexes (base de données, traitement longue durée avec accès complet au matériel). WASM est idéal pour du calcul, de la logique applicative, de l’API edge, du traitement de requêtes, de la transformation de données.
Les fournisseurs edge et l’écosystème
L’écosystème a considérablement mûri. Les grands acteurs du edge computing ont adopté WASM comme fondation, chacun à sa manière.
- Cloudflare Workers : exécute les workers sur WASM, avec un modèle de pay-per-request très agressif. Vous pouvez écrire en JavaScript/TypeScript, Rust, C, Go, Python ou assembleur WASM directement.
- Fastly Compute : anciennement Compute@Edge, s’appuie fortement sur WASM et le runtime Lucya. Très performant, pensé pour la couche edge.
- Fermyon Spin : un framework développeur qui orchestre des composants WASM et cache les modules pour un démarrage quasi instantané.
- Wasmtime et Wasmer : les runtimes généralistes, que vous pouvez embarquer dans votre propre serveur, en Node, en Rust, en Python…
Cette unanimité autour de WASM est ce qui rend le concept si puissant : le même module .wasm se déploie sur tous ces environnements sans recompilation. Vous écrivez votre logique une fois, vous la testez localement avec Wasmtime, et vous la poussez sur n’importe quel provider edge, ou vous l’embarquez dans votre propre service.
Les limites à connaître
Je ne voudrais pas vendre WASM comme une solution magique. Il faut être pragmatique.
La simplicité d’accès aux ressources. Sans WASI, un module WASM ne peut pas faire grand-chose de plus que du calcul pur. Même avec WASI, le modèle d’accès aux systèmes de fichiers et au réseau est plus verbeux qu’en natif. Pour un développeur habitué à fs.readFile en Node, ça va faire bizarre.
La maturité des bibliothèques. L’écosystème WASM serveur est jeune comparé à celui de Node ou des containers. Certaines bibliothèques compilent, d’autres pas, et parfois il faut adapter le code (notamment pour les fonctions qui dépendent de libc ou de threads).
La gestion des appels asynchrones et des évènements. Le modèle WASM est historiquement synchrone et sans fils d’exécution natifs. Les choses évoluent (threads, exceptions), mais certains patterns serveur asynchrones restent moins naturels à exprimer.
L’accessibilité au réseau et aux sockets : c’est le point le moins mature. Le réseau sortant via WASI est encore en cours de stabilisation (wasi-sockets), et le support varie selon les runtimes. Pour un service qui fait beaucoup d’appels réseau sortants, attendez-vous à des frictions.
Conclusion
WebAssembly côté serveur n’est plus une expérience de laboratoire : c’est une solution réelle, utilisée en production par les plus gros acteurs du edge. Ses atouts sont indiscutables dans les bons cas d’usage : portabilité totale, sandbox par défaut, démarrage en quelques millisecondes, indépendance vis-à-vis des fournisseurs.
Ce n’est pas un remplaçant systématique des containers, mais un outil complémentaire, particulièrement adapté au calcul, à la logique applicative et au edge computing. Le jour où vous devrez déployer la même logique sur plusieurs providers edge sans vous enfermer, ou où vous voudrez un cold start quasi nul, WASM devient soudainement l’option la plus évidente.
Dans le prochain article, je creuserai WASI : l’interface système qui donne à ces modules l’accès au système de fichiers, aux variables d’environnement, à l’horloge et au réseau, et je l’expliquerai du point de vue d’un développeur Java ou Node. Car c’est là que le WASM serveur cesse d’être un simple calcul isolé pour devenir une véritable alternative exécutable.