# WASM über Bord: Rust-Parser in TypeScript umgeschrieben – 3x schneller
Der Wechsel von Rust/WASM zu TypeScript beim Parsen einer benutzerdefinierten DSL direkt im Browser hat den Overhead der Laufzeitgrenzen eliminiert und einen inkrementellen Algorithmus ermöglicht. Die Gesamtzeit für die Stream-Verarbeitung sank um den Faktor 2,6–3,3.
Architektur des openui-lang Parsers
Der Parser wandelt eine benutzerdefinierte DSL, die von einem Sprachmodell (LLM) generiert wird, in einen React-Komponentenbaum um. Der Pipeline-Prozess umfasst sechs aufeinanderfolgende Stufen, die jeweils eine spezifische Aufgabe erledigen:
- Autocloser — stellt die syntaktische Korrektheit von Teilen des Texts sicher, indem er die minimale Anzahl schließender Klammern und Anführungszeichen ergänzt.
- Lexer — scannt den Eingabestring in einem Durchgang und erzeugt typisierte Tokens.
- Splitter — zerlegt den Token-Stream in einzelne
id = expression-Anweisungen. - Parser — baut mit rekursivem Abstieg aus Ausdrücken einen abstrakten Syntaxbaum (AST).
- Resolver — löst Variablenreferenzen auf, unterstützt Hoisting von Deklarationen und erkennt zyklische Abhängigkeiten.
- Mapper — wandelt den internen AST in das öffentliche
OutputNode-Format um, das für das React-Rendering bereit ist.
Jede Stufe ist für die Gesamtleistung entscheidend, da der Parser bei jedem Chunk des LLM-Output-Streams läuft.
Das Problem: Overhead an der WASM–JS-Grenze dominiert
Die ursprüngliche Rust-Implementierung, kompiliert zu WebAssembly, war beim Parsen selbst nicht langsam – sie litt unter den Datenübertragungskosten zwischen den Laufzeiten. Jeder Aufruf umfasste:
- Kopieren des Strings vom JS-Heap in den WASM-Linear-Memory.
- Serialisierung des Parse-Ergebnisses zu JSON innerhalb von Rust.
- Kopieren des JSON-Strings zurück in den JS-Heap.
- Deserialisierung des Strings in ein JS-Objekt via
JSON.parse().
Der Versuch mit serde-wasm-bindgen für direkte Objektübertragung ohne JSON-Serialisierung verschlechterte die Leistung sogar um 9–29 %. Der Grund: rekursive Materialisierung von Rust-Datenstrukturen in native JS-Objekte, was unzählige feingranulare Laufzeitübergänge auslöste.
Die Lösung: Komplette TypeScript-Portierung + inkrementeller Algorithmus
Der komplette Verzicht auf WASM zugunsten von TypeScript hat den Datenübertragungs-Overhead getötet. Der echte Game-Changer war jedoch die Optimierung für Streaming mit inkrementellem Ansatz.
Die naive Variante parste bei jedem neuen Chunk den gesamten akkumulierten String von vorne – mit quadratischer O(N²)-Komplexität bezogen auf die Chunk-Anzahl.
Wir führten eine inkrementelle Strategie mit Anweisungs-Caching ein:
- Anweisungen, die mit Zeilenumbruch bei Nulllage der Einrückung enden, gelten als unveränderlich.
- Ihre ASTs werden nach dem ersten Parse gecacht.
- Bei neuen Chunks wird nur der Schwanz (unfertige Anweisung) neu geparst.
- Das Endergebnis fusioniert gecachte ASTs mit dem frischen Schwanz-Parse.
Die Komplexität sank damit auf lineare O(L), wobei L die Gesamtlänge des Dokuments ist.
Benchmarks und Leistungsergebnisse
Wir haben drei realistische LLM-generierte Testfälle geprüft.
Einzelner Parser-Aufruf (Medianzeit, Mikrosekunden):
| Testfall | TypeScript | WASM (via JSON) | Beschleunigung |
|---------------|------------|-----------------|---------------|
| simple-table | 9,3 | 20,5 | 2,2x |
| contact-form | 13,4 | 61,4 | 4,6x |
| dashboard | 19,4 | 57,9 | 3,0x |
Gesamtkosten Stream-Verarbeitung (Medianzeit, Mikrosekunden):
| Testfall | Naiv TS (O(N²)) | Inkrementell TS (O(N)) | Beschleunigung |
|---------------|------------------|------------------------|---------------|
| simple-table | 69 | 77 | — |
| contact-form | 316 | 122 | 2,6x |
| dashboard | 840 | 255 | 3,3x |
Kein Gewinn bei simple-table, da es sich um eine einzelne Anweisung ohne Cache-Nutzen handelt. Die Vorteile skalieren mit der Anzahl der Anweisungen.
Wichtige Erkenntnisse
- In Rust/WASM-Setups mit häufigen Aufrufen ist nicht die Rechenleistung der Killer – sondern Datenkopieren und Serialisierung an Grenzen.
- Direkte WASM-zu-JS-Objektübergabe via
serde-wasm-bindgenkann langsamer sein als optimiertes JSON durch endlose interne Konvertierungen. - Algorithmische Verbesserungen (O(N²) zu O(N)) schlagen oft Sprach- oder Laufzeit-Mikro-Optimierungen.
- WASM glänzt bei low-interaction-Aufgaben (Bildverarbeitung), scheitert aber bei häufigem Parsing in komplexe JS-Objekte.
Wann WASM einsetzen (und wann nicht)
✅ WASM siegt:
- Rechenintensive Tasks mit Bulk-Input und skalaren/in-place Outputs (Media-Verarbeitung, Krypto, Physik-Sims).
- Portierung bestehender C/C++-Libs in den Browser ohne komplette Neuschreibung.
❌ WASM scheitert:
- Häufiges Parsing strukturierter Texte mit komplexen JS-Objekt-Rückgaben.
- Hot Paths mit winzigen Inputs, wo Grenz-Overhead nicht amortisiert wird.
— Editorial Team
Noch keine Kommentare.