Der Heckenschütze: Mehrere requestAnimationFrame() ...problematisch?

Hallo, bevor ich mich (noch tiefer?) ins Unglück stürze, wollte ich doch die prinzipielle Architektur meines Vorhabens klarstellen:

Ist es eigentlich prinzipiell zulässig [bzw. wohl eher ratsam] sowie Good Practice, mehrere requestAnimationFrame() parallel ablaufen zu lassen? Oder sollten / müssen alle Repaint-Modifikationen in einem Callback zusammengefasst werden (welches dann mit einem requestAnimationFrame() ausgelöst wird)?

(Lieber wäre es mir aus Gründen der Übersichtlichkeit doch die Variante mit mehreren requestAnimationFrame())

Was denkt ihr dazu? Könnt ihr dazu Literatur empfehlen?

Dank euch, der Heckenschütze.

  1. Lieber Heckenschütze,

    (Lieber wäre es mir aus Gründen der Übersichtlichkeit doch die Variante mit mehreren requestAnimationFrame())

    ist das nicht im Wesentlichen eine Performance-Frage?

    Liebe Grüße

    Felix Riesterer

  2. Hallo,

    wenn Du requestAnimationFrame aufrufst, wird der Aufruf "bestellt" und beim nächsten fälligen Animationszeitpunkt ausgeführt. Frag mich nicht, wann das ist. Das hängt sicherlich auch daran, wie der Browser geschrieben ist und ob die Darstellung mit einem double buffer erfolgt oder nicht.

    Rein technisch bist Du nicht gezwungen, nur einen Aufruf zu machen. Wenn 20 Aufrufe bestellt sind, werden auch 20 ausgeführt. Alle 20 erhalten den gleichen Timestamp, rechnen also auf der gleichen Grundlage. Ob Du also 20 Aufrufe hinterlegst, oder nur einen, in dem Du dann 20 Funktionen aufrufst, ist gleichgültig.

    Ob Du Objekte im gleichen Aufruf animierst oder in verschiedenen, ist also frei entscheidbar.

    Wann sollte man Objekte nicht mit requestAnimationFrame animieren?

    • Der Browser kann eine Menge Animationen von ganz allein.

      • CSS Transitionen
      • CSS Animationen
      • SVG Animationen

      Bevor man selbst animiert, sollte man genau prüfen, was schon vorhanden ist. Ich wollte letztens einen Pfeil animieren; er sollte aus einem Punkt heraus entlang eines Pfades wachsen und die Pfeilspitze sollte dabei mitlaufen. Ich das mühsam mit requestAnimationFrame programmiert. Und dann stellte ich fest, dass das mit ein paar SVG Optionen ohne mein Zutun ging 😫

    Wann sollte man Objekte im gleichen Aufruf animieren?

    • es gibt Informationen, die für diese Objekte gemeinsam gebraucht werden und die Du andernfalls jedesmal neu berechnen müsstest
    • du malst auf einen Canvas und brauchst einen RenderingContext. Das Beschaffen des Context ist meines Wissens eine "teure" Operation, und wenn man darin auch noch Einstellungen treffen muss, wird es noch teurer
    • du musst vor dem Zeichnen eine z-Order bestimmen, d.h. die Reihenfolge, in der Du zeichnest

    Wann sollte man Objekte in verschiedenen Aufrufen animieren?

    • Du animierst HTML Elemente (d.h. du änderst an ihrem style herum) und diese haben miteinander nichts zu tun.
    • Du baust eine Komponente, die in ihrem UI Animationen verwendet, und diese Komponente taucht mehrfach auf. In dem Fall möchtest Du keinen zentralen Animator haben, sondern jede Komponente sollte das für sich steuerm

    Rolf

    --
    sumpsi - posui - obstruxi
  3. Hallo, bevor ich mich (noch tiefer?) ins Unglück stürze

    Bei solchen Performance-Fragen muss man aber erstmal ins Unglück laufen. Es gibt eine goldene Regel in Sachen Optimierung, die sagt: Optimiere nicht, bis es zu spät ist. Der Grund dafür ist, dass man im Voraus einfach nicht wissen kann, wo sich später Optimierungs-Potenziale auftun. Du kannst ja nicht messen und vergleichen wo noch nichts ist. Es ist deshalb nicht ökonomisch auf Verdacht zu optimieren. Man programmiert die Anwendung erstmal einfach so sauber es nun mal geht. Im Anschluss kann man messen und zielgerichtet Performance-Engpasse wegoptimieren.

    Ist es eigentlich prinzipiell zulässig [bzw. wohl eher ratsam] sowie Good Practice, mehrere requestAnimationFrame() parallel ablaufen zu lassen?

    Klar ist das zulässig. Kleiner Tipp am Rande: Du meinst "konkurrierend" bzw. "asynchron" und nicht "parallel".

    Oder sollten / müssen alle Repaint-Modifikationen in einem Callback zusammengefasst werden (welches dann mit einem requestAnimationFrame() ausgelöst wird)?

    Nein, dafür gibt es im Allgemeinen keinen guten Grund. Es kann sein, dass sich nach einer Performance-Messung dafür im Einzelfall ein Grund ergibt.

    (Lieber wäre es mir aus Gründen der Übersichtlichkeit doch die Variante mit mehreren requestAnimationFrame())

    Dann solltest du damit anfangen.

    Was denkt ihr dazu? Könnt ihr dazu Literatur empfehlen?

    Dieses Einsteiger-Tutorial erklärt wie man die Chrome-DevTools nutzt, um die Laufzeit-Performance zu analysieren.

    1. Hallo 1unitedpower,

      Ist es eigentlich prinzipiell zulässig [bzw. wohl eher ratsam] sowie Good Practice, mehrere requestAnimationFrame() parallel ablaufen zu lassen?

      Klar ist das zulässig. Kleiner Tipp am Rande: Du meinst "konkurrierend" bzw. "asynchron" und nicht "parallel".

      Zulässig vielleicht, aber in JavaScript nicht möglich. Die requestAnimationFrame Callbacks werden strikt nacheinander ausgeführt. JavaScript ist an dieser Stelle nicht parallelisierbar. Ob der Grafiktreiber oder die Grafikhardware es in anderen Sprachen zulässt, dass zwei Threads am gleichen Grafikkontext herumspielen - oder an zwei Grafikkontexten für verschiedene Bildschirmbereiche - oder ob auch da sequenziert werden muss, keine Ahnung.

      Man kann natürlich Berechnungen in Worker Threads auslagern. Mit denen kommuniziert man in JavaScript aber asynchron über postMessage und fängt sich damit den Overhead des Messagings ein. Ich habe auch keine Ahnung, ob es dabei zu unkalkulierbaren Verzögerungen kommen kann, die innerhalb einer requestAnimationFrame Verarbeitung nicht tolerierbar sind. Das eigentliche Zeichnen muss man aber auf jeden Fall im UI-Thread machen, das geht nicht im Worker.

      In einem Spiel könnte ich mir vorstellen, dass die Physik der Spielwelt in einem Worker läuft. Immer, wenn die Physik eines Frames neu berechnet ist, wird mit postMessage ein Datenpaket bereitgestellt, das der UI Thread dann zum Zeichnen verwendet. Wenn der User Kommandos gibt, werden diese mit postMessage dem Worker übermittelt und der bezieht sie in die Berechnung des nächsten Frames ein. Ob das funktioniert - keine Ahnung. Ob es effizient ist und nicht laggt[1] - ebensowenig Ahnung. Um das zu merken, braucht man schon ein komplexes Spiel, das in einer single-thread Lösung einknickt.

      Rolf

      --
      sumpsi - posui - obstruxi

      1. Zur Illustration des Problems: Daniel LaBelle - If people lagged in real life ↩︎

      1. Danke euch allen für den Input!

        Keine Sorge, habe nicht vor, hier Grand Theft Auto für Arme zu scripten - es gibt aber schon vier bis fünf Positionsveränderungen im DOM, die simultan zeitgleich ablaufen sollen (da ich eben noch in den Grundzügen der Überlegung bezüglich prinzipieller Architektur bin, gibt es allerdings noch keine Ergebnisse, die ich posten könnte).

        CSS-Animationen haben mich bei einem ähnlichen Projekt schon mal in Teufels Küche gebracht, da man einem etwaigen Animation-Lag ziemlich hilflos ausgeliefert ist, ein animationend - eventListener im schlimmsten Fall dadurch nicht feuert und das ganze Programm sich dadurch aufhängt (wohlgemerkt in VANILLA Javascript; - OHNE Einbindung irgendwelcher Frameworks / Bibliotheken!). CSS-Animationen sind in gewissen Situationen erstaunlich wenig performant, habe mich zugegebenermaßen allerdings noch nicht näher mit Workarounds, die eine Hardware Beschleunigung erzwingen beschäftigt (wie das bei Einbindung von transform: translateZ() anscheinend der Fall ist, siehe auch http://html-tuts.com/fix-laggy-transitions-in-css/ ).

        Dieses Problem habe ich mit requestAnimationFrame() insofern nicht, da ich sämtliche Positionierungen mithilfe von Delta Time gezielt berechnen wie steuern kann, und Bedingungen am Ende eines gewissen Animationszykluses auch sicher zu hundert Prozent ausgeführt werden.

        @Rolf B

        Wenn requestAnimationFrame() also hintereinander konsekutiv abspielt, heißt das also, dass ich gleichzeitig laufende Animationen in einem Callback zusammenfassen muss, richtig ...?

        Dank euch, Heckenschütze

        1. Hallo Der,

          Wenn requestAnimationFrame() also hintereinander konsekutiv abspielt, heißt das also, dass ich gleichzeitig laufende Animationen in einem Callback zusammenfassen muss,

          Nein. Das musst Du nicht. Wenn der Zeitpunkt für den Aufruf der Animationframe-Callbacks gekommen ist, werden sie allesamt nacheinander aufgerufen, in Reihenfolge ihrer Registrierung (genauer: in Reihenfolge ihrer ID). Wenn sie alle zusammen zu lange brauchen, dann fällt vermutlich ein Callback-Zyklus aus.

          Natürlich werden die Callbacks, die in einem Zyklus neu registriert werden, erst im nächsten wieder aufgerufen.

          Die Spec schreibt:

          To run the animation frame callbacks for a target object target with a timestamp now:

          • Let callbacks be target's map of animation frame callbacks.
          • Let callbackHandles be the result of getting the keys of callbacks.
          • For each handle in callbackHandles, if handle exists in callbacks:
            • Let callback be callbacks[handle].
            • Remove callbacks[handle].
            • Invoke callback, passing now as the only argument, and if an exception is thrown, report the exception.

          D.h. zuerst wird eingesammelt, welche Callbacks registriert sind, und dann werden die der Reihe nach aufgerufen und danach entfernt, während die gerufenen Callbacks schon wieder neue Callbacks registrieren.

          Rolf

          --
          sumpsi - posui - obstruxi
      2. Hallo 1unitedpower,

        Ist es eigentlich prinzipiell zulässig [bzw. wohl eher ratsam] sowie Good Practice, mehrere requestAnimationFrame() parallel ablaufen zu lassen?

        Klar ist das zulässig. Kleiner Tipp am Rande: Du meinst "konkurrierend" bzw. "asynchron" und nicht "parallel".

        Zulässig vielleicht, aber in JavaScript nicht möglich. Die requestAnimationFrame Callbacks werden strikt nacheinander ausgeführt. JavaScript ist an dieser Stelle nicht parallelisierbar.

        Also bevor wir hier noch mehr Verwirrung stiften, ich wollte lediglich darauf hinweisen, dass es einen Unterschied zwischen "parallel" und "konkurrierend" gibt. "Parallelismus" wird auf der Hardware implementiert: Im einfachsten Fall stell man sich vor, dass zwei oder mehr Prozessoren gleichzeitig Arbeit verrichten.

        "Konkurrenz" findet auf der Software-Ebene statt. Es bedeutet, dass man die Ausführungsreihenfolge verschiedener Berechnung unterspezifiziert lässt. requestAnimationFrame ist ein Beispiel für Konkurrenz: man sagt dem Browser nur: bitte führe die Callback-Funktion aus bevor du den nächstens Frame zeichnest – wann auch immer das ist. Man lässt unterspezifiziert wann genau die Callback-Funktion aufgerufen wird − insbesondere in Bezug auf andere Browser-Events wie Timer oder Benutzereingaben. Worker Threads sind ebenfalls ein Beispiel für Konkurrenz. Man lässt unspezifiziert wann welchem Thread rechenleistung zugeteilt wird. Verschiedene Threads konkurrieren um Rechenleistung. Hat ein System nur einen Prozessor, dann muss das System die Rechenleistung des Prozessors aufteilen und nacheinander den einzelnen Threads zur Verfügung stellen. Hat ein System mehrere Prozessoren kann es von dieser Unterspezifikation gebrauch machen, und mehreren Threads gleichzeitg Rechenleistung auf verschiedenen CPUs zur Verfügung stellen. Dann hat man auf Hardware-Ebene wieder echte Parallelität.

        1. Hallo 1unitedpower,

          sorry, ich hätte nach "zulässig" nicht die Aufmerksamkeit abschalten dürfen.

          Wobei "unterspezifiziert" hier genau das richtige Wort ist - "unspezifiziert" wäre zu hart. Der Zeitpunkt an sich ist unklar; außer dem Umstand, dass kein anderes JS laufen darf, gibt es vermutlich keine Einschränkung.

          Die Reihenfolge der Aufrufe ist aber definiert: Wer zuerst angefordert wurde, wird auch zuerst aufgerufen. Das ergibt sich aus dem Umstand, dass die Spec eine "ordered map" als Speicher für die Callbacks definiert und als Schlüssel einen pro request-Aufruf aufsteigenden Zähler.

          Und ich hab noch was gelernt: Worker können Animationframes requesten und auch malen. Die Spec sagt:

          Inside workers, requestAnimationFrame() can be used together with an OffscreenCanvas transferred from a canvas element.

          Und auf diesem OffscreenCanvas kann der Worker nach Herzenslust herumkritzeln.

          Rolf

          --
          sumpsi - posui - obstruxi