# Abandonner WASM : Réécrire un parseur Rust en TypeScript accélère de 3x
Passer de Rust/WASM à TypeScript pour analyser un DSL personnalisé dans le navigateur a éliminé les frais de passage de frontières d'exécution et permis un algorithme incrémental, réduisant le temps total de traitement du flux de 2,6 à 3,3 fois.
Architecture du parseur openui-lang
Le parseur convertit un DSL personnalisé généré par un modèle de langage (LLM) en un arbre de composants React. Le pipeline comporte six étapes séquentielles, chacune gérant une tâche spécifique :
- Autofermeur — garantit la correction syntaxique du texte partiel en ajoutant le nombre minimal de crochets et guillemets de fermeture.
- Lexer — effectue un balayage en un seul passage de la chaîne d'entrée pour générer des jetons typés.
- Séparateur — découpe le flux de jetons en instructions individuelles
id = expression. - Parseur — utilise une descente récursive pour construire un arbre syntaxique abstrait (AST) à partir des expressions.
- Résolveur — résout les références de variables, gère le hoist des déclarations et détecte les dépendances cycliques.
- Mappeur — transforme l'AST interne au format public
OutputNodeprêt pour le rendu React.
Chaque étape est critique pour les performances globales, car le parseur s'exécute sur chaque chunk du flux de sortie du LLM.
Le problème : Les frais de frontière WASM–JS dominent
L'implémentation Rust originale compilée en WebAssembly n'était pas lente en parsing pur — elle était handicapée par les coûts de transfert de données entre runtimes. Chaque appel impliquait :
- Copie de la chaîne du tas JS vers la mémoire linéaire WASM.
- Sérialisation du résultat d'analyse en JSON à l'intérieur de Rust.
- Copie de la chaîne JSON vers le tas JS.
- Désérialisation de la chaîne en objet JS via
JSON.parse().
Utiliser serde-wasm-bindgen pour un transfert direct d'objets sans sérialisation JSON a empiré les performances de 9 à 29 %. Cela provenait de la matérialisation récursive des structures Rust en objets JS natifs, déclenchant des myriades de passages fins entre runtimes.
La solution : Portage complet en TypeScript + Algorithme incrémental
Abandonner totalement WASM pour TypeScript a éliminé les frais de transfert de données. Mais le vrai changement a été l'optimisation pour le streaming avec une approche incrémentale.
La version naïve re-parsait la chaîne accumulée entière à partir de zéro à chaque nouveau chunk, menant à une complexité quadratique O(N²) par rapport au nombre de chunks.
Nous avons introduit une stratégie incrémentale avec mise en cache au niveau instruction :
- Les instructions se terminant par un saut de ligne à niveau d'imbrication zéro sont considérées comme immuables.
- Leurs AST sont mis en cache après le premier parsing.
- Sur les nouveaux chunks, seule l'instruction de queue (inachevée) est re-parsée.
- Le résultat final fusionne les AST en cache avec le parsing frais de la queue.
Cela a réduit la complexité à linéaire O(L), où L est la longueur totale du document.
Benchmarks et résultats de performance
Nous avons testé sur trois fixtures réalistes générées par LLM.
Appel unique au parseur (temps médian, microsecondes) :
| Fixture | TypeScript | WASM (via JSON) | Accélération |
|--------------|------------|-----------------|--------------|
| simple-table | 9,3 | 20,5 | 2,2x |
| contact-form | 13,4 | 61,4 | 4,6x |
| dashboard | 19,4 | 57,9 | 3,0x |
Coût total de traitement du flux (temps médian, microsecondes) :
| Fixture | TS naïf (O(N²)) | TS incrémental (O(N)) | Accélération |
|--------------|------------------|-----------------------|--------------|
| simple-table | 69 | 77 | — |
| contact-form | 316 | 122 | 2,6x |
| dashboard | 840 | 255 | 3,3x |
Pas d'accélération pour simple-table car c'est une seule instruction sans rien à mettre en cache. Les gains croissent avec le nombre d'instructions.
Enseignements clés
- Dans les setups Rust/WASM avec appels fréquents, le vrai tueur n'est pas le calcul — c'est la copie de données et la sérialisation aux frontières.
- Le passage direct d'objets WASM vers JS via
serde-wasm-bindgenpeut être plus lent que du JSON optimisé à cause des conversions internes infinies. - Les gains algorithmiques (O(N²) vers O(N)) surpassent souvent les micro-optimisations de langage ou runtime.
- WASM excelle pour les tâches à faible interaction (traitement d'images), mais échoue pour le parsing fréquent vers des objets JS complexes.
Quand utiliser WASM (et quand l'éviter)
✅ WASM gagne :
- Tâches à fort calcul avec entrée massive et sortie scalaire/en place (traitement média, crypto, simulations physiques).
- Portage de libs C/C++ existantes vers le navigateur sans réécriture complète.
❌ WASM perd :
- Parsing fréquent de texte structuré nécessitant des objets JS complexes en retour.
- Chemins chauds avec entrées minuscules où les frais de frontière ne s'amortissent pas.
— Editorial Team
Aucun commentaire pour le moment.