Janky CSS-Animationen mit DevTools debuggen
Ruckelnde CSS-Animationen entstehen durch überschrittene Frame-Budgets: Bei 60fps hat der Browser etwa 16,7ms, um jeden Frame zu rendern. Jeder Frame, der länger dauert – aufgrund von Layout-Neuberechnungen, Paint-Vorgängen oder einem ausgelasteten Main-Thread – senkt die Framerate und äußert sich als sichtbares Ruckeln. Die Lösung lautet selten „mehr will-change hinzufügen”. Es geht um Diagnose: Herausfinden, welche Stufe der Rendering-Pipeline zu lange gedauert hat und auf welchem Thread. Dieser Artikel beschreibt einen systematischen Vier-Panel-Workflow in den aktuellen Chrome DevTools – Rendering, Performance, Animations und Layers –, um eine ruckelnde Animation auf ihre Ursache zurückzuführen, inklusive eines reproduzierbaren Vorher-Nachher-Beispiels.
Der Zielleser kennt transform und opacity als kostengünstige Eigenschaften, verfügt aber über kein systematisches Vorgehen. Die Animation, die auf einem Laptop einwandfrei lief, auf einem mittelklassigen Android-Gerät jedoch ruckelte, ist der klassische Fall. Gefragt ist Triage, kein weiterer Property-Tipp.
Wichtigste Erkenntnisse
- Bei 60fps hat der Browser etwa 16,7ms pro Frame, um Style, Layout, Paint und Composite abzuschließen. Ein einziger überschrittener Wert erzeugt ein sichtbares Ruckeln (Chrome DevTools Performance-Dokumentation).
- Diagnose in Panel-Reihenfolge: Rendering für einen schnellen visuellen Überblick, Performance zur Ursachenanalyse des langsamen Frames, Animations zur Isolierung des problematischen Keyframes, Layers zur Überprüfung der Compositor-Speicherkosten.
- Lila Recalculate-Style- oder Layout-Balken direkt unter einem gelben JavaScript-Balken im Main-Track des Performance-Panels signalisieren ein erzwungenes synchrones Layout; das rote Dreieck verlinkt zur genauen JS-Zeile.
- Der Compositor kann
transformundopacityohne Layout oder Paint animieren; die Animation vonleft/top/width/heighterzwingt bei jedem Frame ein Layout auf dem Main-Thread (CSS Triggers). - Scroll-gesteuerte Animationen mit
animation-timelinelaufen fürtransform/opacityauf dem Compositor, sodass ihr Ruckeln im Frames-Track erscheint, nicht als Long Task auf dem Main-Thread.
Was ist Animation-Jank?
Jank bezeichnet einen gedropten oder verzögerten Frame, den der Nutzer wahrnehmen kann. Um 60fps aufrechtzuerhalten, muss der Browser jeden Frame innerhalb von etwa 16,7ms abschließen (1000ms ÷ 60). Dieses Zeitfenster umfasst Style-Neuberechnung, Layout, Paint und Composite für den jeweiligen Frame. Wenn ein einzelner Frame zu lange dauert, verfehlt der Browser seine Deadline, die effektive Framerate sinkt – auf 30fps oder weniger – und die Bewegung wirkt abgehackt. Googles Leitfaden zur Rendering-Performance beschreibt dasselbe Budget: Flüssige visuelle Änderungen müssen in das Frame-Zeitfenster passen, und Animationen sind der sichtbarste Bereich, in dem das Budget überschritten wird, weil das Auge kontinuierliche Bewegung verfolgt.
Der Grund, warum ein einzelner langsamer Frame auffällt, während eine einzelne langsame Netzwerkanfrage es nicht tut: Eine Animation ist eine Folge von Frames, die das Gehirn zu einer Bewegung integriert. Ein einzelner verspäteter Frame unterbricht diese Integration, und ein Stocken mitten in einer Bewegung wirkt wie ein Sprung. Deshalb reicht „im Wesentlichen flüssig” nicht aus – der schlechteste Frame, nicht der Durchschnitt, bestimmt die wahrgenommene Qualität.
Die Rendering-Pipeline im Überblick
Jede visuelle Aktualisierung durchläuft eine festgelegte Pipeline: Parse → Style → Layout → Paint → Composite. Der Browser parst HTML und CSS zu DOM und CSSOM, berechnet, welche Styles gelten (Style), ermittelt Geometrie und Position jeder Box (Layout), rastert Pixel in Ebenen (Paint) und kombiniert diese schließlich zum angezeigten Bild (Composite). Der web.dev-Artikel zur Rendering-Pipeline ist die kanonische Langform-Referenz; die Kurzfassung genügt hier.
Der entscheidende Punkt für den Rest dieses Artikels: Jede Pipeline-Stufe läuft auf einem bestimmten Thread und erscheint in einem bestimmten DevTools-Panel. Layout und Paint laufen auf dem Main-Thread, gemeinsam mit JavaScript. Compositing läuft separat. Eine Animation, die nur compositet – eine bestehende Ebene verschiebt oder deren Deckkraft ändert – umgeht den Main-Thread nahezu vollständig. Eine Animation, die Layout auslöst, zieht bei jedem Frame Arbeit zurück auf den Main-Thread, wo sie mit allem anderen konkurriert. Genau diesen Unterschied machen die folgenden Panels sichtbar.
Welche DevTools-Panels diagnostizieren Animation-Jank?
Discover how at OpenReplay.com.
Eine systematische Jank-Diagnose durchläuft vier Panels der Reihe nach: das Rendering-Panel für einen schnellen visuellen Check (Tritt Paint-Flashing auf, wo es nicht sollte?), das Performance-Panel zum Aufzeichnen und zur Ursachenanalyse des langsamen Frames, das Animations-Panel zur Isolierung des problematischen Keyframes oder der problematischen Property, und das Layers-Panel zur Überprüfung, ob die Compositor-Layer-Promotion hilft oder Speicherdruck erzeugt. Die Reihenfolge ist bewusst gewählt: Rendering schließt ganze Problemklassen in Sekunden ein oder aus, Performance liefert den Trace, Animations grenzt auf die Property ein, und Layers prüft die Kosten der Lösung.
Rendering-Panel: Der schnelle Überblick
Das Rendering-Panel ist der erste Anlaufpunkt, weil es visuell und unmittelbar beantwortet, ob die Animation neu gezeichnet wird, obwohl sie es nicht sollte. Es lässt sich über das Command Menu öffnen (Cmd/Ctrl+Shift+P, „Show Rendering” eingeben) oder über More Tools → Rendering (Chrome DevTools Rendering-Referenz). Drei Schalter sind relevant:
- Frame Rendering Stats zeigt eine Live-FPS-Anzeige und ein GPU-Speicher-Overlay, während die Animation läuft. Ein Wert, der während der Animation deutlich unter 60 fällt, bestätigt das Vorhandensein von Jank.
- Paint flashing hebt Bereiche, die der Browser neu zeichnet, durch grünes Aufleuchten hervor. Ein Element, das nur
transformanimiert, sollte während der Bewegung kein grünes Aufleuchten erzeugen; ein grünes Aufleuchten, das der Animation folgt, bedeutet, dass Paint ausgelöst wird. - Layer borders umrandet Compositor-Layers in Orange. Damit lässt sich bestätigen, dass ein Element, das hardwarebeschleunigt sein soll, tatsächlich eine eigene Layer erhalten hat – und unbeabsichtigte Layers lassen sich erkennen.
Vorgehensweise:
- Rendering-Panel öffnen.
- Paint flashing aktivieren und die Animation auslösen.
- Leuchtet das animierte Element während der Bewegung grün auf, zeichnet die Animation jeden Frame neu – eine Layout- oder Paint-Property wird animiert. Das deutet auf ein Property-Problem hin und signalisiert, als nächstes das Performance-Panel zu öffnen.
- Gibt es kein Aufleuchten, aber die Bewegung ruckelt trotzdem, liegt der Engpass wahrscheinlich bei JavaScript auf dem Main-Thread, nicht beim Paint – ebenfalls eine Frage für das Performance-Panel.
Performance-Panel: Ursachenanalyse des langsamen Frames
Im Performance-Panel wird die Animation aufgezeichnet und lässt sich ablesen, welche Pipeline-Stufe das Frame-Budget überschritten hat. Es zeigt das FPS-Diagramm, Frame-Timing im Frames-Track, die Main-Thread-Aktivität und – in aktuellem Chrome – eine Insights-Seitenleiste, die Probleme wie erzwungene Reflows automatisch kennzeichnet (Chrome DevTools Performance-Referenz).
Vor der Aufzeichnung sollte die CPU gedrosselt werden, um das Gerät zu approximieren, auf dem Jank tatsächlich auftritt. Die Chrome DevTools dokumentieren CPU-Throttling-Presets unter Capture settings; das Panel bietet ein „4x slowdown”-Preset, wobei empfohlen wird, gegen eine Verlangsamung zu testen, die schwächere Hardware annähert (Performance-Referenz, CPU-Throttling). Throttling ist wichtig, weil der häufigste Grund, warum eine CSS-Animation lokal problemlos läuft, in der Produktion aber ruckelt, der Gerätekontext ist: Ein mittelklassiges Android-Gerät mit mehreren offenen Tabs verfügt über einen Bruchteil des CPU-Budgets eines Entwicklungs-Laptops – was Throttling annähert, aber ohne gleichzeitige Simulation von Speicherdruck und GPU-Last nicht vollständig replizieren kann.
Vorgehensweise:
- Performance-Panel öffnen und Screenshots aktivieren.
- In den Capture settings (Zahnrad-Symbol) CPU-Throttling auf ein Verlangsamungs-Preset setzen.
- Auf Record klicken, die Animation einige Sekunden laufen lassen, dann Stop klicken.
- Zuerst den FPS/Frames-Track lesen. Rote Markierungen über Frames kennzeichnen Frames, die das Budget überschritten haben.
- In einen schlechten Frame hineinzoomen und den Main-Track untersuchen.
Hier ist die wichtigste Heuristik beim Debuggen von Animationen:
Lila Balken unter gelben im Main-Track = erzwungenes synchrones Layout. Das rote Dreieck ist der direkte Link zur Lösung.
Im Main-Thread-Track signalisieren lila Recalculate-Style- oder Layout-Balken, die direkt unter einem gelben JavaScript-Balken erscheinen, ein erzwungenes synchrones Layout – der Browser wurde gezwungen, die Geometrie mitten im Script aufzulösen, weil JavaScript eine Layout-Property unmittelbar nach einem DOM-Schreibzugriff ausgelesen hat. Das Lesen von offsetWidth, offsetTop oder der Aufruf von getBoundingClientRect() nach einem Style-Schreibzugriff zwingt den Browser, das Layout synchron zu flushen; Paul Irishs kanonische Liste der Layout/Reflow-auslösenden Operationen führt diese Auslöser auf. Das rote Dreieck auf dem lila Balken öffnet einen Summary-Eintrag mit einer „Layout Forced”-Warnung und einem Quelldatei-Link zur genauen JS-Zeile. web.devs Leitfaden zu Layout-Thrashing behandelt das Read-after-Write-Muster ausführlich.
Wenn sich kein Lila unter dem Gelb befindet, hat JavaScript seine Arbeit abgeschlossen und den Browser das Rendering nach eigenem Zeitplan erledigen lassen. Das ist der angestrebte Trace.
Animations-Panel: Den Keyframe isolieren
Das Animations-Panel ermöglicht es, aktive Animationen zu inspizieren, zu scrubben und zu verlangsamen, um Jank einem bestimmten Keyframe oder einer bestimmten Property zuzuordnen, statt der Animation als Ganzes. Es lässt sich über More Tools → Animations öffnen (Chrome DevTools Animations-Dokumentation). Chrome erkennt Animationen und listet sie beim Auslösen auf, sodass die aufgezeichnete Animation inspiziert, ihre Timeline gescrubt und ihre Keyframes untersucht werden können.
Die diagnostische Stärke liegt in der Kombination mit Paint flashing. Eine Animation auf 10% Wiedergabegeschwindigkeit zu verlangsamen und dabei Paint flashing im Rendering-Panel zu beobachten, ist der schnellste Weg, um den spezifischen Keyframe zu identifizieren, der einen Repaint auslöst – das grüne Aufleuchten erscheint genau in dem Moment, in dem der problematische Property-Wert wirksam wird.
Vorgehensweise:
- Animations-Panel öffnen und die Animation auslösen, sodass sie in der Liste erscheint.
- Wiedergabegeschwindigkeit auf 10% setzen (Steuerelemente oben im Panel).
- Mit aktiviertem Paint flashing die Timeline scrubben und auf grünes Aufleuchten achten.
- Erscheint das grüne Aufleuchten an einer bestimmten Stelle der Timeline, die Untersuchung auf den zu diesem Zeitpunkt aktiven Keyframe konzentrieren.
Firefox und andere Browser verfügen über eigene Animations-Inspektoren; Chrome ist hier die zugrunde liegende Annahme.
Layers-Panel: Compositor-Kosten prüfen
Das Layers-Panel zeigt, welche Elemente auf eine eigene Compositor-Layer hochgestuft wurden, warum, und zu welchen Speicherkosten – so lässt sich vermeiden, will-change wahllos einzusetzen. Es lässt sich über More Tools → Layers öffnen (Chrome DevTools Layers-Dokumentation). Die Auswahl einer Layer zeigt im Detailbereich deren Speicherverbrauch und den Compositing-Grund an.
Promotion ist ein Kompromiss. Ein Element auf eine eigene Layer zu verschieben, ermöglicht dem Compositor, es zu animieren, ohne benachbarte Elemente neu zu zeichnen, aber jede Layer belegt GPU-Speicher für ihre Textur. MDNs will-change-Dokumentation stellt klar, dass die Property ein letztes Mittel ist: Sie auf zu viele Elemente anzuwenden, verschwendet Ressourcen, weil der Browser die günstigen Properties bereits von sich aus optimiert, und übermäßige Promotion kann die Performance verschlechtern. Das Layers-Panel verwenden, um hochgestufte Layers zu zählen und sicherzustellen, dass jede ihren Speicherbedarf rechtfertigt.
Vorher/Nachher-Beispiel: left vs. transform animieren
Die Animation von left löst bei jedem Frame ein Layout aus; die Animation von transform: translateX() löst weder Layout noch Paint aus. Dieselbe Bewegung läuft auf einem anderen Thread. Hier ist die fehlerhafte Version mit animiertem left:
/* Fehlerhaft: animiert eine Layout-Property */
.box {
position: absolute;
left: 0;
width: 100px;
height: 100px;
background: tomato;
animation: slide 1s ease-in-out infinite alternate;
}
@keyframes slide {
to {
left: 200px;
}
}
Was jedes Panel bei dieser Version anzeigt: Das Rendering-Panel lässt die Box während der gesamten Animation grün aufleuchten, weil die Änderung von left ein Layout erzwingt, dem stets ein Paint folgt. Der Performance-Panel-Main-Track füllt sich bei jedem Frame mit lila Recalculate-Style- und Layout-Balken, und der Frames-Track zeigt bei aktiviertem CPU-Throttling Budget-überschreitende Frames. left, top, width und height lösen alle ein Layout aus – siehe CSS Triggers für die Property-spezifische Aufschlüsselung – und Layout läuft auf dem Main-Thread, wo es mit allem anderen um das 16,7ms-Budget konkurriert.
Die überarbeitete Version drückt dieselbe Bewegung ausschließlich mit transform aus:
/* Korrigiert: animiert eine Composite-only-Property */
.box {
position: absolute;
left: 0;
width: 100px;
height: 100px;
background: tomato;
animation: slide 1s ease-in-out infinite alternate;
}
@keyframes slide {
to {
transform: translateX(200px);
}
}
translateX reproduziert die Positionsänderung über transform. Nach der Überarbeitung: Paint flashing zeigt während der Bewegung kein Grün mehr, der Performance-Panel-Main-Track füllt sich nicht mehr bei jedem Frame mit Lila, und die Animation läuft auf dem Compositor. Der Compositor kann transform und opacity animieren, ohne Layout oder Paint auszulösen, sodass der Browser eine bestehende Layer-Textur verschiebt, anstatt bei jedem Frame die Geometrie neu zu berechnen.
Die Lösungsliste
Die Lösung ist ein Property-Tausch: Alles, was Layout oder Paint auslöst, durch transform oder opacity ersetzen. Die Tabelle ordnet jeder Animationsabsicht ihre Composite-only-Entsprechung zu.
| Absicht | Vermeiden (löst Layout/Paint aus) | Verwenden (Composite-only) |
|---|---|---|
| Verschieben | left, top, margin | transform: translate() |
| Größe ändern | width, height | transform: scale() |
| Drehen | Layout-beeinflussende Workarounds | transform: rotate() |
| Einblenden/Ausblenden | visibility-Wechsel, Hintergrundänderungen | opacity |
Die sichersten und am weitesten unterstützten CSS-Properties für Animationen sind transform (Translation, Skalierung, Rotation, Scherung) und opacity, da Browser sie typischerweise auf dem Compositor ausführen können, ohne Layout oder Paint auszulösen. filter kann für Funktionen wie blur() ebenfalls GPU-beschleunigt sein, aber Unterstützung und Verhalten variieren – daher im Rendering-Panel mit Paint flashing prüfen, bevor man davon ausgeht, dass es kostenlos ist. MDNs filter-Dokumentation beschreibt die Property, und CSS Triggers dokumentiert ihre Rendering-Auswirkungen je Engine. Viele andere animierte Properties lösen Paint aus, und Properties, die Größe oder Position ändern, lösen typischerweise eine Layout-Neuberechnung auf dem Main-Thread aus.
Bei JavaScript-gesteuerten Animationen alle DOM-Lesezugriffe vor allen DOM-Schreibzugriffen bündeln. Das erzwungene synchrone Layout im Beispiel-Trace entsteht durch das Lesen einer Layout-Property nach einem Schreibzugriff; werden Lesezugriffe zuerst gruppiert, kann der Browser sie aus dem Layout des vorherigen Frames bedienen, anstatt ein neues zu flushen. Der Leitfaden zu Layout-Thrashing beschreibt dieses Muster im Detail.
will-change gezielt einsetzen, nicht standardmäßig. Es auf ein Element anwenden, das gleich animiert werden soll, und nach Ende der Animation wieder entfernen; laut MDN verschwendet ein breiter Einsatz GPU-Speicher, weil der Browser die günstigen Properties bereits selbst optimiert. Den Effekt im Layers-Panel bestätigen.
Scroll-gesteuerte Animationen: Ein anderes Jank-Muster
Scroll-gesteuerte Animationen, die mit animation-timeline: scroll() oder animation-timeline: view() deklariert werden, verändern den Blickwinkel in den DevTools. Wenn sie nur transform oder opacity animieren, laufen sie auf dem Compositor, sodass ihr Jank nicht als Long Task im Main-Thread-Track erscheint – stattdessen im Frames-Track nach gedropten Frames suchen. MDNs animation-timeline-Dokumentation und der Chrome-Leitfaden zu scroll-gesteuerten Animationen behandeln das Feature und seine Browser-Unterstützung. Wenn die Main-Track-Heuristik nichts ergibt, der Frames-Track aber weiterhin Budget-überschreitende Frames zeigt, ist wahrscheinlich eine nicht-compositierbare Property in die scroll-gesteuerten Keyframes eingeschlichen.
Warum ruckelt eine Animation nur in der Produktion?
DevTools-Profile entstehen unter kontrollierten Bedingungen; die Variable, die sich nicht vollständig reproduzieren lässt, ist der reale Nutzerkontext – CPU-Klasse des Geräts, Speicherdruck, gleichzeitige Aktivitäten. Wenn sich ein Jank-Bericht lokal nicht reproduzieren lässt, ist dieser fehlende Kontext in der Regel die Ursache. Session-Replay erfasst ihn, sodass bekannt ist, welche Bedingungen vor der Aufzeichnung simuliert werden müssen.
Die vier Panels der Reihe nach durchlaufen – Rendering zur Bestätigung, Performance zur Ursachenanalyse, Animations zur Isolierung, Layers zur Überprüfung – und die nächste ruckelnde Animation ist keine Raterei mehr.
FAQs
Gerätekontext, nicht der Code. Ein mittelklassiges Android-Gerät mit mehreren offenen Tabs verfügt über einen Bruchteil des CPU-Budgets eines Laptops. CPU-Throttling in den Capture settings des Performance-Panels aktivieren und Session-Replay verwenden, um die tatsächlichen Bedingungen zu erfassen, unter denen Jank in der Produktion aufgetreten ist.
`transform` läuft auf dem Compositor-Thread, ohne Layout oder Paint auszulösen – der Browser verschiebt bei jedem Frame lediglich eine bestehende Layer-Textur. `left` oder `top` erzwingt bei jedem Frame eine Layout-Neuberechnung auf dem Main-Thread, gefolgt von einem Paint, und konkurriert dabei mit JavaScript um das 16,7ms-Budget.
Nicht zuverlässig. Nur `transform` und `opacity` sind garantiert Composite-only. `filter` kann in manchen Engines für Funktionen wie `blur()` GPU-beschleunigt sein, aber die Unterstützung variiert. Im Rendering-Panel mit Paint flashing prüfen: Grünes Aufleuchten bedeutet, dass bei jedem Frame neu gezeichnet wird.
`animation-timeline: scroll()` und `view()` laufen auf dem Compositor, wenn sie nur `transform` oder `opacity` animieren, und erzeugen keinen Long Task auf dem Main-Thread. Jank erscheint stattdessen im Frames-Track. Wenn der Main-Track nichts zeigt, der Frames-Track aber Budget-überschreitende Frames anzeigt, ist wahrscheinlich eine nicht-compositierbare Property in die Keyframes eingeschlichen.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.