Hot Path im JavaScript-Code der App durch WebAssembly ersetzen

Es ist immer schnell

In meinen vorherigen Artikeln habe ich beschrieben, wie Sie mit WebAssembly das Bibliothekssystem von C/C++ ins Web bringen können. Eine App, die C/C++-Bibliotheken intensiv nutzt, ist squoosh, unsere Webanwendung, mit der Sie Bilder mit einer Vielzahl von Codecs komprimieren können, die aus C++ in WebAssembly kompiliert wurden.

WebAssembly ist eine Low-Level-virtuelle Maschine, die den Bytecode ausführt, der in .wasm-Dateien gespeichert ist. Dieser Bytecode ist stark typisiert und so strukturiert, dass er viel schneller als JavaScript für das Hostsystem kompiliert und optimiert werden kann. WebAssembly bietet eine Umgebung zum Ausführen von Code, die von Anfang an für Sandboxing und Einbetten konzipiert wurde.

Meiner Erfahrung nach werden die meisten Leistungsprobleme im Web durch erzwungenes Layout und übermäßiges Painting verursacht. Aber hin und wieder muss eine App eine rechenintensive Aufgabe ausführen, die viel Zeit in Anspruch nimmt. WebAssembly kann hier helfen.

Hot Path

In Squoosh haben wir eine JavaScript-Funktion geschrieben, die einen Bildpuffer um ein Vielfaches von 90 Grad dreht. OffscreenCanvas wäre dafür ideal, wird aber nicht von allen Browsern unterstützt, auf die wir ausgerichtet waren, und ist in Chrome etwas fehlerhaft.

Diese Funktion durchläuft jedes Pixel eines Eingabebilds und kopiert es an eine andere Position im Ausgabebild, um eine Drehung zu erzielen. Für ein Bild mit 4.094 × 4.096 Pixeln (16 Megapixel) wären über 16 Millionen Iterationen des inneren Codeblocks erforderlich, den wir als „Hotpath“ bezeichnen. Trotz dieser relativ großen Anzahl von Iterationen erledigen zwei der drei von uns getesteten Browser die Aufgabe in weniger als zwei Sekunden. Eine akzeptable Dauer für diese Art von Interaktion.

for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    const in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

Ein Browser benötigt jedoch über 8 Sekunden. Die Art und Weise, wie Browser JavaScript optimieren, ist sehr kompliziert und verschiedene Engines optimieren für unterschiedliche Dinge. Einige optimieren für die reine Ausführung, andere für die Interaktion mit dem DOM. In diesem Fall haben wir in einem Browser einen nicht optimierten Pfad gefunden.

WebAssembly hingegen ist vollständig auf die reine Ausführungsgeschwindigkeit ausgelegt. Wenn wir also eine schnelle, vorhersehbare Leistung für Code wie diesen in allen Browsern wünschen, kann WebAssembly helfen.

WebAssembly für vorhersehbare Leistung

Im Allgemeinen können JavaScript und WebAssembly dieselbe Spitzenleistung erreichen. Bei JavaScript kann diese Leistung jedoch nur auf dem „schnellen Pfad“ erreicht werden. Es ist oft schwierig, auf diesem Pfad zu bleiben. Ein wichtiger Vorteil von WebAssembly ist die vorhersehbare Leistung, auch plattformübergreifend. Die strenge Typisierung und die Low-Level-Architektur ermöglichen es dem Compiler, stärkere Garantien zu geben, sodass WebAssembly-Code nur einmal optimiert werden muss und immer den „schnellen Pfad“ verwendet.

Code für WebAssembly schreiben

Bisher haben wir C/C++-Bibliotheken in WebAssembly kompiliert, um ihre Funktionen im Web zu nutzen. Wir haben den Code der Bibliotheken nicht wirklich verändert, sondern nur kleine Mengen C/C++-Code geschrieben, um eine Brücke zwischen dem Browser und der Bibliothek zu bilden. Diesmal ist unsere Motivation anders: Wir möchten etwas von Grund auf neu mit WebAssembly schreiben, damit wir die Vorteile von WebAssembly nutzen können.

WebAssembly-Architektur

Wenn Sie für WebAssembly programmieren, sollten Sie wissen, was WebAssembly eigentlich ist.

Zitat von WebAssembly.org:

Wenn Sie C- oder Rust-Code in WebAssembly kompilieren, erhalten Sie eine .wasm-Datei mit einer Moduldeklaration. Diese Deklaration besteht aus einer Liste von „Importen“, die das Modul von seiner Umgebung erwartet, einer Liste der Exporte, die dieses Modul dem Host zur Verfügung stellt (Funktionen, Konstanten, Speicherbereiche), und natürlich den tatsächlichen Binäranweisungen für die darin enthaltenen Funktionen.

Mir war das erst klar geworden, als ich mir das genauer angesehen habe: Der Stack, der WebAssembly zu einer „stackbasierten virtuellen Maschine“ macht, wird nicht im Speicherbereich gespeichert, den WebAssembly-Module verwenden. Der Stack ist vollständig VM-intern und für Webentwickler nicht zugänglich (außer über die DevTools). So ist es möglich, WebAssembly-Module zu schreiben, die überhaupt keinen zusätzlichen Arbeitsspeicher benötigen und nur den VM-internen Stack verwenden.

In unserem Fall müssen wir zusätzlichen Arbeitsspeicher verwenden, um beliebigen Zugriff auf die Pixel unseres Bildes zu ermöglichen und eine gedrehte Version dieses Bildes zu generieren. Dafür ist WebAssembly.Memory da.

Speicherverwaltung

Wenn Sie zusätzlichen Arbeitsspeicher verwenden, müssen Sie diesen in der Regel auch verwalten. Welche Teile des Arbeitsspeichers sind belegt? Welche sind kostenlos? In C gibt es beispielsweise die Funktion malloc(n), die einen Speicherplatz von n aufeinanderfolgenden Byte findet. Funktionen dieser Art werden auch als „Allocatoren“ bezeichnet. Natürlich muss die Implementierung des verwendeten Allocators in Ihrem WebAssembly-Modul enthalten sein. Dies erhöht die Dateigröße. Die Größe und Leistung dieser Speicherverwaltungsfunktionen kann je nach verwendetem Algorithmus sehr unterschiedlich sein. Daher bieten viele Sprachen mehrere Implementierungen zur Auswahl („dmalloc“, „emmalloc“, „wee_alloc“ usw.).

In unserem Fall kennen wir die Abmessungen des Eingabebilds (und damit die Abmessungen des Ausgabebilds), bevor wir das WebAssembly-Modul ausführen. Hier sahen wir eine Chance: Normalerweise übergeben wir den RGBA-Puffer des Eingabebilds als Parameter an eine WebAssembly-Funktion und geben das gedrehte Bild als Rückgabewert zurück. Um diesen Rückgabewert zu generieren, müssten wir den Allocator verwenden. Da wir jedoch die Gesamtmenge des benötigten Arbeitsspeichers kennen (doppelt so groß wie das Eingabebild, einmal für die Eingabe und einmal für die Ausgabe), können wir das Eingabebild mit JavaScript in den WebAssembly-Speicher einfügen, das WebAssembly-Modul ausführen, um ein zweites, gedrehtes Bild zu generieren, und dann das Ergebnis mit JavaScript zurücklesen. Wir können ganz ohne Speicherverwaltung auskommen!

Die Qual der Wahl

Wenn Sie sich die ursprüngliche JavaScript-Funktion ansehen, die wir in WebAssembly umwandeln möchten, sehen Sie, dass es sich um reinen Berechnungscode ohne JavaScript-spezifische APIs handelt. Daher sollte es ziemlich einfach sein, diesen Code in eine beliebige Sprache zu portieren. Wir haben drei verschiedene Sprachen evaluiert, die in WebAssembly kompiliert werden: C/C++, Rust und AssemblyScript. Die einzige Frage, die wir für jede der Sprachen beantworten müssen, lautet: Wie greifen wir auf den Rohspeicher zu, ohne die Funktionen zur Speicherverwaltung zu verwenden?

C und Emscripten

Emscripten ist ein C-Compiler für das WebAssembly-Ziel. Emscripten soll als Drop-in-Ersatz für bekannte C-Compiler wie GCC oder clang dienen und ist größtenteils flag-kompatibel. Dies ist ein zentraler Bestandteil der Emscripten-Philosophie, da das Kompilieren vorhandenen C- und C++-Codes in WebAssembly so einfach wie möglich gemacht werden soll.

Der Zugriff auf den Rohspeicher ist in C von Natur aus vorgesehen und genau aus diesem Grund gibt es Zeiger:

uint8_t* ptr = (uint8_t*)0x124;
ptr[0] = 0xFF;

Hier wird die Zahl 0x124 in einen Verweis auf ungesignierte 8‑Bit-Ganzzahlen (oder Bytes) umgewandelt. Dadurch wird die Variable ptr effektiv in ein Array umgewandelt, das an der Speicheradresse 0x124 beginnt und das wir wie jedes andere Array verwenden können. So können wir zum Lesen und Schreiben auf einzelne Byte zugreifen. In unserem Fall sehen wir uns einen RGBA-Puffer eines Bildes an, den wir neu anordnen möchten, um eine Drehung zu erzielen. Um ein Pixel zu verschieben, müssen wir tatsächlich vier aufeinanderfolgende Bytes gleichzeitig verschieben (ein Byte für jeden Kanal: R, G, B und A). Um das zu vereinfachen, können wir ein Array aus vorzeichenlosen 32‑Bit-Ganzzahlen erstellen. Gemäß der Konvention beginnt unser Eingabebild bei Adresse 4 und unser Ausgabebild direkt nach dem Ende des Eingabebilds:

int bpp = 4;
int imageSize = inputWidth * inputHeight * bpp;
uint32_t* inBuffer = (uint32_t*) 4;
uint32_t* outBuffer = (uint32_t*) (inBuffer + imageSize);

for (int d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
    for (int d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
    int in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
    outBuffer[i] = inBuffer[in_idx];
    i += 1;
    }
}

Nachdem wir die gesamte JavaScript-Funktion in C portiert haben, können wir die C-Datei mit emcc kompilieren:

$ emcc -O3 -s ALLOW_MEMORY_GROWTH=1 -o c.js rotate.c

Wie immer generiert emscripten eine Glue-Codedatei namens c.js und ein WASM-Modul namens c.wasm. Das WASM-Modul wird durch GZIP auf nur etwa 260 Byte komprimiert, während der Glue-Code nach GZIP etwa 3,5 KB groß ist. Nach einigem Herumprobieren konnten wir den Glue-Code entfernen und die WebAssembly-Module mit den Vanilla-APIs instanziieren. Das ist mit Emscripten oft möglich, solange Sie nichts aus der C-Standardbibliothek verwenden.

Rust

Rust ist eine neue, moderne Programmiersprache mit einem umfassenden Typsystem, ohne Laufzeit und einem Besitzmodell, das Speicher- und Threadsicherheit garantiert. Rust unterstützt WebAssembly auch als Kernfunktion und das Rust-Team hat dem WebAssembly-Ökosystem viele hervorragende Tools zur Verfügung gestellt.

Eines dieser Tools ist wasm-pack von der rustwasm-Arbeitsgruppe. wasm-packwandelt Ihren Code in ein webfreundliches Modul um, das sofort mit Bundlern wie webpack funktioniert. wasm-pack ist äußerst praktisch, funktioniert aber derzeit nur für Rust. Die Gruppe prüft derzeit, ob auch andere WebAssembly-Zielsprachen unterstützt werden sollen.

In Rust entsprechen Slices den Arrays in C. Und genau wie in C müssen wir Slices erstellen, die unsere Startadressen verwenden. Das verstößt gegen das von Rust erzwungene Speichersicherheitsmodell. Um unser Ziel zu erreichen, müssen wir das Schlüsselwort unsafe verwenden, mit dem wir Code schreiben können, der nicht diesem Modell entspricht.

let imageSize = (inputWidth * inputHeight) as usize;
let inBuffer: &mut [u32];
let outBuffer: &mut [u32];
unsafe {
    inBuffer = slice::from_raw_parts_mut::<u32>(4 as *mut u32, imageSize);
    outBuffer = slice::from_raw_parts_mut::<u32>((imageSize * 4 + 4) as *mut u32, imageSize);
}

for d2 in 0..d2Limit {
    for d1 in 0..d1Limit {
    let in_idx = (d1Start + d1 * d1Advance) * d1Multiplier + (d2Start + d2 * d2Advance) * d2Multiplier;
    outBuffer[i as usize] = inBuffer[in_idx as usize];
    i += 1;
    }
}

Rust-Dateien mit kompilieren

$ wasm-pack build

ergibt ein 7,6 KB großes WASM-Modul mit etwa 100 Byte Glue-Code (beides nach gzip).

AssemblyScript

AssemblyScript ist ein relativ junges Projekt, das als TypeScript-zu-WebAssembly-Compiler dienen soll. Es wird jedoch nicht einfach nur TypeScript verwendet. AssemblyScript verwendet dieselbe Syntax wie TypeScript, ersetzt jedoch die Standardbibliothek durch eine eigene. Die Standardbibliothek modelliert die Funktionen von WebAssembly. Das bedeutet, dass Sie nicht einfach TypeScript-Code in WebAssembly kompilieren können. Sie müssen jedoch keine neue Programmiersprache lernen, um WebAssembly zu schreiben.

    for (let d2 = d2Start; d2 >= 0 && d2 < d2Limit; d2 += d2Advance) {
      for (let d1 = d1Start; d1 >= 0 && d1 < d1Limit; d1 += d1Advance) {
        let in_idx = ((d1 * d1Multiplier) + (d2 * d2Multiplier));
        store<u32>(offset + i * 4 + 4, load<u32>(in_idx * 4 + 4));
        i += 1;
      }
    }

Angesichts der geringen Typenoberfläche unserer rotate()-Funktion war es ziemlich einfach, diesen Code in AssemblyScript zu portieren. Die Funktionen load<T>(ptr: usize) und store<T>(ptr: usize, value: T) werden von AssemblyScript zum Zugriff auf den Rohspeicher bereitgestellt. Um unsere AssemblyScript-Datei zu kompilieren, müssen wir nur das npm-Paket AssemblyScript/assemblyscript installieren und

$ asc rotate.ts -b assemblyscript.wasm --validate -O3

AssemblyScript liefert uns ein WASM-Modul mit etwa 300 Byte und keinen Glue-Code. Das Modul funktioniert nur mit den Vanilla-WebAssembly-APIs.

WebAssembly-Forensik

Die 7,6 KB von Rust sind im Vergleich zu den beiden anderen Sprachen überraschend groß. Es gibt einige Tools im WebAssembly-Ökosystem, mit denen Sie Ihre WebAssembly-Dateien analysieren können (unabhängig von der Sprache, mit der sie erstellt wurden), um herauszufinden, was los ist, und die Situation zu verbessern.

Twiggy

Twiggy ist ein weiteres Tool des WebAssembly-Teams von Rust, mit dem eine Reihe nützlicher Daten aus einem WebAssembly-Modul extrahiert werden. Das Tool ist nicht Rust-spezifisch und ermöglicht es Ihnen, beispielsweise den Aufrufgraphen des Moduls zu prüfen, nicht verwendete oder überflüssige Abschnitte zu ermitteln und herauszufinden, welche Abschnitte zur Gesamtdateigröße Ihres Moduls beitragen. Das geht mit dem Befehl top von Twiggy:

$ twiggy top rotate_bg.wasm
Screenshot der Twiggy-Installation

In diesem Fall sehen wir, dass der Großteil der Dateigröße auf den Allocator zurückzuführen ist. Das war überraschend, da in unserem Code keine dynamischen Zuweisungen verwendet werden. Ein weiterer wichtiger Faktor ist ein Abschnitt zu „Funktionsnamen“.

wasm-strip

wasm-strip ist ein Tool aus dem WebAssembly Binary Toolkit, kurz wabt. Es enthält einige Tools, mit denen Sie WebAssembly-Module prüfen und bearbeiten können. wasm2wat ist ein Disassembler, der ein binäres WASM-Modul in ein für Menschen lesbares Format umwandelt. Wabt enthält auch wat2wasm, mit dem Sie dieses für Menschen lesbare Format wieder in ein binäres WASM-Modul umwandeln können. Wir haben diese beiden ergänzenden Tools verwendet, um unsere WebAssembly-Dateien zu prüfen. Wir fanden wasm-strip jedoch am nützlichsten. wasm-strip entfernt unnötige Abschnitte und Metadaten aus einem WebAssembly-Modul:

$ wasm-strip rotate_bg.wasm

Dadurch wird die Dateigröße des Rust-Moduls von 7,5 KB auf 6,6 KB (nach GZIP) reduziert.

wasm-opt

wasm-opt ist ein Tool von Binaryen. Es nimmt ein WebAssembly-Modul und versucht, es sowohl hinsichtlich Größe als auch Leistung nur anhand des Bytecodes zu optimieren. Einige Tools wie Emscripten nutzen dieses Tool bereits, andere nicht. Es empfiehlt sich in der Regel, mithilfe dieser Tools zusätzliche Bytes zu sparen.

wasm-opt -O3 -o rotate_bg_opt.wasm rotate_bg.wasm

Mit wasm-opt können wir noch ein paar Byte einsparen, sodass nach gzip insgesamt 6,2 KB übrig bleiben.

#![no_std]

Nach einigen Beratungen und Recherchen haben wir unseren Rust-Code ohne die Standardbibliothek von Rust neu geschrieben und dabei die Funktion #![no_std] verwendet. Dadurch werden auch dynamische Speicherzuordnungen vollständig deaktiviert und der Allocator-Code aus unserem Modul entfernt. Kompilieren von dieser Rust-Datei mit

$ rustc --target=wasm32-unknown-unknown -C opt-level=3 -o rust.wasm rotate.rs

führte nach wasm-opt, wasm-strip und gzip zu einem 1,6 KB großen WASM-Modul. Es ist zwar immer noch größer als die von C und AssemblyScript generierten Module, aber klein genug, um als „leichtgewichtig“ zu gelten.

Leistung

Bevor wir vorschnell Schlüsse aufgrund der Dateigröße ziehen: Wir haben diese Änderungen vorgenommen, um die Leistung zu optimieren, nicht die Dateigröße. Wie haben wir die Leistung gemessen und welche Ergebnisse haben wir erzielt?

Benchmarking

Obwohl WebAssembly ein Low-Level-Bytecode-Format ist, muss es trotzdem durch einen Compiler gesendet werden, um hostspezifischen Maschinencode zu generieren. Genau wie bei JavaScript arbeitet der Compiler in mehreren Phasen. Einfach ausgedrückt: Die erste Phase ist viel schneller beim Kompilieren, generiert aber in der Regel langsameren Code. Sobald das Modul gestartet wird, beobachtet der Browser, welche Teile häufig verwendet werden, und sendet diese an einen optimierteren, aber langsameren Compiler.

Unser Anwendungsfall ist interessant, da der Code zum Drehen eines Bildes einmal oder vielleicht zweimal verwendet wird. In den meisten Fällen können wir also nie die Vorteile des optimierten Compilers nutzen. Das sollten Sie beim Benchmarking berücksichtigen. Wenn wir unsere WebAssembly-Module 10.000 Mal in einer Schleife ausführen würden,wären die Ergebnisse unrealistisch. Um realistische Zahlen zu erhalten, sollten wir das Modul einmal ausführen und Entscheidungen anhand der Zahlen aus diesem einzelnen Durchlauf treffen.

Leistungsvergleich

Geschwindigkeitsvergleich nach Sprache
Geschwindigkeitsvergleich nach Browser

Diese beiden Diagramme sind unterschiedliche Ansichten derselben Daten. Im ersten Diagramm wird nach Browser, im zweiten nach verwendeter Sprache verglichen. Ich habe eine logarithmische Zeitskala gewählt. Außerdem ist es wichtig, dass für alle Benchmarks dasselbe 16-Megapixel-Testbild und dasselbe Hostgerät verwendet wurden, mit Ausnahme eines Browsers, der nicht auf demselben Computer ausgeführt werden konnte.

Ohne diese Grafiken zu sehr zu analysieren, ist klar, dass wir unser ursprüngliches Leistungsproblem gelöst haben: Alle WebAssembly-Module werden in etwa 500 ms oder weniger ausgeführt. Das bestätigt, was wir zu Beginn erwähnt haben: WebAssembly bietet eine vorhersehbare Leistung. Unabhängig von der ausgewählten Sprache ist die Abweichung zwischen Browsern und Sprachen minimal. Genauer gesagt: Die Standardabweichung von JavaScript in allen Browsern beträgt etwa 400 ms, während die Standardabweichung aller unserer WebAssembly-Module in allen Browsern etwa 80 ms beträgt.

Aufwand

Ein weiterer Messwert ist der Aufwand, den wir für die Erstellung und Integration unseres WebAssembly-Moduls in Squoosh betreiben mussten. Es ist schwierig, dem Aufwand einen numerischen Wert zuzuweisen. Daher erstelle ich keine Grafiken. Ich möchte aber auf Folgendes hinweisen:

AssemblyScript war unkompliziert. Mit diesem Tool können Sie nicht nur TypeScript zum Schreiben von WebAssembly verwenden, was die Codeüberprüfung für meine Kollegen sehr einfach macht, sondern es werden auch klebstofffreie WebAssembly-Module erstellt, die sehr klein und leistungsstark sind. Die Tools im TypeScript-Ökosystem, wie prettier und tslint, funktionieren wahrscheinlich einfach weiter.

Rust in Kombination mit wasm-pack ist ebenfalls äußerst praktisch, eignet sich aber besser für größere WebAssembly-Projekte, bei denen Bindungen und Speicherverwaltung erforderlich sind. Wir mussten ein wenig vom Happy Path abweichen, um eine wettbewerbsfähige Dateigröße zu erreichen.

C und Emscripten haben ein sehr kleines und leistungsstarkes WebAssembly-Modul erstellt, aber ohne den Mut, den Glue-Code auf das Nötigste zu reduzieren. Die Gesamtgröße (WebAssembly-Modul + Glue-Code) ist daher ziemlich groß.

Fazit

Welche Sprache sollten Sie also verwenden, wenn Sie einen JS-Hotpath haben und ihn schneller oder konsistenter mit WebAssembly machen möchten? Wie immer bei Fragen zur Leistung lautet die Antwort: Es kommt darauf an. Was haben wir also ausgeliefert?

Vergleichsgrafik

Wenn wir den Kompromiss zwischen Modulgröße und Leistung der verschiedenen von uns verwendeten Sprachen vergleichen, scheint entweder C oder AssemblyScript die beste Wahl zu sein. Wir haben uns entschieden, Rust zu veröffentlichen. Dafür gibt es mehrere Gründe: Alle bisher in Squoosh enthaltenen Codecs werden mit Emscripten kompiliert. Wir wollten unser Wissen über das WebAssembly-Ökosystem erweitern und in der Produktion eine andere Sprache verwenden. AssemblyScript ist eine gute Alternative, aber das Projekt ist relativ jung und der Compiler ist nicht so ausgereift wie der Rust-Compiler.

Auch wenn der Unterschied in der Dateigröße zwischen Rust und den anderen Sprachen im Streudiagramm ziemlich drastisch aussieht, ist er in Wirklichkeit nicht so groß: Das Laden von 500 B oder 1,6 KB dauert selbst über 2 Gbit/s weniger als ein Zehntel einer Sekunde. Und Rust wird diese Lücke in Bezug auf die Modulgröße hoffentlich bald schließen.

Bei der Laufzeitleistung ist Rust durchschnittlich in allen Browsern schneller als AssemblyScript. Insbesondere bei größeren Projekten ist es mit Rust wahrscheinlicher, dass schneller Code generiert wird, ohne dass manuelle Codeoptimierungen erforderlich sind. Das sollte Sie jedoch nicht davon abhalten, das zu verwenden, mit dem Sie am besten vertraut sind.

Alles in allem war AssemblyScript eine tolle Entdeckung. So können Webentwickler WebAssembly-Module erstellen, ohne eine neue Sprache lernen zu müssen. Das AssemblyScript-Team ist sehr hilfsbereit und arbeitet aktiv an der Verbesserung seiner Toolchain. Wir werden AssemblyScript in Zukunft im Auge behalten.

Update: Rust

Nach der Veröffentlichung dieses Artikels hat uns Nick Fitzgerald vom Rust-Team auf sein hervorragendes Buch zu Rust Wasm hingewiesen, das einen Abschnitt zur Optimierung der Dateigröße enthält. Durch das Befolgen der Anleitung dort (insbesondere durch die Aktivierung von Optimierungen zur Linkzeit und die manuelle Panikbewältigung) konnten wir „normalen“ Rust-Code schreiben und wieder Cargo (das npm von Rust) verwenden, ohne die Dateigröße zu vergrößern. Das Rust-Modul hat nach gzip eine Größe von 370 B. Weitere Informationen finden Sie in der PR, die ich bei Squoosh eingereicht habe.

Ein besonderer Dank geht an Ashley Williams, Steve Klabnik, Nick Fitzgerald und Max Graey für ihre Unterstützung auf diesem Weg.