WebAssembly et Java : compiler du Java en WASM avec GraalVM et TeaVM
WebAssembly avec Java : GraalVM Native Image, TeaVM et Chicory pour exécuter du code Java dans le navigateur ou dans un runtime WASM sécurisé. Guide pratique 2026.
WebAssembly (WASM) est sorti du navigateur. En 2026, il s’est imposé comme format d’exécution pour les fonctions serverless (Fastly Compute, Cloudflare Workers), les plugins isolés (Envoy Proxy, Kubernetes admission webhooks), et les environnements edge. Java, avec GraalVM et TeaVM, peut cibler ces environnements. Voici comment.
Pourquoi WebAssembly en 2026 ?
WASM offre quatre propriétés que les architectures cloud-native cherchent depuis longtemps :
- Isolation forte : chaque module WASM est sandboxé par défaut — pas d’accès au filesystem ou réseau sans permission explicite
- Portabilité binaire : le même
.wasmtourne sur x86, ARM, et dans un navigateur - Démarrage instantané : < 1ms contre 100-500ms pour une JVM standard
- Sécurité mémoire : pas de vulnérabilités buffer overflow par conception
Le composant WASM succède aux conteneurs pour les workloads éphémères courts.
Les 3 approches Java + WASM
┌─────────────────────────────────────────────────────────┐
│ Approche 1 : TeaVM │
│ Java bytecode → WebAssembly (navigateur) │
│ Cas d'usage : logique métier Java dans une SPA │
├─────────────────────────────────────────────────────────┤
│ Approche 2 : GraalVM Native Image + WASM target │
│ Java source → binaire WASM (serveur/edge) │
│ Cas d'usage : serverless, edge functions │
├─────────────────────────────────────────────────────────┤
│ Approche 3 : Chicory │
│ JVM Java pur qui exécute des modules .wasm │
│ Cas d'usage : plugins isolés dans une app Java │
└─────────────────────────────────────────────────────────┘
Approche 1 : TeaVM — Java dans le navigateur
TeaVM compile du bytecode Java en JavaScript ou WASM. La logique métier Java tourne dans le navigateur sans serveur.
Setup Maven
<plugin>
<groupId>org.teavm</groupId>
<artifactId>teavm-maven-plugin</artifactId>
<version>0.10.1</version>
<executions>
<execution>
<goals><goal>compile</goal></goals>
<phase>compile</phase>
<configuration>
<targetDirectory>${project.build.directory}/generated/js</targetDirectory>
<mainClass>com.example.ClientApp</mainClass>
<targetType>WEBASSEMBLY</targetType> <!-- ou JAVASCRIPT -->
<optimizationLevel>ADVANCED</optimizationLevel>
</configuration>
</execution>
</executions>
</plugin>
Code Java compilé vers WASM
// Logique métier partagée Java ↔ browser
public class TaxCalculator {
// Appelable depuis JavaScript via TeaVM bridge
@JSExport
public static double calculateNetRevenue(double dailyRate, int daysPerYear, double charges) {
double grossRevenue = dailyRate * daysPerYear;
double cotisations = grossRevenue * charges;
double netBeforeTax = grossRevenue - cotisations;
return applyIncomeTax(netBeforeTax);
}
private static double applyIncomeTax(double income) {
if (income <= 11_600) return income;
if (income <= 29_578) return income - (income - 11_600) * 0.11;
if (income <= 84_577) return income - (income - 29_578) * 0.30 - 1977.18;
return income * 0.55; // simplification
}
}
// Point d'entrée TeaVM
public class ClientApp {
public static void main(String[] args) {
// Enregistrement des exports
TeaVM.current().exportStaticMethod(
"calculateNetRevenue",
TaxCalculator.class,
"calculateNetRevenue",
double.class, int.class, double.class
);
}
}
Consommer depuis JavaScript
// Charger le module WASM généré par TeaVM
const { instance } = await WebAssembly.instantiateStreaming(
fetch('/app.wasm'),
{ /* imports */ }
);
// La fonction Java est maintenant disponible
const netRevenue = instance.exports.calculateNetRevenue(
500.0, // TJM
200, // jours/an
0.256 // cotisations 2026
);
console.log(`Revenu net estimé : ${netRevenue.toFixed(2)} €`);
Ce que TeaVM supporte (et ne supporte pas)
| Supporté | Non supporté |
|---|---|
| Collections Java, streams | Reflection dynamique |
| Lambdas, records | ClassLoader dynamique |
| Interfaces, polymorphisme | Threads / virtual threads |
| String, Math, format | I/O filesystem direct |
| Gson, Jackson (partiel) | La plupart des librairies serveur |
Approche 2 : GraalVM Native Image
GraalVM compile l’ensemble de votre application Spring Boot en un binaire natif. Le démarrage passe de 3-8s à < 100ms, et la mémoire de 256MB à 30-50MB.
Configuration Spring Boot 3.x pour Native
<!-- pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.0</version>
</parent>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<buildArgs>
<arg>--no-fallback</arg>
<arg>-H:+ReportExceptionStackTraces</arg>
<arg>--initialize-at-build-time=ch.qos.logback</arg>
</buildArgs>
<metadataRepository>
<enabled>true</enabled>
</metadataRepository>
</configuration>
</plugin>
</plugins>
</build>
Compiler et mesurer
# Compiler en binaire natif (nécessite GraalVM JDK 21+)
./mvnw -Pnative native:compile
# Mesurer le démarrage
time ./target/mon-service
# real 0m0.087s ← vs 3-8s avec JVM standard
# Comparer les tailles mémoire
ps aux --sort rss | grep mon-service
# JVM : 280 MB RSS
# Native : 42 MB RSS
Hints pour la reflection
GraalVM analyse le code au build-time. Tout ce qui utilise la reflection doit être déclaré :
@Configuration
@ImportRuntimeHints(MyRuntimeHints.class)
public class AppConfig { }
public class MyRuntimeHints implements RuntimeHintsRegistrar {
@Override
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
// Sérialisation Jackson
hints.serialization().registerType(OrderDto.class);
hints.serialization().registerType(ProductDto.class);
// Reflection pour des classes annotées dynamiquement
hints.reflection().registerType(
TypeReference.of(MyService.class),
MemberCategory.INVOKE_DECLARED_METHODS
);
// Resources embarquées
hints.resources().registerPattern("templates/*.html");
hints.resources().registerPattern("i18n/*.properties");
}
}
Dockerfile multi-stage avec GraalVM
FROM ghcr.io/graalvm/native-image-community:21-muslib AS builder
WORKDIR /build
COPY . .
RUN ./mvnw -Pnative native:compile -DskipTests
FROM scratch
COPY --from=builder /build/target/mon-service /mon-service
EXPOSE 8080
ENTRYPOINT ["/mon-service"]
# Image finale : ~25 MB vs ~200 MB avec JVM
Approche 3 : Chicory — exécuter du WASM dans Java
Chicory est une JVM WASM écrite en Java pur. Elle permet d’exécuter des modules .wasm dans une application Java existante — idéal pour les plugins sandboxés.
<dependency>
<groupId>com.dylibso.chicory</groupId>
<artifactId>runtime</artifactId>
<version>0.0.12</version>
</dependency>
@Service
public class PluginExecutorService {
public String executePlugin(byte[] wasmBytes, String inputJson) {
// Charger et exécuter le module WASM de façon isolée
var module = Module.builder(wasmBytes).build();
var instance = module.instantiate();
// Appeler une fonction exportée par le plugin WASM
var memory = instance.memory();
var inputBytes = inputJson.getBytes(StandardCharsets.UTF_8);
// Écrire l'input dans la mémoire WASM
var inputPtr = allocateInWasm(instance, inputBytes.length);
memory.write(inputPtr, inputBytes);
// Appeler la fonction du plugin
var result = instance.export("process").apply(
Value.i32(inputPtr),
Value.i32(inputBytes.length)
);
// Lire le résultat depuis la mémoire WASM
var outputPtr = result[0].asInt();
var outputLen = result[1].asInt();
return new String(memory.readBytes(outputPtr, outputLen), StandardCharsets.UTF_8);
}
}
Cas d’usage concrets :
- Plugins utilisateur (un client peut charger son propre plugin
.wasmsans risque pour le serveur) - Règles métier personnalisables (chaque tenant a son
.wasmde règles) - Filtres Envoy Proxy écrits en n’importe quel langage
Benchmark : JVM vs GraalVM Native vs WASM
Traitement de 10 000 commandes en mémoire :
| Runtime | Démarrage | Pic mémoire | Throughput |
|---|---|---|---|
| JVM (JIT) | 3.2s | 280 MB | 45 000 req/s |
| GraalVM Native | 87ms | 42 MB | 38 000 req/s |
| TeaVM WASM (browser) | 12ms | 8 MB | 12 000 ops/s |
| Chicory (interprété) | 2ms | 5 MB | 800 ops/s |
GraalVM Native gagne sur le démarrage et la mémoire. La JVM avec JIT reste devant sur le throughput long terme (le compilateur JIT optimise sur les patterns réels). Chicory est lent mais sécurisé — parfait pour les plugins.
Quand utiliser quoi
TeaVM : logique métier (calcul, validation, parsing) partagée entre backend Java et frontend, sans duplication de code.
GraalVM Native : fonctions serverless, microservices à démarrage rapide, images Docker minimales pour Kubernetes.
Chicory : systèmes de plugins, règles métier isolées, exécution de code utilisateur non fiable dans une application Java.
WebAssembly n’est plus expérimental en Java. GraalVM 21+ et TeaVM 0.10 sont stables en production. Pour un freelance Java, maîtriser ces outils ouvre des marchés spécifiques : edge computing, architectures serverless à froid, et plateformes de plugins extensibles — des sujets où peu de développeurs ont encore de l’expérience concrète.
Sur le même sujet
- IA agentique en Java : construire des agents autonomes — Chicory (WASM runtime en Java) permet d’exécuter des plugins de traitement de données sandboxés dans un agent IA, sans risque pour le serveur hôte.
- DevSecOps : intégrer la sécurité dans votre pipeline CI/CD Java — Les images GraalVM distroless que Trivy scanne en quelques secondes : beaucoup moins de CVEs que les images JVM classiques.
- Quarkus : Java cloud-natif pour les microservices — Quarkus utilise également GraalVM Native Image pour atteindre des démarrages < 50ms — une approche complémentaire à TeaVM pour les services backend.
Amine MEGDICHE
Développeur AEM & Java Full Stack — Freelance depuis 2013