ESP32/ESP8266: Optimizing the hell out of ESPAsyncWebServer

Das schöne an dem Microcontroller ist ja, dass man einen kleinen WebServer darauf laufen lassen kann. Damit hat man eine tolle Möglichkeit ein kleines aber immer verfügbares User Interface (UI) bereitzustellen, das mit fast allen modernen Geräten die einen Webbrowser haben (Tablet, Smartphone, Laptop, PC) sofort funktioniert.

WebServer in klein

Exception Decoder MenuWas man jedoch schnell vergisst, wenn man zuvor schon WebApplications mit WebServern wie nginx oder Apache ausgeliefert hat, ist der Ressourcenbedarf von so einem Server. Der Microcontroller ist halt klein deshalb ja auch „Micro“, hat wenig Speicher und wenig CPU-Zyklen die er für sowas entbehren kann.

Die Kunst liegt also in der Beschränkung. Doch das fällt sauschwer, wenn man erstmal einen gewissen Anspruch an seine Webseiten hatte. Niemand mag gern zurück in die Steinzeit und eine Webseite bauen z.B. ohne Stylsheets. Klar, das geht, sieht aber so schlimm aus und funktioniert auf dem Smartphone z.B. so gar nicht.

Zunächst habe ich den klassischen Weg zur Gestaltung meiner Webseite gewählt, ich hab HTML geschrieben mit Bildern darin und JavaScript das dynamisch Bilder austauscht und platziert, ich habe CSS geschrieben, das Bilder zur Gestaltung einsetzt. Es sieht wunderschön aus auf dem Desktop, aber… es crasht den WebServer des Microcontrollers. Da nutzt dann das ganze tolle Design auch nichts, wenn es letztlich nicht funktioniert.

Und ich habe zig Crashes gehabt. Ich habe mir mühsam den Arduino ESP8266/ESP32 Exception Stack Trace Decoder in der Arduino IDE installiert (Anleitung). Ein dekodierter Stacktrace sieht dann z.B. so aus:

Da sieht man, dass es im konkreten Fall eher ein Speicherproblem ist bei diesem Crash, weil eine alloc Funktion involviert ist. Aber viele andere Crashes sind eben anderer Natur.

Optimizing the hell out of it

Dann beginnt man zu begreifen und optimiert. Was kann man alles tun um zu optimieren? Anfangs dachte ich die Dateigröße wäre der Bottleneck des Microcontrollers. Ich hab die JS, CSS und HTML Dateien mittels GZIP komprimiert und mit dem content-type/gzip im Header ausliefern lassen. Das reduziert auch die Menge an Daten hervorragend. Oft wird nur noch ein Zehntel der Daten übertragen, was die Auslieferung selbst deutlich schneller macht.

Doch die Dateigröße ist gar nicht das Problem beim ESPAsyncWebServer. Das eigentliche Limit ist, dass der nur maximal 3 bis 4 Requests „gleichzeitig“ bzw. parallel bearbeiten kann. Der Rest der Requests hängt dann einfach und wartet ewig auf Antwort und wird verworfen oder führt zu einer Blockade des Webservers und einem späteren Crash. Es hat auch nichts damit zu tun, dass das SPIFFS Filesystem eine Obergrenze an gleichzeitig geöffneten Files hat wie mir ein Entwickler des ESPAsyncWebServer versicherte. Und darauf nun eine passende Antwort bzw. Optimierung zu finden ist gar nicht so einfach gewesen.

Klar, man kann das CSS und das JS alles mit in das HTML-file kippen, aber dann wird es schon arg unübersichtlich. Und des weiteren bleibt das Problem mit den vielen kleinen Icons bestehen, die alle je einen Request auslösen. Das kostet Zeit und trifft auf die Limitierung des Webservers. Das Hauptziel kann also zunächst nur sein die Anzahl initialer Requests (also der Requests die benötigt werden die Seite einmal komplett zu laden) zu minimieren. Und zwar sollte das so passieren, dass man unterhalb des Limits bleibt von max. 3-4 gleichzeitig eintreffenden Requests. Das ist natürlich ein Limit, dass sobald mehr als ein Nutzer gleichzeitig auf das UI zugreift bereits zu Problemen führen kann. Es ist schade, dass die Entwickler von ESPAsyncWebServer das nicht vorhergesehen haben und entsprechend zuviele Requests schlicht und einfach mit Fehler 503 ablehnen sondern stattdessen den Server crashen lassen. Aber vielleicht gibt es auch einen Grund der mir nicht begreiflich ist derzeit.

The Magic of Data-URI’s

Hier kommt dann jetzt eine Optimierungsstrategie zum Zuge, die auch bei „normalen“ Webseiten zu krass verbesserter User Experience (UX) führen kann. Das Einbetten der Bilddaten als sogenannte Daten-URI. Was ist das eine Daten-URI?

The data URI scheme is a uniform resource identifier (URI) scheme that provides a way to include data in-line in Web pages as if they were external resources. It is a form of file literal or here document. This technique allows normally separate elements such as images and style sheets to be fetched in a single Hypertext Transfer Protocol (HTTP) request […]

Das schreibt die Wikipedia dazu. und auf dieser Webseite heißt es „You can embed the image data directly into the document with data URIs.“ und genau das ist eine sehr sinnvolle Sache. Einerseits wird das Dokument (HTML, JS, CSS) in das man diese Daten dann einbettet zwar etwas größer, aber dafür unterbleiben Requests für Bilddaten komplett.

Aus folgendem Code für ein Favicon als Meta-Link im Header in HTML…

<link rel='icon' type='image/png' sizes='32x32' href='fav-icon-32x32.png'>

…wird folgender Code als eingebettete URI:

<link rel='icon' type='image/png' sizes='32x32' href='data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA9tpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuMy1jMDExIDY2LjE0NTY2MSwgMjAxMi8wMi8wNi0xNDo1NjoyNyAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wUmlnaHRzPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvcmlnaHRzLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcFJpZ2h0czpNYXJrZWQ9IkZhbHNlIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9ImFkb2JlOmRvY2lkOnBob3Rvc2hvcDowMzhmMTQ5NC1lNGJkLTExZTEtOGQ4MC1jYjVjZDBjYjc4N2QiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6RDJDODA5ODk0RjU1MTFFQThBRjNFQkJENEQ2MUU4MDUiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6RDJDODA5ODg0RjU1MTFFQThBRjNFQkJENEQ2MUU4MDUiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIENTNiAoTWFjaW50b3NoKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjAyODAxMTc0MDcy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'>

Der entscheidende Teil ist, dass man statt einer Hyperlink-URI eine Data-URI nutzt, die eben so ausschaut:
data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAA

Als Werkzeug zur Umwandlung gibt es zahlreiche Online Tools dafür, wie z.B. den Image to data-URI converter. Dort kann man seine Grafikdateien umwandeln lassen. Es gibt auch apps z.B. für macOS den Data URI Converter von Chris Yip.
Mit dem richtigen Tool ist es jedenfalls schnell gemacht.

Ergebnis: Schnell, schneller, embedded Data-URI

Die Ergebnisse sprechen absolut für sich. Nachfolgend habe ich mit zwei Screenshots im Safari Browser festgehalten, die initialen Requests und deren Laufzeit und Datenumfang, die gegen den Controller gehen im zeitlichen Verlauf. Die Ergebnisse sind absolut eindeutig: Data-URI schlägt den klassischen Weg um Längen.

Man erkennt sofort, dass nach dem Laden der initialen HTML Seite core.html weitere Requests ausgelöst werden, klar ist die core.css und core.js werden benötigt, um die Seite überhaupt korrekt anzuzeigen. Aber allein schon die erste Seite auszuliefern benötigt 1,14 Sekunden Zeit, und dann wird es problematisch, da gleich fünf weitere Requests losgehen gleichzeitig, unter anderem eben auch requests wegen Grafiken. Das schafft der Server eben manchmal nicht. Und dann sieht man auch noch, dass jedes Bild ca. 300 bis 700 Millisekunden Zeit benötigt, und das ist sehr lang!! Die gesamte Seite ist erst nach über sechs Sekunden geladen.

Die gleiche Seite optimiert mit Data-URI’s verhält sich deutlich besser. Zwar wird der initiale Download der core.html um etwa 15KB größer, interessanterweise aber kostet das kaum Zeit. Und der initiale Download führt zu lediglich zwei weiteren Requests für die core.css und core.js. Beide Dateien enthalten selbst wiederum ausschließlich Data-URI’s und führen somit zu keinen Folgerequests.

Die Auswirkung ist enorm. Die Bilddaten sind nun in 10 bis 20 Millisekunden geladen statt wie vorher in 300 bis 700. Eine Verbesserung um Faktor zehn. Insgesamt ist die ganze Seite vollständig geladen nach nur etwas über zwei Sekunden. Das erspart satte 4 Sekunden Wartezeit.

Fazit

Wer Webseiten auf dem Microcontroller ausliefert, der sollte ausschließlich inline Data-URI’s für grafische Inhalte verwenden und die Anzahl der Requests soweit wie möglich senken. Für mein Setup bestünde jetzt noch die Option auch die core.css und core.js in die core.html zu integrieren. Dann wäre nur noch eine große Datei nötig.

Aus Gründen der Code-Übersicht sehe ich davon aber erstmal ab. Sollte es aber nochmal Probleme mit dem WebServer geben, dann kann ich hier gezielt weiter optimieren.

Why do I blog this? Das Problem des instabilen und ständig crashenden ESPAsyncWebServer hat mich einige endlos erscheinende Stunden des Debuggens gekostet. Weil ich auch einfach nicht wusste was den WebServer crasht und/oder blockiert und Serial.print() statements haben nicht wesentlich geholfen. Ich hoffe es hilft anderen nicht in die gleiche Falle zu tappen. Sehr hilfreiche Hinweise zum Thema ESPAsyncWebServer habe ich überigens im Diskussionsforum/Chat zu dem Server gefunden. Das hat mir sehr geholfen.

Ein Gedanke zu „ESP32/ESP8266: Optimizing the hell out of ESPAsyncWebServer“

  1. Hallo,

    das sind ja sehr interresannte Informationen zum ESPAsyncWebServer.
    Ich habe eine Website mit ca. 25 kb und meinESP8266 oder der ESP32 crashen oft.
    Der Tip mit dem GZIP werde ich mir anschauen.
    GIFs oder PNGs hatte ich mir verkniffen.
    Hier könnte nun doch etwas gehen.
    Ich wollte schon das SPIFFS verwenden und die Dateien in das FS legen.
    Ich bekomme es aber nicht zum Laufen.
    Ich benutze aber auch das Websocket um Daten aus dem ESP auf der Seite anzuzeigen.

    Vielen Dank für deine Tipps.

    Gruß
    Andreas

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.