Es gab eine dunkle Zeit im Mittelalter, da war ein Browser dazu da, Dokumente zu teilen und Wissen auszutauschen. Heute leben wir in einer Zeit von Entertainment-Plattformen, Abo-Modellen und weiteren wunderbaren Erfindungen wie DLCs in Computerspielen, Quatsch bei Seite. Ich bin kein Fan von DLCs und Abo-Modellen. Aber das Internet und der Browser haben sich weiterentwickelt. Dass immer mehr über den Browser ohne zusätzliche Programme möglich ist, im Gegenteil, sogar Programme wie Figma, Discord oder auch mein Knowledge Management Programm Logseq verwenden die Chromium Engine in ihrem Desktop-Client. Das haben Projekte wie Electron und Tauri möglich gemacht.
Es gibt viele Programme gerade im Bereich von 3D und Grafiken, die einfach zu komplex sind, um mit JavaScript umgesetzt werden zu können. Diese Programme wollen auch von den Vorteilen des Webs profitieren, dass zum Beispiel nicht mehr jedes Programm heruntergeladen werden muss. Zudem gibt es auch ältere Programme, bei denen die Firmen ihre komplexen Algorithmen nicht noch einmal komplett neu schreiben wollen für das Web. Diese auch im Sinne von Single Point of Truth, sodass die Daten immer exakt gleich gerendert werden können, egal, wo sie eingesetzt werden. Genau für diese Fälle wurde WebAssembly entwickelt, das von jedem modernen Browser von Chrome bis Safari unterstützt wird.
WebAssembly interagiert ähnlich wie JavaScript auch in einer Sandbox, um keine direkten Systemzugriffe zu haben. Es unterliegt also weiterhin wie jeglicher JavaScript-Code denselben Systemrestriktionen, um keine Sicherheitsrisiken zu erzeugen.
JavaScript arbeitet im Browser mit einem "Just-in-Time" (JIT) Compiler, dieser sorgt dafür, dass der Code bereits optimiert ausgeführt wird. Zum Beispiel kann JIT erkennen, ob ein Code mehrfach ausgeführt wird und diese Regionen als Warm oder Heiß markieren und diese in optimierter Variante für das nächste Ausführen dieser Region vorhalten. Dadurch, dass die Kompilierung allerdings immer zur Ausführungszeit stattfindet, gibt es in der Performance auch Nachteile, die sich allerdings erst bei großen komplexen Anwendungen zeigen. Bei WebAssembly hingegen ist das anders, denn es ist eine Art von Intermediate Language, also eine Zwischensprache, in die kompiliert werden kann, so können dann Sprachen wie C, C++, Rust oder Go direkt eine .wasm
Datei bereitstellen. Da WebAssembly in einem effizienten Binärformat geliefert wird, kann WebAssembly schneller zum Maschinencode kompiliert werden. Dies führt zu Performancevorteilen bei rechenintensiven Anwendungen, da weniger Zeit für die Kompilierung und Optimierung im Browser aufgewendet werden muss.
Allerdings lässt sich nicht pauschal sagen, jede WebAssembly-Anwendung wird besser laufen als eine Umsetzung mit JavaScript, zum Beispiel, wenn viele Dom-Manipulationen nötig sind und wenig Berechnungen. Die Interaktion auf einer Oberfläche zum Beispiel, denn WebAssembly greift bei diesen Interaktionen auch wieder auf die JavaScript-API zurück, da WebAssembly keine eigenen APIs zum Browser bekommen hat.
Jetzt haben wir davon gesprochen, dass WebAssembly, genauer gesagt, die .wasm
Dateien aus Sprachen wie C, C++ oder Go erzeugt werden können; natürlich gibt es hier noch viele weitere. Neben dieser Variante WebAssembly zu erhalten, ist es auch möglich, selbst im WebAssembly Textformat Programme zu schreiben und mit dem wat2wasm
Tool aus dem WebAssembly Binary Toolkit das Ganze dann in eine WASM Datei zu kompilieren.
WebAssembly kann für verschiedene Prozesse interessant sein, persönlich sehe ich einen großen Vorteil dort drinnen für Firmen, die bereits viel Quelltext haben und basierend darauf eine neue Anwendung bereitstellen wollen, im Web. Beispielhaft ist das Programm AutoCAD von Autodesk, das dank WebAssembly Performant im Browser verfügbar ist. Ich kann dir den Talk „AutoCAD & WebAssembly: Moving a 30 Year Code Base to the Web“ ans Herzen legen, falls du mehr über diese Transformation erfahren willst.
Ein weiterer Anwendungsfall für WebAssembly könnte sein für Funktionen, die im Browser keine passende Implementierung haben, aber das Ganze ausschließlich über JavaScript zu lösen zu kostspielig wäre. Beispielsweise in Rust gibt es bereits eine passende Lösung, die als WebAssembly Modul bereitgestellt werden kann. Ein Beispiel hierfür gibt es beim Projekt Discourse – für alle, die Discourse nicht kennen, dabei handelt es sich um eine Forensoftware – dort wurde eine clientseitige Komprimierung von Bildern implementiert, um Traffic und Speicherkosten zu reduzieren. Es war naheliegend, die Komprimierung im Browser zu implementieren, da bei schlechten Netzwerkverbindungen am Smartphone das Bild bereits vor dem Upload komprimiert wurde und so sich nicht auf das Datenvolumen der Nutzer auswirkte. Zusätzlich als netter Nebeneffekt wurde so die Last auf den Server auch noch reduziert. Ein inhaltlich spannender Artikel mit dem Titel "Faster (and smaller) uploads in Discourse with Rust, WebAssembly and MozJPEG" hat Discourse veröffentlicht.
Zu guter Letzt habe ich noch mein Lieblingsbeispiel mitgebracht, da ich selbst gerne mit der Software arbeite und nie Performance-Schwierigkeiten festgestellt habe, seitdem ich es verwende, und zwar Figma. Durch die vielen Berechnungen von Grafiken und Rendering, ist die Software prädestiniert für den Einsatz von WebAssembly; bereits 2017 hat Figma von asm.js auf WebAssembly gewechselt. asm.js war sozusagen der Vorgänger, dabei wurde C++ Code im Fall von Figma nach JavaScript compiliert, um dann im Browser ausgeführt werden zu können. Das Projekt "asm.js" hatte bereits eine reduzierte Basis von JavaScript verwendet, um eine bessere Performance zu liefern und C-Code in JavaScript übersetzen zu können. Durch den Einsatz von WebAssembly konnte Figma die Performance noch einmal verbessern, um das Dreifach. Was wunderschön zeigt, wo WebAssembly seine Stärken gegenüber JavaScript ausspielen kann.
Nachdem wir kennengelernt haben, was mit WebAssembly möglich ist, will ich dir ein kleines WebAssembly-Programm im WebAssembly Text Format(.wat
) zeigen, das ich als Beispiel für diesen Artikel geschrieben habe. In der Realität würde es sich nicht lohnen, für diese rudimentäre Aufgabe ein WebAssembly-Programm zu schreiben; es dient nur der Demonstration.
Die Funktion, die ich hier beschrieben habe, dient dazu, das Trinkgeld zu berechnen, indem ein Prozentwert abschließend berechnet wird und auf den Gesamtpreis addiert wird. Was direkt auffällt, dass man hier auf dem Stack arbeitet und zuerst aus den Parametern die Werte auf den Stack legt. Anschließend werden die Operationen ausgeführt, um das Ergebnis zu berechnen. Für mich definitiv keine Sprache, die man produktiv selbst von Hand schreiben möchte.
(module
;; die Funktion calcTip definieren, mit zwei Parametern und einem Rückgabewert vom Typ float 32
(func $calcTip (param $total f32) (param $percent f32) (result f32)
;; den Gesamtbetrag auf den Stack legen
local.get $total
;; den Prozentsatz auf den Stack legen
local.get $percent
;; den Prozentsatz in einen Dezimalbruch umwandeln
f32.const 100
;; den Prozentsatz durch 100 teilen
f32.div
;; den Betrag mit dem Prozentsatz multiplizieren
f32.mul
;; den Gesamtbetrag auf den Stack legen
local.get $total
;; Trinkgeld zum Betrag addieren, um die Gesamtsumme zu erhalten
f32.add
)
;; die Funktion calcTip exportieren
(export "calcTip" (func $calcTip))
)
Mit dem Tool wat2wasm
konnte ich die Datei dann sehr einfach in ein .wasm
File Kompilieren, das Binary Toolkit kann auch ausgesprochen einfach via NPM installiert werden dort ist es das Paket „wabt“, folgenden Befehle habe ich dann ausgeführt: npx -p wabt wat2wasm tip.wat
daraus entsteht dann eine Datei tip.wasm
die via JavaScript ausgeführt werden kann.
Eine WebAssembly-Datei ohne den Einsatz von JavaScript hilft dir wenig weiter, denn die Einbindung von WebAssembly erfolgt im Browser immer über JavaScript. Es gibt ein neues WebAssembly-Interface, das zur Integration von WebAssembly mit JavaScript da ist. Diese wird benötigt, um WebAssembly Dateien auszuführen. In diesem Beispiel laden wir uns die tip.wasm
via fetch
anschließend wird sie von der Methode instantiateStreaming
ausgeführt und wir können auf unseren definierten Export aus dem WebAssembly File zugreifen und in diesem Fall das Trinkgeld berechnen.
WebAssembly.instantiateStreaming(fetch('tip.wasm')).then(tipWasm => {
const tipCalculator = tipWasm.instance.exports.calcTip;
const bill = 150;
const tip = 15;
const total = tipCalculator(bill, tip);
console.log(total);
});
In der Praxis wird man selten diesen Adapter selbst schreiben müssen. Den Sprachen wie Golang oder C stellen bereits etwas JavaScript zur Verfügung, das verwendet werden kann. Zum Beispiel kann man auch noch Imports in WebAssembly hereinreichen, um dann auf bestimmte Funktionen zugreifen zu können, aber das würde hier in dem Artikel zu weit gehen. Im Idealfall beherrschst du für dein Projekt eine Sprache wie Rust, Golang, C oder C++, die du dann kompilieren lässt in WebAssembly.
Du brauchst dann je nach Umsetzung deiner Software nur noch die Exportieren-Funktionen in deinen JavaScriptcode zu verwenden, du stellst sozusagen eine neue Schnittstelle zur Verfügung.
Wir haben nun gelernt, was WebAssembly ist, wie wir WebAssembly nativ einsetzen können, was noch fehlt? Wie kann ich mein Programm jetzt in eine WebAssembly File übersetzen? Meine Sprache der Wahl ist Golang, da sie im Arbeitsleben meine primäre Sprache ist.
Als Erstes benötige ich eine kleine Projektidee. Mein Gedanke ist, ich möchte gerne einen md5 Hash erzeugen können in JavaScript, da der Browser selbst keine hat, nehme ich doch die md5
Methode aus dem Crypto Package von Golang. Kleine Randnotiz md5
verwende ich hier nur für Demonstrationszwecke. Falls du produktiv mit Hashing arbeitest, solltest du md5
nicht für Sicherheitsneuralgische Punkte einsetzen.
Es ist wichtig, dass wir einen Channel definieren, der offen bleibt in Golang, um die Ausführung nicht zu beenden und die definierten Funktionen immer abrufen zu können. Zusätzlich gibt es ein neues Syscall Package js
diese stellt einige Schnittstellen bereit, damit wir eine neue Funktion exportieren können und auf die JavaScript-Values zugreifen können. Die js.Global()
Funktion ist recht ähnlich zu dem, was wir bereits aus WebAssembly kennen, das Exportieren.
package main
import (
"crypto/md5"
"fmt"
"io"
"syscall/js"
)
func main() {
c := make(chan struct{}, 0)
js.Global().Set("md5", js.FuncOf(md5Hash))
<-c
}
func md5Hash(this js.Value, args []js.Value) any {
if len(args) != 1 {
return nil
}
hash := md5.New()
io.WriteString(hash, args[0].String())
return fmt.Sprintf("%x", hash.Sum(nil))
}
Die Args sind alle unsere Values, die wir als Argumente an die Funktion übergeben im JavaScript, unabhängig davon wie viele, wir müssen in Go sehr genau definieren, was wir erwarten und wie wir das prüfen wollen. Wenn wir uns sicher sind, was wir als Wert haben, können wir mit diesem Wert arbeiten und wie hier einen Hash-Algorithmus ausführen. Mit dem Befehl GOOS=js GOARCH=wasm go build -o hash.wasm
können wir von Go direkt nach WebAssembly kompilieren; die daraus entstehende Datei ist in meinem Beispiel 2,1 MB groß.
Unter https://github.com/golang/go/blob/master/misc/wasm/wasm_exec.html findest du ein Beispiel, wie du die WASM Datei einbinden kannst und dann im Global Scope hast, um mit den Methoden zu arbeiten. Wichtig ist, dass du die wasm_exec.js
bei dir hast, den Golang definiert noch verschieden importObject
, die dann in Golang als Schnittstellen zur Verfügung stehen, um mit dem Browser gut interagieren zu können.
Wenn du das komplett hast, kannst du die neue md5
Funktion verwenden und deine Hashs im Client, basierend auf dem hinterlegten Algorithmus, der in Golang geschrieben ist, berechnen.
Während ich den Artikel geschrieben und dafür recherchiert und die Beispiele von Figma und Auto CAD gesehen habe, war ich beeindruckt, dass WebAssembly schon eingesetzt wird. Ich hatte, bevor ich mich jetzt noch einmal so intensiv mit dem Thema beschäftigt habe, immer den Eindruck, dass es WebAssembly zwar gibt, aber keiner es wirklich einsetzen würde. Eine Täuschung, der ich unterlegen war, wie du heute gelernt hast!
Ich denke, für die meisten Webseiten wird sich wenig verändern; den WebAssembly wird meist ein „Mit Kanonen auf Spatzen schießen.“ sein, der Einsatz sollte gut überlegt werden, ob es sich lohnt. Bei 08/15 Anwendungen, die ein Text-Interface haben, zwei Zahlen zusammenrechnen sicherlich kein guter Einsatzpunkt. Wenn es allerdings darum geht, komplexes Rendering zu gestalten oder Bilder direkt im Client zu komprimieren, könnte WebAssembly eine gute Lösung sein. Während meiner Recherche bin ich auch auf ein Projekt namens Vugu gestoßen. Dieses Projekt möchte eine Alternative zu Vue, React oder Angular sein, aber arbeitet mit Golang und kompiliert nach WebAssembly. Dass diese ein Weg sein könnte, mag ich stark zu bezweifeln. Da gerade bei vielen Interaktionen mit dem DOM die Performance-Vorteile von WebAssembly nicht mehr wirklich herausstechen können, ich bin gespannt auf die Zukunft, vielleicht liege ich hier auch falsch.
Einen anderen Blick hatte ich in diesem Artikel bewusst außer Acht gelassen, da WebAssembly im Browser in einer geschlossenen Sandbox läuft; einem Container sozusagen, gab es auch erste Projekte, WASM über Docker lauffähig zu machen. Auch der Einsatz innerhalb von NodeJS ist möglich, aber in diesem Artikel wollte ich mich auf den Einsatz im Browser konzentrieren.
Hinterlasse mir gerne einen Kommentar zum Artikel und wie er dir weitergeholfen hat beziehungsweise, was dir helfen würde das Thema besser zu verstehen. Oder hast du einen Fehler entdeckt, den ich korrigieren sollte? Schreibe mir auch dazu gerne ein Feedback!
Es sind noch keine Kommentare vorhanden? Sei der/die Erste und verfasse einen Kommentar zum Artikel "WebAssembly: Die nächste Evolutionsstufe für Webanwendungen?"!