Tech Talk: "JavaScript and (Non) Blocking UI" mit Klemens Loschy von SEQIS

Tech Talk: "JavaScript and (Non) Blocking UI" mit Klemens Loschy von SEQIS

Ja, grüße euch! Hallo, herzlich willkommen beim heutigen TechTalk mit dem spannenden Thema JavaScript und Non-Blocking-UI.

Bevor wir ins Thema hineinspringen, kurz noch Wer bin ich? Was mache ich? Wo komme ich her? Ich bin Klemens Loschy. Ich bin Teamlead bei razzfazz.io. Das ist eine eigene Abteilung von der Firma SEQIS. Die SEQIS an sich spezialisiert auf Projektmanagement, Qualitätssicherung und Business Analyse. Und seit ungefähr ein-, eineinhalb Jahren haben wir uns wieder auch diesen Bereich Development vorgenommen und da fokussieren wir jetzt hin und ich darf eben dort die Teamlead Rolle übernehmen. Ich komme aus einer langen Geschichte von Java Backend Entwicklung und habe da sehr viel Erfahrung sammeln dürfen. Aber wir haben uns fokussiert auf die Technologien Node.js und Vue.js und da bin ich jetzt ein bisschen mehr hinein gedriftet und darf dort jetzt Fullstack-Development machen. Und seitdem ich dorthin bin, gab es immer wieder Momente, wo ich gedacht habe „Wieso funktioniert das jetzt? Und wieso funktioniert es eigentlich nicht?“ Und der letzte Moment wo es mir so gegangen ist, den möchte ich euch heute gerne präsentieren und erklären, was der Hintergrund ist und was dann auch die Lösung ist.

Also es ging eben – oder es geht eben – um das Thema Blocking UI, was dazu geführt hat und wie wir es dann letztendlich auch gelöst haben. Zuerst noch ein Disclaimer und wohin sollte es heute gehen, was mein Ziel ist: Also ich habe mich schon damit beschäftigt natürlich mit dem ganzen Thema, weil ich hab ja das Problem auch gehabt. Aber ich studiert das Event Loop-Thematik nicht in depth. Also es gibt sicher Leute, die kennen sich noch viel aus besser als ich. Aber ich möchte zumindest an einen guten Überblick verschaffen. Mir war es wichtig, dass ich mich einfach damit auseinandersetze, das Problem verstehe und das ich auch eine Lösung verstehen kann. Weil ein Kollege ist dann gekommen und hat gemeint das ist die Lösung – hab ich gesagt „Das kann nicht die Lösung sein“. Aber es ist leider so. Aber das habe ich erst geglaubt, nachdem ich mich eben mehr und mehr damit beschäftigt habe. Ziel soll es in Wirklichkeit sein, dass ich Leute heute das Problem erkläre, zeige - eben Leute, die vielleicht auch so neu sind wie ich im JavaScript Bereich, wo es eben darum geht single threaded, non blocking, blocking und so - dass die sich besser damit auskennen und damit einen ungefähren Anhaltspunkt haben und damit das Thema Event Loop und Co. zumindest ein bisschen manifestiert wird in ihren Gehirnen. Gut. Ich hab dazu ein Sample mitgebracht, ein Setup. Das ist in Wirklichkeit ein ganz normaler Anwendungsfall. Wie ihn glaub ich die meisten von uns schon kennen. Ich klicke vom Button in einem Browser. Dann lade ich im Hintergrund Daten über den Web Request nach, die verarbeite ich dann irgendwie. Und währenddessen das Ganze passiert, möchte ich den Benutzer zeigen, dass etwas passiert. Weil es ist wichtig dem Nutzer irgendwie zu signalisieren, dass im Hintergrund etwas bewegt und deswegen versuche ich eben meine Lade-Animation, ein Overlay oder was auch immer einzublenden. Ich hab das umgesetzt mit unserem Standardtechnologie Stack, das ist Node.js und im Frontend ist das Vue.Js und ich lasse das ganze im Chrome in einer V8 JavaScript engine. In Wirklichkeit ist aber wurscht, ihr könnt da mit ganz normalen HTML und vanilla JavaScript und CSS auch arbeiten. Das Problem ist dasselbe und diese Frameworks sind weder die Ursache noch die Lösung. Also die Lösung hat gar nichts mit Node.js oder Vue.Js oder was auch immer zu tun - es ist allgemein so.

Das ist die Applikation und wenn jetzt die Technik mitspielt - ja perfekt. Ich habe das aufgeteilt in vier Versionen. Das zeigt so ein bisschen den iterativen Ansatz und wie man halt selber vorgegangen sind und wie wir dann letztendlich eben in dieses Problem hinein gestoßen sind. Also ihr seht da mal einen Ladebutton. Klar, das ist irgendwie so ein Button der die Daten lädt und dann letztendlich verarbeitet. Ich kann es prinzipiell auch wieder löschen die Daten und ich habe da zwei Schieberegler wo ich die Anzeige oder die Dauer der Response und der Dauer der Verarbeitung manipulieren kann damit man ein bisschen sieht wie sich das Problem dann mit der Zeit in Wirklichkeit ergibt. Am Anfang von der Applikation geht alles noch relativ schnell, 200-500 Millisekunden ist jetzt nicht superschnell, aber es ist noch akzeptabel sag ich einmal und ihr seht, dass innerhalb von ungefähr 7 Millisekunden da unten meine Antwort, meine Daten erscheinen. Das ist noch okay. Wenn die Applikation aber fortschreitet, kommen Features dazu, vielleicht muss ich mehrere Backends ansprechen, ich muss es vielleicht sequentiell machen weil die Daten nicht wir aufeinander aufbauen, es werden mehr Daten, die Verarbeitung wird komplexer und irgendwann geht es halt in die Höhe. Und irgendwann denke ich mir: aber jetzt – das dauert aber schon ärger lang. Also ungefähr zweieinhalb Sekunden. Und das ist eben der Zeitpunkt, wo ich dem Benutzer sagen muss irgendwas bewegt sich. Man sagt so im User Experience Bereich im Web alles über 2 Sekunden muss ich dem Benutzer irgendwie visualisieren.

Also arbeiten wir an V2. V2 geht in die Richtung ab 2 Sekunden mach ich eben diese Ladeanimation. Ich zeige ein Overlay an, dass was passiert. Unter 2 Sekunden lasse ich es einfach bleiben. Also so wie vorher unter 2 Sekunden passiert mal nichts oder nichts Neues und über 2 Sekunden jetzt, tatsächlich, man sieht dieses bekannte Overlay. Also die UI ist kurz gesperrt, ich zeig dem Benutzer, dass was passiert. Er kann in dem Moment nichts mit der UI machen und im Nachhinein wird das ganze wieder enabled sag ich mal und die Daten werden angezeigt. Cool, so haben wir es auch gemacht. Und haben wir uns gedacht: Naja, also eine Daten und Datenverarbeitung da fällt uns jetzt nichts Gutes ein wie wir das noch tunen können. Da sind wir der Meinung, da sind wir schon relativ gut, obwohl es lange dauert zugegebenermaßen, aber wir können auf der Response Duration arbeiten. Und zwar haben wir uns gedacht – oder ist in unserem Fall wirklich so gewesen –, dass wir öfters dieselben Request ins Backend geschickt haben. Und die Daten, die wissen wir, die verändern sich nicht so schnell. Also ist eigentlich unnötig so viel Requests zu schicken. Und das Einfachste, was man machen kann oder was zumindest uns aufgefallen ist was wir machen könnten ist, dass wir einen Request Cache einbauen. Dass also die Request Library in Wirklichkeit gar nicht zum Backend geht, sondern die Daten aus dem Cache holt. Cool, das ist in Wirklichkeit also nur eine kleine Konfiguration und das Ganze sollte hinhauen. Und wir merken, wir haben ungefähr in dem Beispiel 900 Millisekunden eingespart. Sehr cool. Also machen wir das. V3 - wir haben ja mit diesen Cache implementiert. Ohne Cache selbes Verhalten wir vorher - über 2 Sekunden sich die Ladeanimation. Dann haben wir den Cache enabled. Ich hab da die Response Time auf 0 gesetzt in dem Fall. Keine Ladeanimation mehr. Wir haben ja nur einen Cache eingebaut - was ist, was ist das Problem? Das müsste eigentlich viel schneller gehen und stattdessen blockiert die UI und wir sehen keinen Overlay mehr. Was ist passiert?

Und was passiert ist das möchte ich eben jetzt erläutern. Gut. Die Core-Komponenten von JavaScript, also JavaScript: Single Threaded. Das ist nun mal so. Es gibt einen CallStack wo die Methoden drauf landen die abgearbeitet werden.

Wenn ich asynchrone Methoden aufrufe, zum Beispiel set time, ist etwas typisches asynchrones - das kennt ja jeder schon von ganz lange her wahrscheinlich. Oder asynchrone Web Requests mache oder DOM-Manipulation mache. Das geht in Wirklichkeit über die Web APIs, die dann tatsächlich asynchron sind und der Callback landet letztendlich dann wieder auf der Callback Queue. Da kann man noch unterscheiden zwischen den Tasks und den Micro Tasks - ist aber in unserem Fall gar nicht weiter notwendig. Ich habe auch auf den weiteren Folien die Web-API ausgeblendet. Dies auch nicht zwingend notwendig, um das Problem mal prinzipiell zu verstehen.

Aber so läuft das Ganze ab. Die Event Loop ist dann dazu da, wenn der Callstack letztendlich leer ist, die Callbacks aus der Callback Queue wiederum auf den Callstack zu schieben, damit die dann wieder abgearbeitet werden vom main thread. So läuft das ungefähr ab.

Und jetzt schauen wir uns an, wie es in unserer Applikation de facto abläuft. Genau, das ist noch der Code, ihr seht es ist original nicht viel passiert, was da im Hintergrund abläuft. Die erste Zeile ist wir setzen das Loading Flag. Wenn sie über 2 Sekunden ist, macht man das Loading Flag auf true. Dann holen uns die Daten über einen Web Request. Ich hab das im Hintergrund natürlich ein bisschen gefaked, damit es jetzt fürs Beispiel gut passt, um mit dem Response Daten, die verarbeiten wir dann letztendlich mit einer komplexen Verarbeitung und am Ende setzt man wieder das Loading Flag auf false, damit das Overlay wieder weggeht. Also wie gesagt, jetzt nicht viel dahinter. So wie schaut das jetzt aber in unserem Stack, in unser Callback Queue aus? Ihr seht links den Browser, der symbolisiert ein bisschen kein Overlay, wir sind am Anfang. Wir klicken auf den Button.

Der Button-Klick löst dann die Methode loadData aus, die ich euch gerade gezeigt hab. Und der erste Schritt im loadData war wir setzen einmal die Ladeanimation auf True und damit das Overlay gezeigt wird, ganz klar. Im Hintergrund, das ist bisschen Vue.Js Magic. Im Hintergrund wird in Wirklichkeit dann eine Callback in die Callback Queue gestellt, damit tatsächlich auch die UI repainted werden kann damit das Overlay gezeigt wird.

Der nächste Schritt war wir holen uns die Daten vom Backend. Das passiert über den asynchrone Web Request. Das heißt ich schicke einen Web Request ab und warte auf die Daten. Das Gute ist, dass es asynchron abläuft - das heißt, ich blockiere in Wirklichkeit meine Applikation, ich blockiere die main thread nicht. Ich warte asynchron, dass die Daten wieder empfangen wurden und das passiert auch wieder über den Callback. Der Callback wird wiederum auf der Callback Queue landen und letztendlich kann ich mit meinem main thread weitermachen. Der Stack wird abgebaut und der Stack wird leer. Perfekt.

Jetzt triggert die Event Loup und sagt: Passt, der Stack ist leer, ich kann jetzt von der Callback Seite mir den nächsten Callback holen, der dran ist und in den Stack schieben. Der wird wieder abgearbeitet und der Browser wird mit einem Overlay versehen – perfekt. Stack ist wieder leer, die Event Loop schlägt wieder zu, holt sie den nächsten Callback von der Callback Queue. Nehme ich das Ergebnis von einem Request, das man vorher geschickt haben und ich kann mit der Verarbeitung weitermachen. Komplexe Verarbeitung kommt jetzt dran und danach, wenn ihr euch erinnert Loading Flag auf false. Das triggert wiederum den repaint von der UI. Ich kriege wieder Callback im Callback Queue, ich kann meinen Stack wieder abarbeiten, weil es ist nichts mehr da was zum Arbeiten ist und die Event Loop holt sich den nächsten Callback, nämlich den repaint und das Overlay verschwindet letztendlich wieder. Cool, hat funktioniert - wir haben es eh gesehen, dass es funktioniert. Das ist hier die Theorie dahinter. Was war jetzt, ich hab's genannt non blocking by accident, weil wir haben ja nix in Wirklichkeit absichtlich gemacht, dass es funktioniert. Wir haben es einfach gecodet und es hat funktioniert, das war bei uns ja auch so.

Und das nächstes das ist jetzt das unglückliche Blocking. Was haben wir jetzt verändert dass es jetzt auf einmal blockiert und das nicht mehr die Ladeanimation in der Zwischenzeit gezeigt werden konnte? Gehen wir es nochmal durch. Wir klicken auf den Button, danach ist das loadData, danach kommt wieder das Loading of True und dementsprechend wieder auch der Callback in die Callback Queue damit sich die UI refreshen kann. Perfekt.

Der nächste ist jetzt das requestData und erinnert euch, wir haben den Cache eingeschaltet. Das heißt, es wird nicht mehr der Web Request Richtung Backend geschickt, der per Definition und Umsetzung her asynchron ist, sondern wir greifen von lokalen Cache zu und das passiert synchron.

Wir haben also nicht mehr den Absprung Richtung Callback Queue, sondern es geht weiter. GetDataFromCache, die Daten kommen sofort, nämlich synchron zurück aus dem Cache. Die Verarbeitung beginnt und ihr seht, der Browser ist noch immer nicht im Overlay. Wir sind noch nicht so weit, dass der Browser sich refreshen konnte, weil das Stack immer noch voll ist. Daten werden verarbeitet, dann kommt schon das Loading Flag auf false. Im Hintergrund passiert sowas ähnliches wie dass sich das show overlay auf ein hide overlay umgedreht wird. Da passiert ein bisschen noch was mehr, aber es reicht um die Problematik zu erklären. So, jetzt sind wir fertig. Das Stack wird abgearbeitet. Jetzt schlägt wieder die Event Loop an, holt sich das repaint event oder ein repaint Callback her in den Stack und ja, das war's. Also wir hatten kein Overlay zu Gesicht bekommen und in Wirklichkeit sagen wir es dem Browser, dass er nichts tun soll, weil das Overlay ist eh gehidet in dem Moment. Also das ist das Problem dahinter. Wieso tritt das Problem jetzt tatsächlich auf? Ich habe eh schon gesagt JavaScript Single Threaded. Es gibt nur einen Callstack der auf diesem Single Threaded, auf diesem Main Thread eben lebt und solang dieser Callstack belegt ist, geben wir dem Browser, der UI nicht die Möglichkeit sich zu refreshen. Es war zwar der Callback gequeued, aber wir hatten einfach keine Zeit, dass sich der Browser letztendlich refresht. Das ist unabsichtlich passiert dadurch, dass wir ihm den Web Request geschickt haben, weil der hat wiederum dazu geführt, dass ein anderer Callback gequeued wurde und dass der Stack geleert werden konnte. Bei dem Beispiel mit dem Cache war das nicht der Fall. Die Abarbeitung war rein synchron und dementsprechend haben wir der UI nie Zeit gegeben, sich selbst zu aktualisieren.

Nonblocking by Design. Wir müssen also darauf achtgeben, dass wir, wenn wir wissen, dass wir lange Verarbeitungen machen oder dass wir ihn in die Problemstellung hineinlaufen könnten oder wenn wir es nicht wissen und dann unabsichtlich in die Problemstellung reinlaufen, dann daran denken, dass wir der UI de facto Zeit gehen müssen, damit sie sich refreshen kann. Und dafür gibt eine Lösung. Die Lösung heißt requestAnimationFrame. RequestAnimationFrame ist eine Methode, die der Browser zur Verfügung stellt, die sagt: Warte bis zum nächsten Frame. Und auf den Callback kann ich mich hängen. Und wenn sich nächste Frame passiert ist, wenn sich die UI repainted hat, dann mach ich meine Businesslogik hinterher. Und die Krux an der Sache ist – und das war auch der Grund, wieso ich am Anfang so skeptisch war – die Lösung ist Double Request Animation Frame. Ich muss die Methode zweimal aufrufen. Der Hintergrund ist, dass die Browser unterschiedlich dieses Request Animation Frame implementiert haben und dass es sein kann, dass der eine Browser erst nach 2 Request Animation Frames tatsächlich die UI repainted hat. Also, um ganz sicher zu sein und um eben browserunabhängig zu sein, muss es einfach zweimal hintereinander ausführen. Und das war der Grund, wieso ich mich damit beschäftigen wollte weil ich mir gedacht habe: Das kann nicht euer Ernst sein, dass das die Lösung ist. Aber offensichtlich ist sie das. Und wenn einer von euch ebenfalls auf diese Lösung kommt, dann gibt's immerhin einen, dem es genauso ging wie euch.

So läuft das Ganze jetzt ab. Ich zeige es auch wieder, wie es in der Applikation abläuft, damit ihr mir mehr oder weniger glaubt, dass das eh funktioniert.

In V4 haben wir das ganze mit Double Request Animation Frame gemacht. Zuerst wieder ohne Cache. Das ist wieder relativ einfach, weil wir haben die asynchrone Abfrage der Daten und der entsprechenden Callback. Und dann mit enableten Cache werden wir hoffentlich sehen - es funktioniert also jedenfalls. Also das war nicht die große Überraschung, aber trotzdem.

Und wie haben wir das jetzt umgesetzt?

Das ist jetzt der Code dahinter. Und ihr seht, das ist jetzt genau dieses Double Request Animation Frame. Wir rufen zum ersten Mal Request Animation Frame auf, in dem Callback rufen und nochmal Request Animation Frame auf, und in dem Callback beginnen wir dann die Daten zu holen mit der Datenverarbeitung an sich. Damit dieses Loading Flag, was wir da umsetzen, auch tatsächlich dazu führen kann, dass sich die UI repainted. Schaut komisch aus aber wie gesagt, das ist die Lösung. Und wenn wir uns das Ganze jetzt auch im Stack anschauen, dann schaut es so aus. Ich habe hier den Buttonklick – kennen wir schon, loadData, loading is true, was wiederum zu einem Callback auf der Callback Queue führt. Dann kommt man in requestAnimationFrame, was tatsächlich auch dazu führt, dass im Callback gequeued wird. Perfekt. Und jetzt kann sich das Stack wieder abarbeiten. Die Event Loop schlägt an, holt sich das repaint, ich painte mein Overlay. Perfekt. Das Stack ist wieder leer. Ich hole jetzt den Callback vom requestAnimationFrame, mache das Ganze ein zweites Mal, um sicher zu gehen. Queue wieder einen Callback auf der Callback Queue, arbeite wieder den Stack ab, hole mir den nächsten Callback via Event Loop. Und jetzt kommt erst mein requestData, meine Datenverarbeitung. In dem Moment ist, der Browser schon geoverlayed. Perfekt. Jetzt ist es wurscht, dass die Daten synchron geholt werden, weil ich habe schon das Overlay gepainted, hole mir die Daten, verarbeite die Daten, setze wieder Loading auf false, damit der Overlay verschwinden kann - was wiederum den Callback triggert. Arbeitet in Stack ab, die Event Loop holt sich wieder den Callback. Und mein Browser ist wieder hell. Perfekt. Also so funktioniert es im Hintergrund. Das ist die Theorie dahinter, wenn ich sage Double Request Animation Frame ist die Lösung.

Gut, ja, bottom line, zusammenfassend. Non blocking UI ist wichtig, es ist User Experience. UI zu blocken ist ganz schlechte User Experience. Und blocken passiert dann, wenn ich es nicht schaffe, den Stack immer wieder leer zu räumen, damit eben die UI wieder responsive wird. Wir haben es unabsichtlich geschafft, indem man eben diese asynchrone Web Requests gemacht haben. Das ist nun mal typisch und das führt automatisch dazu, dass wir die UI wieder responsiver machen. Wenn man dann auf den Cache umschaltet der synchron arbeitet, dann ist das eben nicht mehr der Fall. Und was uns auch noch hinein gespielt hat, wir haben eh gesagt: Wir verwenden doch asynchrone Methoden, wir haben doch ein await stehen, das muss doch funktionieren. Await is noch keine Garantie, dass irgendwas asynchron abläuft. Es kommt drauf an, was man aufruft, ob das tatsächlich asynchron arbeitet, ob da jetzt await vorne steht oder nicht ist relativ wurscht. Und die Lösung ist wie gesagt, auch wenn es komisch klingt, zweimal requestAnimationFrame aufzurufen.

Wie versprochen – oder nicht versprochen ich glaube, ich hab's gar nicht erwähnt – ich hab mir eben einige coole Videos angeschaut und die hab ich euch zusammengestellt. Das erste ist In the Loop, das ist ein cooler Talk wo der sehr, sehr nett und informativ und unterhaltsam die Event Loop erklärt. Und danach ist man schon vieles, vieles gescheiter.

Der zweite ist auch sehr cool, What the Heck is Event Loop anyway. Der hat doch tatsächlich ein kleines Framework geschrieben, wo er JavaScript Code laufen lassen kann und dynamisch die Event Loop, den Callstack und die Callback Queue visualisieren kann – sehr empfehlenswert, wirklich cool.

Und genauso der Blog-Artikel Tasks, Microtask, Queues and Schedules, der auch wieder versucht eben gleichzeitig zu zeigen JavaScript Code, Callback Queue, Stack und auch darauf eingeht, dass die unterschiedlichen Browser und die unterschiedlichen JavaScript Engines auch tatsächlich unterschiedliche Ergebnisse liefern können. Deswegen auch dieses Double Request Animation Frame, weil die Browser oder die JavaScript Engines eben unterschiedlich auch arbeiten können. Und zum Schluss wer es sich durchlesen mag, ist natürlich auch noch die Dokumentation von RequestAnimationFrame an sich.

Ja, das war's. Wie gesagt, ich hoffe, ich hab das Problem ein bisschen näher bringen können. Dass jetzt Event Loop und Rollback Queue und Callstack zumindestens Begriffe sind, mit denen man etwas anfangen kann. Wen es wirklich interessiert, schaut sich die Videos an! Taucht mehr in das Thema ein. Da gibts noch vieles weiteres, was interessant ist und was noch wichtiger ist als Fullstack-Developer im Endeffekt zu kennen.

Ja und wer Interesse hat: wir suchen Leute, wir suchen gute Leute in dem Umfeld, Node.js und JavaScript und Vue.Js - das ist unser Stack. Und wer Lust hat, wir sind in Wien - ruft einfach an, kommts vorbei, schreibt uns eine E-Mail. Dankeschön!

 

Technologien in diesem Artikel