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
| Langage | Compilateur | Taille WASM | Performance | Maturité |
|---|---|---|---|---|
| Rust | wasm-pack, cargo | Petite (sans std) | ★★★★★ | Très mature |
| C/C++ | Emscripten, Clang | Moyenne | ★★★★★ | Très mature |
| Go | Go 1.11+ | Grande (runtime Go) | ★★★☆☆ | Mature |
| AssemblyScript | asc | Petite (TypeScript-like) | ★★★★☆ | Mature |
| Zig | Zig build | Petite | ★★★★★ | Émergent |
| Kotlin | Kotlin/WASM | Grande | ★★★☆☆ | Expérimental |
| Python | Pyodide | Trè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 Rust | Type JS | Note |
|---|---|---|
| i32/u32 | Number | Entier 32 bits |
| i64/u64 | BigInt | Nécessite BigInt |
| f32/f64 | Number | Flottant 32/64 bits |
| bool | Boolean | - |
| String | String | Copie via mémoire |
| &str | String | Mapping automatique |
| Vec<u8> | Uint8Array | Zéro-copie possible |
| struct | Object | Via serde JSON |
| Option<T> | null | Option<T> → nullable |
| Result<T,E> | Exception | Ré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ération | JS (ms) | WASM (ms) | Ratio |
|---|---|---|---|
| Filtre image 4K (flou) | 850 | 42 | 20x |
| Mandelbrot (1000 iters) | 3200 | 85 | 37x |
| JSON.parse (100MB) | 120 | 95 | 1.3x |
| Regex matching | 45 | 110 | 0.4x |
| DOM manipulation | 15 | N/A | N/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ère | JS | WASM |
|---|---|---|
| Taille bundle | Petite | Moyenne (wasm ~500KB) |
| Chargement | Streamé | Streamé + compilation |
| Debugging | Excellent | Limité (DWARF) |
| DOM | Direct | Indirect (via JS) |
| GC | Automatique | Manuel ou WASM GC (2024+) |
| Portabilité | Universel | → modules ES |
| Écosystème | Immense | En 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
- Mesurer avant d'optimiser : ne pas supposer que WASM est plus rapide
- Streamer l'instanciation :
WebAssembly.instantiateStreaming - Minimiser les allers-retours JS ↔ WASM : grouper les appels
- Utiliser les views TypedArray pour un accès mémoire efficace
- wasm-pack + wasm-opt : toujours optimiser la taille
- Feature detection : vérifier le support WASM avant de charger
- Fallback JS : prévoir une implémentation JS si WASM indisponible
- 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.