Verbesserte Web-App-Performance durch optimierte Hardware-Nutzung
Web-Anwendungen können auf vielfältige Weise entwickelt werden. Allerdings werden nicht alle Performance-Tweaks beachtet, die notwendig sind, um die steigenden Performance-Erwartungen zu erfüllen. Insbesondere wenn rechenintensive Services angeboten werden sollen, bietet der Einsatz leistungsfähiger Server Vorteile. Doch gerade bei der Übertragung der Daten zum Client bildet die Latenz oft den limitierenden Faktor. Unser Ziel ist es daher, abzuwägen: Lohnt sich ein leistungsstarker Server oder bringt optimierter Client-Code mehr Performance?
Disclaimer: Die Grafiken sind alle selbst erstellt und dienen der Veranschaulichung der Thematik. Die Grafiken sind vereinfacht und stellen nicht die genaue komplexe Funktionsweise dar.
Beispielanwendung: Bildkomprimierung
Stellen wir uns eine einfache Anwendung vor, die Bilder komprimiert. Kleinere Datenmengen bedeuten kürzere Übertragungszeiten und damit ein schneller ladendes Web-Erlebnis. Das Grundkonzept:
- Upload: Der Nutzer lädt ein oder mehrere Bilder hoch.
- Verarbeitung: Mittels Bibliotheken oder Canvas-Techniken wird beispielsweise ein JPEG komprimiert.
- Download: Das komprimierte Bild kann anschließend heruntergeladen werden.
JavaScript-Performance und ihre Limitierungen
Es ist allgemein bekannt, dass
Web-Anwendungen oft durch die Limitierungen von JavaScript beeinträchtigt werden
– einer Sprache, die in modernen Browsern von Engines wie V8 (Chrome),
SpiderMonkey (Firefox) oder JavaScriptCore (Safari) ausgeführt wird.
Ablauf bei der Code-Ausführung:
- Parsing: Der Quellcode wird in einen abstrakten Syntaxbaum (AST) umgewandelt.
- Interpretation: Ein Interpreter liest den AST und übersetzt ihn in Bytecode, der von der Engine ausgeführt werden kann. Dieser Schritt ist relativ schnell, führt aber zu weniger optimiertem Code.
- JIT-Optimierung: Häufig verwendete Codeabschnitte („Hot Spots“) werden in effizienten Maschinencode umgewandelt.
Herausforderungen:
- Laufzeit-Overhead: Der dynamische Charakter von JavaScript führt zu zusätzlichem Overhead im Vergleich zu vorkompilierten Sprachen.
- Single-Threaded: Da JavaScript in der Regel in einem einzigen Thread läuft, sind parallele Verarbeitungen limitiert.
- Garbage Collection: JavaScript verwendet eine automatische Speicherverwaltung. Der Garbage Collector kann zu unerwarteten Leistungseinbrüchen führen, da die Speicherbereinigung unvorhersehbar erfolgt.
Wie können wir unsere Anwendung Schritt für Schritt verbessern?
Die Endgeräte werden immer besser, und es ist nichts Neues, dass Webanwendungen in ihrer Grundform die Hardware nicht wirklich nutzen. Genau hier setzen wir an: Durch gezielte Optimierungen können wir die Performance unserer Anwendung Schritt für Schritt verbessern. Das fängt bei der Optimierung des JavaScript-Codes selbst an - aber dafür haben wir ja OpenAI, DeepSeek und Co. -, geht über die effiziente Nutzung der Browser-APIs und endet bei der Integration von Technologien wie WebAssembly, WebGPU usw., die es ermöglichen, rechenintensive Aufgaben deutlich schneller auszuführen. Im Folgenden wollen wir uns anschauen, welche konkreten Webtechnologien wir in unserem Projekt einsetzen können, um das Beste aus unserer Webanwendung herauszuholen.
Unsere Beispielanwendung ist sehr einfach gehalten, daher gibt es gerade auch nicht unbedingt etwas was wir tun müssen, doch erweitern wir diese, kommt es schnell zu signifikanten Performanceproblemen. Wir ermöglichen dem Nutzer nicht nur das Hochladen eines einzelnen Bildes, sondern beliebig viele.
Hier wird die Problematik des einzelnen Threads direkt deutlich. Denn wir müssen in diesem Fall jedes Bild nacheinander einzeln komprimieren. Doch da gibt es die erste Lösung den Einschränkungen des Main-Threads zu entkommen. Es kommen die Web Worker ins Spiel.
Web Worker sind eine einfache Web Lösung, die es ermöglicht, JavaScript-Code in separaten Hintergrund-Threads auszuführen. Diese Threads laufen parallel zum Main-Thread, wodurch Aufgaben ausgelagert werden können, ohne die die Benutzeroberfläche zu beeinträchtigen.
Wie hier gezeigt, sind Web Worker sehr einfach einzubinden, man legt ein JS file
an, welches irgendeinen Code ausführt, hier als Beispiel einen Counter mit
Timeout. Würden wir den so direkt auf dem Main-Thread ausführen, blockiert er
die meisten anderen Aktivitäten. Rufen wir das File aber mit new Worker
auf,
dann starten wir einen weiteren Thread auf dem unser Counter läuft. Zwischen den
Threads kommunizieren wir dann in beide Richtungen mit onMessage und
postMessage. In diesem Fall bekommt der Main Thread alle 500ms eine Nachricht
und agiert dementsprechend, ist aber nicht von dem Timeout blockiert. Für unsere
Beispielanwendung bedeutet das, dass wir erst checken, wie viele Threads der
Client zur Verfügung hat und starten angemessen viele Web Worker für unsere
Anwendung. So können wir z.B. mit 4 Threads arbeiten. Die hochgeladenen Bilder
in eine Queue packen und die Threads diese nach und nach abarbeiten lassen, ohne
dass unsere UI- und Nutzerinteraktion beeinträchtigt werden.
Jetzt haben wir schon Parallelisierung eingebaut, welches unsere Anwendung durch Parallelisierung x-Fach schneller machen kann, je nachdem wie viele Threads wir sinnvoll verwenden können. Doch das ändert nichts an der Geschwindigkeit der einzelnen Aufgaben, diese haben immer noch den Javascript Overhead. Außerdem fällt uns auf, wenn wir zu verschiedenen Bildformaten konvertieren, um besser zu komprimieren, dauert es noch viel länger oder wir finden keine Libraries, welche De- und Encoding bestimmter Bildformate können.
Hier kommt eine weitere hervorragende Technologie ins Spiel, die viele unserer Probleme löst oder verbessert, aber die Implementierung ein wenig oder erst möglich macht. In der Bild- und Videoverarbeitung gibt es unzählige Algorithmen, aber die meisten sind in nativen Sprachen wie C++ und Rust geschrieben und haben oft keinen Javascript counterpart. Hier hilft uns WebAssembly oder kurz WASM.
WASM ist ein Bytecode-Format, welches in Web Browsern ausgeführt werden kann. Es ermöglicht die plattformübergreifende Nutzung von nativer Code-Leistung in Webanwendungen. Es dient dazu z.B. rechenlastige Operationen, welche außerhalb von JavaScript besser optimiert werden können, in den jeweiligen Sprachen bei fast nativer Geschwindigkeit auszuführen.
WASM können wir nun einsetzen um mehrere Dinge zu erreichen:
- Der Code ist bereits kompiliert und optimiert, damit ist er per se schon mal schneller als was wir mit interpretiertem Javascript erreichen können.
- Wir können jegliche Sprache verwenden, die einen WASM-Compiler anbietet.
- Zugriff auf angemessene Libraries aus anderen Sprachen.
- Durch WasmGC kann die Garbage Collection der kompilierten Sprache selbst verwendet werden.
Nehmen wir z.B. C++ und schreiben einen Encoder für das Avif Bildformat. Dieses
lässt eine sehr gute Komprimierung zu, weswegen wir es definitiv in unserer
WebApp anbieten wollen. Haben wir den C++ Code in einer aufrufbaren Funktion
geschrieben, können wir diese durch EMSCRIPTEN
BINDINGS dem Javascript Code zur Verfügung stellen. Das ganze dient als
Interface, damit, wenn wir den Code kompilieren, klar ist, wie von Javascript
auf was zugegriffen werden kann. Dabei können sogar Typescript-Typen erstellt
werden, wenn erwünscht. Das Resultat ist eine .wasm
file, das ist der
kompilierte Code, ein Type File, sofern erwünscht, und ein Javascript file um
auf das WASM File zuzugreifen. Instanziieren wir nun das Javascript file, können
wir daraus die encode
Funktion aufrufen. Das können wir direkt von unseren Web
Workers aus machen, dann haben wir sowohl Parallelisiert, als auch besseren,
schnelleren, bereits kompilierten Code.
Kommen wir also zum nächsten Schritt. Heutzutage ist ja alles irgendwie mit KI. Und warum sollte unsere Anwendung da nicht auch teilhaben? Ob wir ein weiteres Feature anbieten wollen, welches einem KI Modell bedarf, wie z.B. Hintergrundentfernung oder es einfach KI-Komprimiermodelle gibt, welche wir anbieten wollen.
Wenn wir aber von KI reden, denkt man direkt an Grafikkarten und wir lassen derzeit unseren Code nur auf der CPU laufen. Sind wir nun an dem Punkt angekommen, wo wir Code verwenden wollen, der schlichtweg dafür gedacht ist auf einer GPU zu laufen, bringt uns unser derzeitiges Setup nichts. Kommen wir also zu einer weiteren Hardware-Schnittstelle. WebGPU.
WebGPU (WebMetal bei iOS) ist ein Nachfolger beziehungsweise eine Alternative zu WebGL im Stil der nativen GPU APIs wie Khronos Vulkan, Microsofts DirectX und Apples Metal. Es ermöglicht die Nutzung der Features von modernen GPUs im Browser und wurde mit W3C entwickelt [20]. Chrome unterstützt WebGPU seit Version M113 (2.Mai 2023), jedoch mangelt es noch an Stabilität und Ressourcen um aktiv von JavaScript Bibliotheken verwendet zu werden. TensorflowJs bietet z.B. bereits die Möglichkeit an WebGPU zu nutzen sofern möglich.
Generell ist die eigenständige Nutzung hierbei ein wenig komplizierter, ist aber hervorragend wenn man KI Libraries wie TensorflowJs verwendet. Wichtig ist hierbei noch zu checken ob WebGPU bereits supported ist, da es noch zahlreiche Browser Versionen oder alte Geräte gibt, die dieses (noch) nicht unterstützen. Dann holt man sich den Adapter und GPU Zugriff und kann z.B. mit Shadern arbeiten und komplexe Matrix-Kalkulationen durchführen.
Um die Reise mit AI und Hardware Zugriff zu vervollständigen, gucken wir noch was uns die Zukunft liefert. Gehen wir also noch einen Schritt weiter und betrachten die Möglichkeit im Machine-Learning Bereich mit Neural Networks an.
Wie WASM und WebGPU bietet auch WebNN eine Abstraktionsschicht zur Hardware
Die Web Neural Network API definiert eine web-freundliche, hardware-agnostische Abstraktionsschicht, die die Fähigkeiten des maschinellen Lernens von Betriebssystemen und zugrunde liegenden Hardware-Plattformen nutzt, ohne an plattformspezifische Fähigkeiten gebunden zu sein. Die Abstraktionsschicht erfüllt die Anforderungen der wichtigsten JavaScript-Frameworks für maschinelles Lernen und ermöglicht es Webentwicklern, die mit der ML-Domäne vertraut sind, benutzerdefinierten Code ohne die Hilfe von Bibliotheken zu schreiben.
Evaluation
Technology | Emoji | Beschreibung |
---|---|---|
Web Workers | 😊 | Multithreading |
WASM | 🤓 | Komplexe Libs, Interpreter umgehen |
WebGPU | 🥵 | Zugriff auf GPU, AI Modelle & Animation (WebGL Alternative/Nachfolger) |
WebNN | ⚒️ | NPU Zugriff, Neural networks, AI Accelerator |
Fazit und Ausblick
Die Reise von der traditionellen Browser-Umgebung hin zur optimalen Nutzung moderner Hardware zeigt: Durch gezielte, technologieübergreifende Optimierungen kann die Performance von Web-Anwendungen signifikant verbessert werden. Dabei gilt es stets, den richtigen Kompromiss zwischen Server- und Client-seitiger Verarbeitung zu finden und die Komplexität nicht über das notwendige Maß hinaus zu steigern. Zukünftige Entwicklungen, etwa in Form kleinerer KI-Modelle oder weiterer Optimierungen durch WASM, werden diesen Spagat weiter erleichtern.
Weitere Fragestellungen, die den Rahmen dieses Beitrags sprengen, betreffen etwa:
- Wie stark müssen wir auf ältere Geräte Rücksicht nehmen?
- Lohnt sich die Auslagerung von Services auf den Client gegenüber Serverlösungen?
- Wie beeinflusst die Internetgeschwindigkeit die Notwendigkeit leistungsfähiger Endgeräte?
- Ist ein Ansatz mit minimalem JavaScript-Einsatz langfristig vorteilhaft?