Der Rust-WASM-Parser wurde in TypeScript neu geschrieben – und war plötzlich 3-mal schneller
(openui.com)- Der in Rust geschriebene WASM-Parser ist strukturell schnell, doch der Overhead durch Datenkopien und Serialisierung an der JS-WASM-Grenze erwies sich als Performance-Engpass
- Die direkte Objektrückgabe über
serde-wasm-bindgenwar 9 bis 29 % langsamer als die JSON-Serialisierung, was auf die feingranularen Konvertierungskosten zwischen den Laufzeitumgebungen zurückzuführen ist - Nach dem Portieren der gesamten Pipeline nach TypeScript wurde bei gleicher Architektur eine 2,2- bis 4,6-mal schnellere Performance pro Einzelaufruf erreicht
- Bei der Streaming-Verarbeitung wurde durch die Verbesserung von O(N²) auf O(N) mittels satzweisem Caching eine 2,6- bis 3,3-mal schnellere Gesamtverarbeitung erzielt
- Im Ergebnis zeigte sich, dass WASM für rechenintensive, selten aufgerufene Aufgaben geeignet ist, für JS-Objekt-Parsing oder häufig aufgerufene Funktionen hingegen weniger
Struktur und Grenzen des Rust-WASM-Parsers
- Der Parser
openui-langbesteht aus einer 6-stufigen Pipeline, die von einem LLM erzeugte DSL in einen React-Komponentenbaum umwandelt- Schritte:
autocloser → lexer → splitter → parser → resolver → mapper → ParseResult - Jede Stufe übernimmt Aufgaben wie Tokenisierung, Syntaxanalyse, Variablenauflösung und AST-Transformation
- Schritte:
- Der Rust-Code selbst ist schnell, doch bei jedem Aufruf fallen Zeichenkettenkopien sowie JSON-Serialisierung und -Deserialisierung zwischen JS und WASM an
- Kopie des Eingabestrings (JS→WASM), internes Parsing in Rust, JSON-Serialisierung des Ergebnisses, JSON-Kopie (WASM→JS), Deserialisierung in JS
- Dieser Grenz-Overhead dominierte die Gesamtperformance; die Rechengeschwindigkeit von Rust war nicht der Flaschenhals
Versuch mit serde-wasm-bindgen und warum er scheiterte
- Um die JSON-Serialisierung zu vermeiden, wurde
serde-wasm-bindgeneingesetzt, das Rust-Strukturen direkt als JS-Objekte zurückgibt - Beobachtet wurde jedoch eine 30 % schlechtere Performance
- JS kann den Speicher von Rust-Strukturen nicht direkt lesen, und da sich die Speicherlayouts der Laufzeitumgebungen unterscheiden, ist eine feldweise Konvertierung erforderlich
- Bei der JSON-Serialisierung hingegen wird in Rust einmal ein String erzeugt, der in JS mit dem optimierten
JSON.parseverarbeitet wird
- Benchmark-Ergebnisse
Fixture JSON round-trip serde-wasm-bindgen Veränderung simple-table 20.5µs 22.5µs -9% contact-form 61.4µs 79.4µs -29% dashboard 57.9µs 74.0µs -28%
Wechsel zu TypeScript und Performance-Gewinn
- Die gleiche 6-stufige Struktur wurde vollständig nach TypeScript portiert, wodurch die WASM-Grenze entfiel und alles direkt im V8-Heap ausgeführt werden konnte
- Benchmark-Ergebnisse pro Einzelaufruf
Fixture TypeScript WASM Geschwindigkeitsgewinn simple-table 9.3µs 20.5µs 2.2x contact-form 13.4µs 61.4µs 4.6x dashboard 19.4µs 57.9µs 3.0x - Allein das Entfernen von WASM reduzierte die Kosten pro Aufruf deutlich, allerdings blieb die Ineffizienz der Streaming-Struktur bestehen
Das O(N²)-Problem beim Streaming-Parsing und seine Verbesserung
- Wenn LLM-Ausgaben in mehreren Chunks geliefert werden, entsteht die O(N²)-Ineffizienz, weil bei jedem Schritt der gesamte kumulierte String erneut geparst wird
- Beispiel: Ein Dokument mit 1000 Zeichen wird 50-mal in 20-Zeichen-Stücken geparst → insgesamt 25.000 verarbeitete Zeichen
- Als Lösung wurde inkrementelles Caching auf Satzebene eingeführt
- Abgeschlossene Sätze werden gecacht, nur der noch unvollständige Satz wird erneut geparst
- Der gecachte AST wird mit dem neuen AST zusammengeführt und als Ergebnis zurückgegeben
- Benchmarks für den gesamten Stream
Fixture Naives TS Inkrementelles TS Geschwindigkeitsgewinn simple-table 69µs 77µs keiner contact-form 316µs 122µs 2.6x dashboard 840µs 255µs 3.3x - Je mehr Sätze vorhanden sind, desto größer ist der Cache-Effekt, und der Gesamtdurchsatz verbessert sich linear
Erkenntnisse zum Einsatz von WASM
- Geeignete Fälle
- Rechenintensive Aufgaben mit wenig Interaktion: Bild- und Videoverarbeitung, Kryptografie, Physiksimulationen, Audio-Codecs usw.
- Portierung bestehender nativer Bibliotheken: SQLite, OpenCV, libpng usw.
- Ungeeignete Fälle
- Parsing strukturierter Texte in JS-Objekte: Hier dominieren die Serialisierungskosten
- Funktionen mit kurzen Eingaben und sehr häufigen Aufrufen: Die Grenzkosten sind größer als die eigentliche Berechnung
- Zentrale Lehren
- Die Sprache sollte erst nach Profiling des tatsächlichen Flaschenhalses gewählt werden
- Die direkte Objektübergabe mit
serde-wasm-bindgenist teurer - Die Verbesserung der algorithmischen Komplexität ist wirkungsvoller als ein Sprachwechsel
- WASM und JS teilen sich keinen Heap, daher gibt es immer Konvertierungskosten
Endergebnis: Durch den Wechsel zu TypeScript und inkrementelles Caching wurde eine 2,2- bis 4,6-mal bessere Performance pro Aufruf sowie eine 2,6- bis 3,3-mal bessere Performance über den gesamten Stream erreicht
2 Kommentare
War das nicht vielleicht eher als ironische Spitze gegen einen hochgradig auf Performance-Optimierung in Rust fokussierten Artikel gemeint..
Hacker-News-Kommentare
Der eigentliche Kern ist nicht TypeScript statt Rust, sondern die Korrektur des Streaming-Algorithmus von O(N²) auf O(N)
Allein diese Änderung mit Caching auf Statement-Ebene brachte bereits eine 3,3-fache Verbesserung
Unabhängig von der Sprachwahl ist das aus Sicht der Nutzer der Hauptgrund für die spürbare Verbesserung der Latenz
Der Titel wirkt, als würde er diesen interessanten Engineering-Punkt unterbewerten
Der Beitrag selbst ist interessant, aber ich bin in letzter Zeit von übertriebenen Clickbait-Titeln genervt
Es werden die Zeiten einzelner Aufrufe gemessen und der Median verwendet, aber in Browser-Umgebungen mit Timing-Angriffsschutz in der JS-Engine habe ich Zweifel an der Genauigkeit
„Ich habe Code von Sprache L nach M umgeschrieben und er wurde schneller“ ist kaum überraschend
Denn dabei bekommt man die Gelegenheit, Verhakungen und Fehlentscheidungen zu beseitigen und einen neu entstandenen besseren Ansatz anzuwenden
Tatsächlich gilt das sogar bei L=M: Der Geschwindigkeitsgewinn kommt nicht von der Sprache, sondern aus dem Prozess von Rewrite und Redesign
Ich habe tiefer hineingeschaut, um die Performance der Objektserialisierung an der Grenze zwischen Rust und JS zu verbessern
Der Ansatz von serde wirkte aus Performance-Sicht ungünstig, und ich habe einen Versuch zu dessen Verbesserung in meinem Blogbeitrag beschrieben
Ich hatte mich gefragt, warum Open UI nichts im WASM-Bereich macht
Dann war ich verwirrt, weil diese neue Firma den Namen Open UI verwendet
Die eigentliche Open UI W3C Community Group ist seit über fünf Jahren die Gruppe, die Standards für Dinge wie HTML-Popover, anpassbare Selects, Invoker-Commands und Accordions entwickelt
Sie leisten wirklich hervorragende Arbeit
Es heißt, man habe
serde-wasm-bindgenintegriert, um den „JSON-Roundtrip zu überspringen“, aber am Ende wirkt es wie eine Neuerfindung von JSON in BinärformJSON in V8 ist inzwischen bereits stark optimiert, und Implementierungen wie simdjson schaffen Durchsatz im Gigabyte-Bereich pro Sekunde
Ich halte es für unwahrscheinlich, dass JSON der Engpass ist
Mir gefiel das Design dieses Blogs wirklich sehr
Besonders gut fand ich die „scrollspy“-Sidebar, die Überschriften je nach Scroll-Position hervorhebt
Laut Claude wurde die Seite wohl mit fumadocs.dev gebaut
Ich habe den Zweck des Rust-WASM-Parsers nicht richtig verstanden
Im Artikel war das nicht klar genug, da bräuchte es mehr Erklärung
Das scheint dazu zu dienen, Informationsabfluss durch Prompt Injection zu verhindern
Der Parser kompiliert aus dem LLM gestreamte Chunks und baut damit in Echtzeit die UI auf
Früher wurde der Parser für jeden Chunk wieder von vorne gestartet, aber durch die Umstellung auf inkrementelle Verarbeitung (während der Portierung von Rust nach TypeScript) wurde die Performance stark verbessert
Ich hatte die Frage, ob TypeScript inzwischen nicht Go-basiert läuft
Halb im Scherz: Wenn man es wieder in Rust neu schreibt, gibt es vielleicht noch einmal eine 3-fache Leistungssteigerung /s