MFormations
Modern Frontend Engineering

Chapitre 15

Chapitre 15 — WebAssembly

> Exécuter du code natif dans le navigateur : performances, cas d'usage et architecture.

WebAssembly — Cours Complet

1. Pourquoi WebAssembly ?

1.1 Les limites historiques de JavaScript

JavaScript a été créé en 10 jours en 1995 par Brendan Eich. Son objectif initial : ajouter de l'interactivité basique aux pages web. Il n'a jamais été conçu pour :

  • Le traitement d'images ou de vidéos en temps réel
  • Le rendu 3D (WebGL a dû être ajouté comme extension)
  • Les calculs scientifiques ou cryptographiques
  • Les jeux AAA dans le navigateur
  • Les IDE complexes (VS Code était un exploit d'ingénierie)

Même avec les optimisations JIT (V8, SpiderMonkey), JS reste limité :

  • Typage dynamique : le JIT ne peut pas deviner tous les types
  • Garbage Collector imprévisible : pas adapté au temps réel
  • Pas de parallélisme réel (Web Workers = threads séparés, pas de shared memory avant SharedArrayBuffer)
  • Compilation et optimisation JIT chaudes : la performance est non déterministe
// Exemple : traitement d'une image pixel par pixel en JS
function grayscale(imageData) {
  const pixels = imageData.data;
  for (let i = 0; i < pixels.length; i += 4) {
    const gray = 0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2];
    pixels[i] = pixels[i + 1] = pixels[i + 2] = gray; // 3 assignments
  }
}
// Problème : millions d'itérations, pas de typage statique

1.2 L'évolution : asm.js → WASM

asm.js (2013) : sous-ensemble de JS que le navigateur peut compiler à l'avance (AOT). Mozilla a démontré qu'on pouvait exécuter Unreal Engine 4 dans le navigateur.

// asm.js — sous-ensemble JS optimisable AOT
function sum(a, b) {
  a = a | 0;  // annotation : a est un entier 32 bits
  b = b | 0;  // annotation : b est un entier 32 bits
  return (a + b) | 0;
}

WebAssembly (2017) : format binaire standardisé, pas un sous-ensemble de JS. Quatre équipes de navigateurs (Chrome, Firefox, Safari, Edge) se sont unies pour le créer.

JS (texte)                    WASM (binaire)
─────────────────────         ─────────────────────
function add(a, b) {          00 61 73 6d 01 00 00 00
  return a + b;                01 05 01 60 02 7f 7f 01 7f
}                              03 02 01 00 0a 09 01 07 00
                               20 00 20 01 6a 0b
├─ 10x plus lent que natif ┤  ├─ 1.2x à 2x natif ┤

2. Architecture de WebAssembly

2.1 Format binaire (.wasm)

Le format .wasm est un binaire compact conçu pour :

  • Chargement rapide (streaming compilation)
  • Taille réduite (souvent < 10 KB pour des modules simples)
  • Backward compatibility (versionné)
  • Validation statique (pas de code invalide)

Structure d'un module WASM :

Module WASM
├── Type section (types de fonctions)
├── Import section (fonctions/mémoires importées)
├── Function section (déclarations)
├── Memory section (taille de la mémoire linéaire)
├── Export section (fonctions/mémoires exportées)
├── Code section (bytecode)
└── Custom sections (debug, name, etc.)

2.2 Modules WASM

Un module WASM :

  • Est stateless : pas d'état entre les instanciations
  • Est sécurisé : pas d'accès au système, à la mémoire du processus, ou au DOM
  • Importe des fonctions depuis l'hôte (JS)
  • Exporte des fonctions, mémoires, tables ou globals
// Chargement d'un module WASM (browsers modernes)
const response = await fetch('module.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes, {
  env: {
    log: (msg) => console.log(msg),
    memory: new WebAssembly.Memory({ initial: 1 })
  }
});
instance.exports.myFunction();

2.3 Mémoire linéaire

WASM utilise un modèle mémoire simple : un tableau d'octets contigu (ArrayBuffer).

Mémoire linéaire WASM
┌─────────────────────────────────────────┐
│ 0x0000 │ 0x0004 │ 0x0008 │ ... │ 0xFFFF │
├─────────────────────────────────────────┤
│ Variables globales │ Tas │ Pile │ Données │
└─────────────────────────────────────────┘

Avantages :

  • Pas de segmentation, pas de pagination
  • L'hôte (JS) peut lire/écrire directement via DataView
  • Contrôle fin : malloc/free gérés par le langage source (Rust via allocator)
// Communication JS ↔ WASM via mémoire linéaire
const memory = new WebAssembly.Memory({ initial: 1 }); // 1 page = 64KB
const view = new Uint8Array(memory.buffer);

// JS écrit dans la mémoire
view[0] = 42;

// WASM lit la même mémoire et écrit le résultat
// export function process(offset: i32): void { ... }

// JS lit le résultat
const result = new Float64Array(memory.buffer, 0, 1)[0];

2.4 Table et fonctions indirectes

WASM supporte les appels de fonction indirects via une table de fonctions (call_indirect). Cela permet :

  • Les pointeurs de fonction
  • Le dispatch virtuel (polymorphisme)
  • Les callbacks
;; Table de fonctions
(table 2 funcref)
(elem (i32.const 0) $add $sub)

;; Appel indirect
(call_indirect (type $binop) (i32.const 0) (local.get $a) (local.get $b))

3. Langages compilables vers WASM

3.1 Tableau comparatif

LangageCompilateurTaille WASMPerformanceMaturité
Rustwasm-pack, cargoPetite (sans std)★★★★★Très mature
C/C++Emscripten, ClangMoyenne★★★★★Très mature
GoGo 1.11+Grande (runtime Go)★★★☆☆Mature
AssemblyScriptascPetite (TypeScript-like)★★★★☆Mature
ZigZig buildPetite★★★★★Émergent
KotlinKotlin/WASMGrande★★★☆☆Expérimental
PythonPyodideTrès grande (runtime)★★☆☆☆Niche

3.2 Rust → WASM (recommandé)

Rust est le langage le plus populaire pour WASM grâce à :

  • Zero-cost abstractions : pas de runtime
  • Ownership : pas de GC nécessaire
  • wasm-pack : outil tout-en-un pour builder, tester, publier
  • wasm-bindgen : pont JS/WASM généré automatiquement
# Installation
cargo install wasm-pack

# Créer un nouveau projet WASM
cargo new --lib mon-projet-wasm
cd mon-projet-wasm

# Ajouter wasm-bindgen
cargo add wasm-bindgen

# Builder pour le web
wasm-pack build --target web

Structure d'un projet Rust WASM :

// src/lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
    a + b
}

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Hello, {}!", name)
}

// Support des types complexes via serde
#[wasm_bindgen]
pub fn process_image(data: &[u8], width: u32, height: u32) -> Vec<u8> {
    // Traitement performant sans overhead GC
    let mut result = Vec::with_capacity(data.len());
    for chunk in data.chunks(4) {
        let r = chunk[0] as f32;
        let g = chunk[1] as f32;
        let b = chunk[2] as f32;
        let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
        result.push(gray);
        result.push(gray);
        result.push(gray);
        result.push(chunk[3]); // alpha
    }
    result
}

3.3 C/C++ → WASM via Emscripten

# Installer Emscripten
git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

# Compiler
emcc main.c -o main.js -s WASM=1 -O3

# Avec SDL pour un jeu
emcc game.c -o game.html -s USE_SDL=2 --preload-file assets

3.4 Go → WASM

package main

import "syscall/js"

func add(this js.Value, args []js.Value) interface{} {
    return js.ValueOf(args[0].Int() + args[1].Int())
}

func main() {
    c := make(chan struct{}, 0)
    js.Global().Set("add", js.FuncOf(add))
    <-c
}
GOOS=js GOARCH=wasm go build -o main.wasm
cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .

4. Communication JS ↔ WASM

4.1 Modèle d'interaction

┌──────────┐         appels de fonction         ┌──────────┐
│          │ ◄─────────────────────────────► │          │
│    JS    │         mémoire linéaire          │   WASM   │
│          │ ◄─────────────────────────────► │          │
└──────────┘                                  └──────────┘
     │                                              │
     │ imports (log, rand, fetch)                   │ exports (add, process)
     └──────────────────────────────────────────────┘

4.2 Types supportés par wasm-bindgen

Type RustType JSNote
i32/u32NumberEntier 32 bits
i64/u64BigIntNécessite BigInt
f32/f64NumberFlottant 32/64 bits
boolBoolean-
StringStringCopie via mémoire
&strStringMapping automatique
Vec<u8>Uint8ArrayZéro-copie possible
structObjectVia serde JSON
Option<T>nullOption<T> → nullable
Result<T,E>ExceptionRésultat ou throw

4.3 Passage de données : copie vs référence

// 1. PAR COPIE — simple, mais overhead
#[wasm_bindgen]
pub fn process_copy(data: Vec<u8>) -> Vec<u8> {
    data.iter().map(|b| b ^ 0xFF).collect()
}

// 2. PAR RÉFÉRENCE (zéro-copie) — performant mais unsafe
#[wasm_bindgen]
pub fn process_inplace(data: &mut [u8]) {
    for byte in data.iter_mut() {
        *byte ^= 0xFF;
    }
}
// JS : wasm.process_inplace(new Uint8Array(memory.buffer, offset, length));

5. Use Cases Concrets

5.1 Figma — Traitement vectoriel temps réel

Figma a été un des premiers adoptants. Ils utilisent WASM pour :

  • Le rendu du canvas (moteur maison compilé depuis C++)
  • La manipulation de chemins vectoriels
  • L'export multi-format (SVG, PNG, PDF)
  • Les opérations booléennes sur les formes

Résultat : des performances comparables à une application native de desktop.

// Simplification : opération booléenne sur des chemins vectoriels
#[wasm_bindgen]
pub fn boolean_union(path1: &[Point], path2: &[Point]) -> Vec<Point> {
    // Algorithme de Greiner-Hormann compilé en WASM
    // Exécution à vitesse native
}

5.2 Photoshop Web — Filtres et effets

Adobe a porté Photoshop dans le navigateur avec WASM :

  • Filtres : flou gaussien, netteté, correction colorimétrique
  • Formats : PSD, RAW, TIFF
  • Calques et masques
#[wasm_bindgen]
pub fn gaussian_blur(pixels: &[u8], width: u32, height: u32, radius: f32) -> Vec<u8> {
    // Implémentation optimisée avec séparation X/Y
}

5.3 Google Earth — Rendu 3D

Google Earth compile son moteur C++ en WASM via Emscripten :

  • Rendu de terrain 3D avec textures
  • Streaming de données géospatiales
  • Interactions utilisateur temps réel

5.4 Jeux dans le navigateur

Unity, Unreal Engine, Godot supportent WASM comme target :

# Unity : Build Settings → WebGL
# Unreal : Target Platform → HTML5
# Godot : Export → Web (WASM)

5.5 Cryptographie et sécurité

#[wasm_bindgen]
pub fn argon2_hash(password: &str, salt: &str) -> Vec<u8> {
    // Argon2 en WASM : plus rapide que toute implémentation JS
    // car pas de GC, pas d'interprétation
}

5.6 Compression/Décompression

#[wasm_bindgen]
pub fn compress_gzip(data: &[u8]) -> Vec<u8> {
    // Utilise zlib-ng compilé en WASM
    // 5-10x plus rapide que pako.js
}

6. WASM vs JS : Benchmarks et Compromis

6.1 Benchmarks

OpérationJS (ms)WASM (ms)Ratio
Filtre image 4K (flou)8504220x
Mandelbrot (1000 iters)32008537x
JSON.parse (100MB)120951.3x
Regex matching451100.4x
DOM manipulation15N/AN/A

6.2 Quand utiliser WASM ?

WASM est excellent pour :

  • Calculs mathématiques intensifs (crypto, simulation, ML)
  • Traitement d'images, audio, vidéo
  • Jeux et rendu 3D/2D temps réel
  • Portage de bibliothèques existantes (C/C++/Rust)
  • Opérations sur de grands tableaux de données

Rester en JS pour :

  • Manipulation du DOM
  • Opérations I/O (fetch, localStorage)
  • Interactions utilisateur simples
  • Petites applications sans goulot d'étranglement CPU
  • Quand le coût de transfert WASM > bénéfice

6.3 Compromis

CritèreJSWASM
Taille bundlePetiteMoyenne (wasm ~500KB)
ChargementStreaméStreamé + compilation
DebuggingExcellentLimité (DWARF)
DOMDirectIndirect (via JS)
GCAutomatiqueManuel ou WASM GC (2024+)
PortabilitéUniversel→ modules ES
ÉcosystèmeImmenseEn croissance

7. Limitations de WebAssembly

7.1 Pas d'accès direct au DOM

WASM ne peut pas toucher le DOM. Il doit passer par JS :

// WASM → JS bridge (overhead)
// Mauvaise idée :
// for 60fps: appel WASM → JS callback → DOM update → JS → WASM

// Bonne idée :
// Grouper les mutations DOM
// Utiliser OffscreenCanvas
// Passer des handles/références plutôt que des objets

7.2 Taille des bundles

Un module WASM basique :

  • Hello World Rust → WASM : ~500 KB (inclut allocator)
  • Avec LTO et optimisation : ~50 KB
  • std complet via Emscripten : ~2 MB
  • Unreal Engine via WASM : ~50 MB

Stratégies de réduction :

# Rust : optimisations
[profile.release]
lto = true
opt-level = "z"   # Taille minimale
codegen-units = 1

# wasm-opt (Binaryen)
wasm-opt -Oz -o optimized.wasm input.wasm

7.3 Garbage Collection (avant 2024)

Avant WASM GC, chaque langage devait embarquer son propre GC :

  • Go : runtime Go complet (~2 MB)
  • Java/Kotlin : runtime complet
  • Python (Pyodide) : CPython entier (~10 MB)

Cela rendait certains langages impraticables pour WASM.

8. WASM GC — Nouvelle ère (2024+)

8.1 Présentation

La proposal WASM GC (Phase 4, incluse dans WASM 2.0) ajoute :

  • Types struct et array natifs dans WASM
  • Garbage collection intégré au runtime WASM
  • References (pas de pointeurs bruts)
  • Cast de type et dispatch
;; WASM GC — types struct natifs
(type $Point (struct (field $x f64) (field $y f64)))

(func $distance (param $p (ref $Point)) (result f64)
  ;; calcul sans manipulation mémoire manuelle
)

8.2 Impact

  • Kotlin/WASM : plus besoin de runtime GC
  • Java/WASM : enfin viable
  • Dart/WASM : Flutter Web en WASM natif
  • Langages managés : performance proche du natif

9. WebAssembly System Interface (WASI)

9.1 WASI pour le serveur

WASI étend WASM au-delà du navigateur :

  • Accès au système de fichiers
  • Networking
  • Horloge et random
  • Arguments de ligne de commande
# Exécuter WASM côté serveur
wasmtime run module.wasm --arg1 --arg2

# Avec WASI
wasmtime run --dir=. module.wasm

9.2 Cas d'usage serveur

  • Serverless : Cloudflare Workers, Fastly Compute
  • Plugins : Extensible sans risque (sandboxé)
  • Edge computing : Déploiement à la périphérie
  • Conteneurs légers : Alternative à Docker
// Rust → WASI (side serveur)
use std::fs;

fn main() {
    let contents = fs::read_to_string("data.txt").unwrap();
    println!("File: {}", contents.len());
}

Compilation :

cargo build --target wasm32-wasi
wasmtime run target/wasm32-wasi/debug/mon-projet.wasm

10. Exemple Complet : Processeur d'Image en Rust → WASM

10.1 Structure du projet

image-processor/
├── src/
│   └── lib.rs
├── pkg/              # Généré par wasm-pack
│   ├── image_processor.js
│   ├── image_processor_bg.wasm
│   └── package.json
├── web/
│   ├── index.html
│   ├── index.js
│   └── style.css
├── Cargo.toml
└── README.md

10.2 Code Rust (src/lib.rs)

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct ImageProcessor {
    width: u32,
    height: u32,
}

#[wasm_bindgen]
impl ImageProcessor {
    pub fn new(width: u32, height: u32) -> Self {
        Self { width, height }
    }

    pub fn grayscale(&self, pixels: &mut [u8]) {
        for chunk in pixels.chunks_mut(4) {
            let r = chunk[0] as f32;
            let g = chunk[1] as f32;
            let b = chunk[2] as f32;
            let gray = (0.299 * r + 0.587 * g + 0.114 * b) as u8;
            chunk[0] = gray;
            chunk[1] = gray;
            chunk[2] = gray;
        }
    }

    pub fn brightness(&self, pixels: &mut [u8], factor: f32) {
        for chunk in pixels.chunks_mut(4) {
            chunk[0] = (chunk[0] as f32 * factor).min(255.0) as u8;
            chunk[1] = (chunk[1] as f32 * factor).min(255.0) as u8;
            chunk[2] = (chunk[2] as f32 * factor).min(255.0) as u8;
        }
    }

    pub fn sepia(&self, pixels: &mut [u8]) {
        for chunk in pixels.chunks_mut(4) {
            let r = chunk[0] as f32;
            let g = chunk[1] as f32;
            let b = chunk[2] as f32;
            chunk[0] = (r * 0.393 + g * 0.769 + b * 0.189).min(255.0) as u8;
            chunk[1] = (r * 0.349 + g * 0.686 + b * 0.168).min(255.0) as u8;
            chunk[2] = (r * 0.272 + g * 0.534 + b * 0.131).min(255.0) as u8;
        }
    }

    pub fn invert(&self, pixels: &mut [u8]) {
        for chunk in pixels.chunks_mut(4) {
            chunk[0] = 255 - chunk[0];
            chunk[1] = 255 - chunk[1];
            chunk[2] = 255 - chunk[2];
        }
    }

    pub fn threshold(&self, pixels: &mut [u8], t: u8) {
        for chunk in pixels.chunks_mut(4) {
            let gray = (0.299 * chunk[0] as f32 + 0.587 * chunk[1] as f32 + 0.114 * chunk[2] as f32) as u8;
            let val = if gray > t { 255 } else { 0 };
            chunk[0] = val;
            chunk[1] = val;
            chunk[2] = val;
        }
    }

    pub fn convolve(&self, pixels: &mut [u8], kernel: &[f32]) {
        // Convolution avec un noyau 3x3
        let w = self.width as usize;
        let h = self.height as usize;
        let mut output = pixels.to_vec();

        for y in 1..h - 1 {
            for x in 1..w - 1 {
                let mut r = 0.0; let mut g = 0.0; let mut b = 0.0;
                for ky in 0..3 {
                    for kx in 0..3 {
                        let px = (y + ky - 1) * w + (x + kx - 1);
                        let k = kernel[(ky * 3 + kx) as usize];
                        r += pixels[px * 4] as f32 * k;
                        g += pixels[px * 4 + 1] as f32 * k;
                        b += pixels[px * 4 + 2] as f32 * k;
                    }
                }
                let idx = (y * w + x) * 4;
                output[idx] = r.clamp(0.0, 255.0) as u8;
                output[idx + 1] = g.clamp(0.0, 255.0) as u8;
                output[idx + 2] = b.clamp(0.0, 255.0) as u8;
            }
        }
        pixels.copy_from_slice(&output);
    }
}

10.3 Code JS (web/index.js)

import init, { ImageProcessor } from '../pkg/image_processor.js';

let processor = null;

async function start() {
  await init(); // Charge et compile le WASM

  const canvas = document.getElementById('canvas');
  const ctx = canvas.getContext('2d');
  const image = new Image();

  image.onload = () => {
    canvas.width = image.width;
    canvas.height = image.height;
    ctx.drawImage(image, 0, 0);
    processor = new ImageProcessor(image.width, image.height);
  };

  image.src = 'photo.jpg';

  window.applyFilter = (filterName) => {
    const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
    const pixels = new Uint8Array(imageData.data.buffer);

    const filters = {
      grayscale: () => processor.grayscale(pixels),
      sepia: () => processor.sepia(pixels),
      invert: () => processor.invert(pixels),
      brighten: () => processor.brightness(pixels, 1.5),
      darken: () => processor.brightness(pixels, 0.5),
      threshold: () => processor.threshold(pixels, 128),
      sharpen: () => processor.convolve(pixels, new Float32Array([
        0, -1, 0,
        -1, 5, -1,
        0, -1, 0
      ])),
      blur: () => processor.convolve(pixels, new Float32Array([
        1/9, 1/9, 1/9,
        1/9, 1/9, 1/9,
        1/9, 1/9, 1/9
      ])),
      edge: () => processor.convolve(pixels, new Float32Array([
        -1, -1, -1,
        -1, 8, -1,
        -1, -1, -1
      ])),
    };

    filters[filterName]();
    imageData.data.set(pixels);
    ctx.putImageData(imageData, 0, 0);
  };
}

start();

10.4 Build

wasm-pack build --target web --release

Résultat : traitement d'une image 4K (3840x2160) en ~30ms au lieu de 800ms en JS pur.

11. Performance : Guide de Décision

11.1 Arbre de décision

Le code est-il CPU-intensive ?
├── Oui → WASM est candidat
│   ├── Fait-il beaucoup d'allocations mémoire ?
│   │   ├── Oui → Attention aux coûts de copie
│   │   └── Non → WASM probablement gagnant
│   ├── S'agit-il de manipulation DOM ?
│   │   ├── Oui → Rester en JS (ou OffscreenCanvas)
│   │   └── Non → WASM probablement gagnant
│   └── Le code est-il déjà écrit en C/C++/Rust ?
│       ├── Oui → Portage trivial → WASM
│       └── Non → Évaluer coût de réécriture
└── Non → JS suffit

Le bundle WASM fait-il < 100 KB ?
├── Oui → Probablement bénéfique
└── Non → Comparer le temps de chargement WASM vs le gain

11.2 Règle empirique

  • Calcul pur (crypto, compression, image) : WASM 5-20x plus rapide
  • Logique métier (validation, mapping) : JS suffit, WASM overhead
  • Traitement par lot (traitement audio, vidéo) : WASM gagnant
  • Interaction utilisateur (clicks, hover) : JS natif, WASM inutile
  • Portage de bibliothèque : WASM si la bibliothèque est en C/C++/Rust

12. Bonnes Pratiques

  1. Mesurer avant d'optimiser : ne pas supposer que WASM est plus rapide
  2. Streamer l'instanciation : WebAssembly.instantiateStreaming
  3. Minimiser les allers-retours JS ↔ WASM : grouper les appels
  4. Utiliser les views TypedArray pour un accès mémoire efficace
  5. wasm-pack + wasm-opt : toujours optimiser la taille
  6. Feature detection : vérifier le support WASM avant de charger
  7. Fallback JS : prévoir une implémentation JS si WASM indisponible
  8. Worker : dédier un Web Worker à WASM pour ne pas bloquer le UI thread
// Chargement optimal
async function loadWasm(url) {
  if (!WebAssembly) throw new Error('WASM not supported');
  
  const imports = { /* vos imports */ };
  
  // 1. Tentative streaming
  if (WebAssembly.instantiateStreaming) {
    const { instance } = await WebAssembly.instantiateStreaming(
      fetch(url), imports
    );
    return instance.exports;
  }
  
  // 2. Fallback buffer
  const response = await fetch(url);
  const bytes = await response.arrayBuffer();
  const { instance } = await WebAssembly.instantiate(bytes, imports);
  return instance.exports;
}

// 3. Web Worker
const worker = new Worker('wasm-worker.js');
worker.postMessage({ type: 'init', wasmUrl: 'module.wasm' });
worker.onmessage = (e) => {
  console.log('WASM result:', e.data);
};

13. Conclusion

WebAssembly n'est pas un remplacement de JavaScript — c'est un complément. Il résout le problème des performances pour les calculs lourds tout en laissant à JS ce qu'il fait de mieux : la manipulation du DOM et l'interaction utilisateur.

L'arrivée de WASM GC (2024+), WASI (côté serveur) et la maturité des outils (wasm-pack, Emscripten) rendent WASM indispensable dans la boîte à outils d'un architecte front-end moderne.

Le futur : WASM dans le cloud (WASI), composants framework-agnostiques (Blazor), applications polyglottes dans le navigateur.