Retour à l'accueil

Accélération du parseur par 3 : transition de Rust/WASM à TypeScript

L'article décrit le cas de réécriture du parseur DSL de Rust/WASM vers TypeScript, ce qui a conduit à une accélération de 2,6–3,3x dans le traitement. Il analyse les raisons du surcoût à la frontière WASM-JS et les avantages de l'algorithme incrémental avec mise en cache des instructions.

Comment nous avons accéléré le parseur 3 fois en abandonnant WASM
Advertisement 728x90

# 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 OutputNode prê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.

Google AdInline article slot

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.

Google AdInline article slot

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.

Google AdInline article slot

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-bindgen peut ê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

Advertisement 728x90

Lire ensuite