Arbeitsplatz Bild SEQIS Group GmbH

JavaScript and (Non) Blocking UI

Description

Klemens Loschy von SEQIS fokusiert sich in seinem devjobs.at TechTalk "JavaScript and (Non) Blocking UI" auf eine Problemstellung aus dem Gebiet der eventloops in JavaScript und demonstriert einen Lösungsweg.

Beim Videoaufruf stimmst Du der Datenübermittlung an YouTube und der Datenschutzerklärung zu.

Video Zusammenfassung

In JavaScript and (Non) Blocking UI erklärt Klemens Loschy, wie Single-Threading, Call Stack, Callback Queue und die Event Loop die Responsiveness im Browser bestimmen und warum ein Request-Cache aus einem asynchronen Backend-Call plötzlich synchrone Arbeit macht. In seiner Demo führt der Cache dazu, dass der Ladeindikator nicht erscheint, weil der Main Thread blockiert wird und „await“ das nicht verhindert. Die pragmatische Lösung ist Double requestAnimationFrame vor der teuren synchronen Verarbeitung, damit der Browser zuerst das Overlay repainted – ein frameworkunabhängiges Muster, das Entwickler direkt anwenden können.

JavaScript and (Non) Blocking UI: Warum Caching die Oberfläche blockieren kann – und wie Double requestAnimationFrame hilft

Ein technisches Recap des Talks von Klemens Loschy (SEQIS Group GmbH)

Wer im Browser arbeitet, lernt schnell: Benutzerinnen und Benutzer akzeptieren keine träge Oberfläche. Spätestens wenn ein Vorgang länger als zwei Sekunden dauert, braucht es visuelles Feedback. Genau darüber sprach Klemens Loschy in der Session mit dem Titel JavaScript and (Non) Blocking UI. Der Team Lead erklärte anhand eines realen Beispiels, warum eine vermeintliche Optimierung mit Caching die UI unerwartet blockierte – und weshalb die unscheinbare Browser-API requestAnimationFrame in doppelter Ausführung den Unterschied macht.

Als Redaktion von DevJobs.at haben wir den technischen Faden verfolgt: von den ersten Symptomen über die Event-Loop-Grundlagen bis zur finalen Lösung. Dieses Recap richtet sich an Frontend- und Full-Stack-Entwicklerinnen und -Entwickler, die verstehen wollen, wie sich Single-Thread-JavaScript, der Call Stack und Repaints konkret auf die Responsiveness einer Anwendung auswirken.

Ziel des Vortrags: Event-Loop begreifen, UI-Blocking vermeiden

Klemens Loschy stellte gleich zu Beginn klar: Er wollte das Problem verstehen, nicht die Event-Loop bis ins letzte Byte studieren. Wichtig war ihm, den konkreten Fehlerfall nachvollziehbar zu machen und das Bauchgefühl in eine belastbare Erklärung zu übersetzen. Seine Kernaussage: Eine gute User Experience steht und fällt mit einer nicht blockierenden UI. Wer länger rechnet oder auf Antworten wartet, muss der Oberfläche eine Chance geben, sich zu aktualisieren.

Ein prägnanter Merksatz aus dem Talk: Alles über zwei Sekunden muss ich dem Benutzer irgendwie visualisieren. Genau an diesem Schwellenwert entzündet sich die Geschichte.

Das Setup: Ein Button, ein Request, eine Verarbeitung – und ein Overlay

Der Demonstrationsfall ist klassisch:

  • Ein Button-Klick im Browser startet den Ladevorgang.
  • Im Hintergrund werden Daten via Web-Request nachgeladen.
  • Anschließend erfolgt eine teils umfangreiche Verarbeitung.
  • Ab einer Dauer von über zwei Sekunden soll ein Overlay mit Ladeanimation anzeigen, dass etwas passiert.

Der Stack dahinter: Node.js im Backend, Vue.js im Frontend, ausgeführt in der V8-JavaScript-Engine. Wichtig: Die verwendeten Frameworks sind weder Ursache noch Lösung. Das Verhalten ist allgemeingültig und lässt sich ebenso mit einfachem HTML, Vanilla-JavaScript und CSS reproduzieren.

Der iterative Weg: vier Versionen bis zur Ursache

Klemens führte durch vier inkrementelle Stände seiner Demo. Die Schritte sind exemplarisch für reale Produktentwicklung und zeigen, wie leicht Non-Blocking by Accident in Blocking by Accident kippen kann.

V1 – Basisfunktion: alles noch flott genug

In der ersten Version kommen die Daten meist in 200 bis 500 Millisekunden, insgesamt erscheinen Resultate nach etwa 700 Millisekunden. Subjektiv ist das in Ordnung, die Oberfläche bleibt responsive, es gibt noch keine sichtbare Ladeanimation. Niemand vermisst an dieser Stelle etwas.

V2 – Nutzerfeedback ab zwei Sekunden: Overlay einblenden

Mit wachsender Komplexität steigen Antwortzeiten. Es kommen weitere Features, eventuell mehrere Backends, sequentielle Abhängigkeiten oder größere Datenmengen. Die Verarbeitung dauert spürbar länger – bis hin zu 2,5 Sekunden oder mehr. Folgerichtig wird ab zwei Sekunden ein Overlay mit Ladeanimation gezeigt. Der Benutzer kann in dieser Zeit nicht interagieren, aber er sieht, dass gearbeitet wird.

Das Muster: Unter zwei Sekunden bleibt die UI wie gehabt, darüber erscheint das Overlay und verschwindet nach Abschluss.

V3 – Request-Cache: plötzlich blockiert die UI und die Animation erscheint nicht mehr

Die nächste Optimierungsidee liegt auf der Hand: Häufig sind dieselben Requests mehrfach identisch. Wenn sich Daten nicht laufend ändern, ist es sinnvoll, einen Request-Cache zu aktivieren, der Antworten lokal liefert, ohne das Backend erneut zu kontaktieren. Gesagt, getan – durch eine kleine Konfiguration in der Request-Library.

Messbar spart das in der Demo etwa 900 Millisekunden ein. Klingt großartig. Doch dann passiert das Unerwartete: Mit aktiviertem Cache ist die Antwort quasi sofort da, aber die UI zeigt kein Overlay mehr. Die Oberfläche wirkt blockiert – ausgerechnet dort, wo wir schneller sein wollten. Was ist passiert?

JavaScript-Kernprinzipien: Single Thread, Call Stack, Callback Queue und Event-Loop

Für die Erklärung zieht Klemens die wesentlichen Bausteine der Laufzeit heran:

  • JavaScript läuft im Browser single-threaded auf dem Main Thread.
  • Es gibt den Call Stack, auf dem Funktionen ausgeführt werden.
  • Asynchrone Operationen (z. B. Web-Requests, Timer, DOM-Manipulationen) laufen über Web-APIs und melden ihre Fertigstellung via Callback zurück.
  • Callbacks landen in einer Callback Queue (mit Unterscheidung in Tasks und Microtasks, für den vorliegenden Fall aber nicht weiter wichtig).
  • Die Event-Loop schiebt Callbacks aus der Queue auf den Call Stack – jedoch nur, wenn der Call Stack leer ist.

Diese Mechanik ist entscheidend: Solange der Call Stack nicht freigegeben ist, bekommt der Browser keine Gelegenheit für Reflow/ Repaint. Ein gequeueter UI-Update-Callback allein reicht also nicht. Die Oberfläche kann erst dann sichtbar reagieren, wenn der Stack zur Ruhe kommt.

Warum V2 funktionierte: Non-Blocking by Accident

Im funktionierenden Fall (ohne Cache) läuft der Klickpfad so ab:

  1. Button-Klick startet die Methode zum Laden.
  2. Das Loading-Flag wird auf true gesetzt. Dadurch wird ein UI-Update gequeued, damit das Overlay gerendert werden kann.
  3. Es wird ein asynchroner Web-Request abgeschickt. Wichtig ist hier: Der asynchrone Sprung verlässt den Call Stack. Der Stack kann geleert werden.
  4. Die Event-Loop erkennt den leeren Stack und holt den UI-Update-Callback. Der Browser kann repainten, das Overlay wird sichtbar.
  5. Wenn die Response eintrifft, landet ihr Callback in der Queue, wird auf den Stack geschoben und die komplexe Verarbeitung startet.
  6. Am Ende wird das Loading-Flag auf false gesetzt, erneut wird ein UI-Update gequeued und nach Abarbeitung verschwindet das Overlay.

Die UI war sichtbar, weil der asynchrone Request eine Lücke schuf, in der die Event-Loop die Repaint-Arbeit einschieben konnte.

Warum V3 fehlschlug: synchroner Cache, unglückliches Blocking

Mit aktiviertem Request-Cache ändert sich nur scheinbar wenig – technisch jedoch etwas Entscheidendes:

  • Das Setzen von Loading auf true queued wieder einen UI-Update.
  • Statt eines asynchronen Web-Requests greift der Code nun synchron auf den lokalen Cache zu.
  • Die Daten sind sofort da, die komplexe Verarbeitung startet unmittelbar.
  • Der Call Stack bleibt die ganze Zeit belegt: Es gibt keine Gelegenheit für die Event-Loop, den UI-Update-Callback auszuführen.
  • Erst am Ende wird Loading wieder auf false gesetzt und der Repaint gequeued – jetzt jedoch, ohne dass das Overlay zuvor jemals sichtbar war.

Kurz gesagt: Der synchrone Cache hat die kritische kleine Pause entfernt, in der die UI repainten kann. Das Ergebnis ist eine blockierte Wahrnehmung: Die Oberfläche zeigt kein Feedback, obwohl wir es explizit vorgesehen hatten.

Ein weiterer Aha-Moment aus dem Talk: Ein await ist keine Garantie für Asynchronität. Es kommt darauf an, was aufgerufen wird. Wenn eine Funktion synchron zurückkehrt (wie der Cache in diesem Fall), bleibt der Call Stack belegt, auch wenn syntaktisch ein await im Code steht.

Non-Blocking by Design: der Weg zur robusten Lösung

Die Schlussfolgerung aus der Analyse: Wer intensive Verarbeitung anstößt, muss bewusst Zeitfenster schaffen, in denen der Browser repainten kann. Nicht, indem man künstlich wartet, sondern indem man den UI-Update tatsächlich zur Ausführung kommen lässt, bevor die schwere Arbeit auf dem Main Thread weiterläuft.

Die zentrale API dafür: requestAnimationFrame.

  • requestAnimationFrame sagt dem Browser, einen Callback zum nächsten Frame auszuführen.
  • Dieser Zeitpunkt ist exakt dann, wenn die UI bereit ist, den nächsten Repaint vorzunehmen.
  • Hängen wir die Business-Logik erst danach an, stellen wir sicher, dass das Overlay vorher sichtbar wurde.

Der Clou aus Klemens’ Praxis: Double requestAnimationFrame. Die Methode muss zweimal hintereinander aufgerufen werden, um browserübergreifend sicherzustellen, dass das Overlay wirklich sichtbar ist, bevor die Arbeit weitergeht. Hintergrund ist die unterschiedliche Implementierung in Browsern und JavaScript-Engines; teils wird erst nach zwei Frames zuverlässig repainted.

Ablauf mit Double requestAnimationFrame: Schritt für Schritt

Klemens hat den funktionierenden Pfad erneut visualisiert – diesmal mit doppeltem Frame-Callback:

  1. Button-Klick startet Laden, Loading wird auf true gesetzt. Ein UI-Update wird gequeued.
  2. Der erste Aufruf von requestAnimationFrame queued einen Frame-Callback.
  3. Der Call Stack wird leer; die Event-Loop holt den Repaint-Callback: Das Overlay wird sichtbar.
  4. Der zweite Aufruf von requestAnimationFrame queued erneut einen Frame-Callback.
  5. Wieder wird der Stack frei; der Browser repainted noch einmal bzw. garantiert, dass der sichtbare Zustand stabil ist.
  6. Erst jetzt erfolgt das Holen der Daten. Ob Antwort synchron aus dem Cache oder asynchron vom Backend kommt, ist nun egal – das Overlay ist bereits sichtbar.
  7. Nach der komplexen Verarbeitung setzt der Code Loading auf false, queued den finalen UI-Update und die Oberfläche kehrt in den normalen Zustand zurück.

Das Ergebnis: Auch wenn der Cache die Antwort synchron liefert, blockiert die UI die Ladeanimation nicht mehr. Die Nutzerinnen und Nutzer sehen, dass etwas passiert, und die Anwendung wirkt reaktionsschnell.

Was wir daraus gelernt haben

Der Talk von Klemens Loschy führt eine wichtige Produktwahrheit vor: Performance ist nicht nur Geschwindigkeit, sondern vor allem Wahrnehmung. In Frontends zählt, ob sich die UI reaktionsfähig anfühlt. Und das wird durch die Event-Loop-Mechanik unmittelbar bestimmt.

Einige zentrale Lehren, die wir aus der Session mitnehmen:

  • Non-Blocking ist kein Zufallsergebnis. Es muss bewusst designt werden, insbesondere bei potenziell synchronen Pfaden wie Caches.
  • Der Call Stack darf nicht dauerhaft blockiert werden, wenn gleichzeitig eine UI-Aktualisierung sichtbar sein soll.
  • Asynchroner Netzwerkzugriff schafft natürliche Lücken; synchroner Cache nicht. Beide Pfade müssen hinsichtlich Repaint-Verhalten bedacht werden.
  • Ein await macht einen synchronen Codepfad nicht wirklich asynchron. Dort, wo synchron gearbeitet wird, bleibt der Stack blockiert.
  • requestAnimationFrame verankert die nächste Aktion am Repaint-Zyklus des Browsers. Mit doppeltem Aufruf wird der Effekt robust und browserunabhängig.

Praxisnahe Hinweise für Engineering-Teams

Wie überträgt man diese Einsichten auf bestehende und neue Projekte? Aus dem Talk lassen sich unmittelbar praktikable Hinweise ableiten:

  • Overlay-Logik klar entkoppeln: Das Setzen des Loading-Flags sollte dem Browser Gelegenheit geben, den sichtbaren Zustand herzustellen. Wenn danach synchron weitergearbeitet wird, hilft Double requestAnimationFrame.
  • Caches als synchronen Pfad behandeln: Wer Request-Caching aktiviert, sollte testen, ob Repaints weiterhin rechtzeitig passieren. Tritt das Overlay nicht auf, liegt es oft an einem befüllten Call Stack.
  • Lange Verarbeitung nicht unterschätzen: Auch ohne Netzwerk kann die pure Datenverarbeitung die UI blockieren. Der Mechanismus ist derselbe.
  • Zustandswechsel bewusst timen: UI-Zustandsänderungen (Overlay an/aus) so planen, dass zwischen Sichtbar-Schalten und schwerer Arbeit mindestens ein Frame liegt.
  • Den Event-Loop-Durchlauf sichtbar machen: Für das Team kann es hilfreich sein, den Ablauf gedanklich in Call Stack, Queues und Frames zu zerlegen – wie es Klemens anhand seiner Diagramme tat. Das schafft ein gemeinsames mentales Modell.

Häufige Missverständnisse adressiert

Der Vortrag räumt nebenbei mit typischen Annahmen auf:

  • Syntaktisches Asynchron sieht nicht gleichbedeutend mit tatsächlich asynchroner Ausführung. Ein await auf eine Funktion, die synchron zurückkehrt, ändert am Blocking-Verhalten nichts.
  • Frameworks lösen diese Klasse von Problemen nicht automatisch. Weder Node.js noch Vue.js noch die V8-Engine sind Ursache oder Quick-Fix; es ist die zugrundeliegende Event-Loop-Mechanik, die zählt.
  • Spinners sind keine bloße Kosmetik. Sie sind ein essenzieller Bestandteil von UX, insbesondere jenseits der Zwei-Sekunden-Marke.

Ressourcen aus dem Talk

Klemens nannte mehrere empfehlenswerte Quellen, um das Thema zu vertiefen:

  • In The Loop – ein unterhaltsamer Vortrag, der die Event-Loop anschaulich erklärt.
  • What the Heck is the Event-Loop Anyway – inklusive einer kleinen Umgebung zur Visualisierung von JavaScript-Code, Call Stack und Callback Queue.
  • Task, Micro-task, Queues and Schedules – ein Blogartikel, der Code, Queues und Stack nebeneinanderstellt und Unterschiede zwischen Browsern und Engines beleuchtet.
  • Die Dokumentation zu requestAnimationFrame – für die Details der API.

Diese Hinweise untermauern auch die Entscheidung für Double requestAnimationFrame: Unterschiedliche Browser und JavaScript-Engines verhalten sich nicht identisch. Zwei aufeinanderfolgende Frame-Callbacks sind ein pragmatischer Weg, die Sichtbarkeit des Overlays zu gewährleisten.

Ein Wort zum Engineering-Stil: iterativ denken, Symptome ernst nehmen

Ein roter Faden des Talks ist die iterative Herangehensweise. Der Weg von V1 bis V4 zeigt, wie Teams plausibel optimieren – und wie ausgerechnet eine sinnvolle Optimierung das UX-Gefühl verschlechtern kann. Der einzige Weg, solche Wechselwirkungen zu beherrschen, ist Beobachten, Hypothesen aufstellen, messen und die Mechanik verstehen. Klemens’ Skepsis gegenüber der Lösung Double requestAnimationFrame war groß – die Messung und das genaue Durchdenken der Event-Loop machten ihn letztlich jedoch zum überzeugenden Fix.

Fazit: Non-Blocking UI ist Pflicht – und ein Frame macht den Unterschied

Die Essenz der Session JavaScript and (Non) Blocking UI von Klemens Loschy: Eine reaktionsfähige Oberfläche ist kein Zufall, sondern Ergebnis bewusster Steuerung des Main Threads. Der Call Stack und die Event-Loop entscheiden, ob die UI Zeit zum Repaint bekommt. Asynchrone Requests schaffen diese Zeit oft von selbst; synchrone Pfade wie ein lokaler Cache nicht.

Die pragmatische Lösung für den gezeigten Fall lautet Double requestAnimationFrame: Erst das Overlay sichtbar werden lassen, dann die schwere Arbeit starten. So bleibt die UI für die Nutzerinnen und Nutzer verlässlich verständlich und gefühlt schnell – selbst wenn die Berechnung dahinter anspruchsvoll ist.

Zum Abschluss verwies Klemens darauf, dass sein Team mit Node.js, JavaScript und Vue.js arbeitet – und Verstärkung in Wien sucht. Wer sich für die hier gezeigten Fragestellungen begeistert, findet dort die passenden Herausforderungen.

Weitere Tech Talks

Weitere Tech Lead Stories

Weitere Dev Stories