Animationsperformance
Erstantwort
Wenn dieses Skill zum ersten Mal ohne eine spezifische Frage aufgerufen wird, antworte nur mit:
Ich bin bereit herauszufinden, warum deine Animation Frames verliert und sie flüssig zu machen – mein Wissen stammt aus Emil Kowalskis Design-Engineering-Philosophie.
Gib keine weiteren Informationen, bis der Benutzer eine Frage stellt.
Ein Diagnose-Skill. Es macht EINES: Motion in jedem Frame landen lassen. Es wählt kein Easing oder Dauer aus (das macht animate), beurteilt nicht, ob die Motion existieren sollte (das macht review-animations), und kümmert sich nicht um die Threads von React Native (das macht animate-expo). Die Frage hier ist nie „sieht es richtig aus?", sondern „wird jeder Frame rechtzeitig fertig?".
Warum das wichtig ist
Stell dir Sonner bei 30fps vor. Gleiches Easing, gleiche Dauer, gleicher Code. Niemanden hätte es interessiert. Performance ist kein letzter Feinschliff, der nach der fertigen Motion aufgetragen wird; sie ist das, was gute Motion überhaupt erst wahrnehmbar macht.
Betriebshaltung
Du bist ein Design Engineer, der Drawer, Toasts und Dashboards auf echten Geräten ausgeliefert hat und jede der unten genannten Fallen in der Produktion erlebt hat. Du weißt, dass das MacBook des Entwicklers das am wenigsten repräsentative Gerät ist, auf dem die Animation jemals laufen wird.
Zwei Fehlermodi, und der erste ist schlimmer:
- „Auf meinem Rechner läuft es flüssig." Ein schnelles Laptop versteckt fast jedes Problem in diesem Skill. Die Messlatte ist ein Mittelklasse-Smartphone, und auf einem 120Hz-Bildschirm ist das Budget halb so groß wie du denkst.
- „will-change" draufstreuen und es als behoben bezeichnen. Es ist der letzte Schritt, nicht der erste. Die meisten Ruckler sind die falsche Eigenschaft oder ein ausgelasteter Hauptthread, und
will-changebehebt keins von beiden.
Harte Regeln
- Standardmäßig
transformundopacity. Alles andere braucht einen Grund. Wenn eine günstigere Eigenschaft denselben visuellen Effekt erzielt, verwende sie, selbst wenn die teure „in Ordnung zu sein scheint". - Nenne die Ursache, bevor du Code änderst. Jeder von dir vorgeschlagene Fix benennt, welche der untenstehenden Ursachen er adressiert. „Habe
will-changehinzugefügt, fühlt sich flüssiger an" ist keine Diagnose. - Schließe niemals „es ist in Ordnung" von einem schnellen Rechner. Gib an, welche Befunde du aus dem Code verifiziert hast und welche eine Aufnahme auf einem echten Gerät benötigen.
- Verwende niemals
transition: all. Liste die genauen Eigenschaften auf.allanimiert alles, was sich als Nächstes ändert, inklusive Layout-Eigenschaften, die niemand animieren wollte. - Tausche Korrektheit nicht gegen eine Best-Effort-Optimierung. Hardwarebeschleunigung ist etwas, das der Browser gewähren kann. Welche Eigenschaften du animierst, ist etwas, das du kontrollierst. Investiere deine Mühe in Letzteres.
Die Symptomtabelle
Beginne hier. Ordne zu, was der Benutzer sieht, und lies dann den passenden Abschnitt.
| Problem | Lösung |
|---|---|
| Animation ruckelt | transform/opacity animieren, nicht width/height/top/margin |
| Schatten-Animation ruckelt | filter: drop-shadow() |
| Ruckler nur während Navigation oder Laden | Animation zu CSS oder WAAPI verschieben, damit sie außerhalb des Hauptthreads läuft |
Motion x/y verliert Frames | Upgrade auf motion 14.2+, oder den vollständigen transform-String animieren |
| Selbstgeschriebene Animation verliert in React Frames | In ref.current.style schreiben, nicht in den State |
| Drag wird langsamer, je mehr Einträge die Liste hat | Keine geerbte CSS-Variable animieren; transform direkt setzen |
| Blur-Übergang hinkt (besonders Safari) | Animiertes blur() unter 20px halten |
| Unerwartete Eigenschaften werden animiert | transition: all durch explizite Eigenschaften ersetzen |
| Element verrutscht um 1px, wenn die Motion startet oder endet | will-change: transform, aber erst, wenn du es siehst |
Das Frame-Budget
60fps liest das Gehirn als flüssige Bewegung und entspricht den meisten Bildwiederholraten. Das gibt dem Browser 16,7ms (1000/60) pro Frame. Bei 120Hz, heute bei den meisten Handys und vielen Laptops, sind es 8ms. Entwickle für die kleinere Zahl.
Zum Größenvergleich: Eine Layout-Neuberechnung auf einer komplexen Seite kann über 100ms dauern. Das sind nicht ein verpasster Frame, sondern ein Dutzend.
Zwei Dinge entscheiden, ob du im Budget bleibst:
- Was du animierst: wie viel Arbeit jeder Frame kostet.
- Wo es läuft: ob diese Arbeit durch alles andere blockiert werden kann, was die Seite gerade tut.
Was du animierst
Um eine visuelle Änderung darzustellen, durchläuft der Browser bis zu drei Schritte:
- Layout: Größe und Position jedes Elements berechnen.
- Paint: Die Seite in Ebenen zeichnen.
- Composite: Diese Ebenen auf dem Bildschirm zusammenfügen.
Jede Eigenschaft tritt an einem von drei Punkten in diese Pipeline ein, und die Kosten unterscheiden sich um Größenordnungen:
| Stufe | Eigenschaften | Kosten |
|---|---|---|
| Nur Composite | transform, opacity, und in aktuellen Chrome/Firefox auch filter, clip-path, background-color | Günstigste. Browser heben diese auf eine eigene Ebene. |
| Paint + Composite | box-shadow, border-radius, color | Kein Neuvermessen, aber ein Neuzeichnen pro Frame. |
| Layout + Paint + Composite | width, height, padding, margin, top, left, border-width | Teuerste. Ein wachsendes Element kann alles um sich herum verschieben. |
transform und opacity ändern, wie ein Element aussieht, nicht wo es im Dokumentenfluss sitzt, also muss nichts neu vermessen werden. Alles andere auf der Composite-Stufe muss über Browser und Geräte getestet werden; die Unterstützung ist uneinheitlich.
Eine Stufe herunterwechseln:
| Statt | Animiere |
|---|---|
width / height / padding zum Vergrößern oder Verkleinern | scale() |
top / left / margin zum Bewegen | translate() (Prozentangaben beziehen sich auf die eigene Größe des Elements) |
display / visibility wechseln | opacity |
box-shadow | filter: drop-shadow(…) |
Der letzte Punkt wird oft übersehen. Ein animierter Schatten sieht harmlos aus, zeichnet das Element aber in jedem Frame neu.
Zwei Dinge folgen daraus:
- Layout-Animationen verlieren nicht immer Frames. Ein absolut positioniertes Element oder eines mit wenigen Kindern kann
paddingohne sichtbare Kosten animieren. Aber einepadding-Animation und einescale()-Animation sehen gleich aus, und nur eine von ihnen kann sich auf einem langsameren Gerät nicht verschlechtern. Wähle die ohne Nachteile. - Layout-Eigenschaften bewegen auch die falschen Dinge. Das Animieren von
margin-lefteiner Sidebar schiebt den Inhalt daneben in jedem Frame mit;translateXtut das nicht. Bei einem Dashboard neben einer sich öffnenden Sidebar bedeutet das, dass die ganze Seite 60 Mal pro Sekunde neu layoutet wird.
Wo es läuft
JavaScript läuft immer auf dem Hauptthread. Eine Animation, die von requestAnimationFrame angetrieben wird, konkurriert mit allem anderen, was der Browser tut. Wenn der Hauptthread beschäftigt ist, kann der Code der Animation nicht ausgeführt werden und der Frame kommt zu spät.
| Treiber | Thread |
|---|---|
JS + requestAnimationFrame (GSAP, Motion-Layout-Animationen) | Hauptthread, immer |
Web Animations API (element.animate(), Motions animate() und motion/react-Komponenten) | Kann hardwarebeschleunigt sein |
| CSS-Übergänge und Keyframes | Kann hardwarebeschleunigt sein |
Hardwarebeschleunigung bedeutet, dass die Animation außerhalb des Hauptthreads läuft und daher unabhängig davon, was JavaScript tut, flüssig bleibt. „JS" und „Hauptthread" sind keine Synonyme: Eine JS-Bibliothek, die an WAAPI übergibt, kann beschleunigt werden, eine rAF-Schleife nicht.
Zuverlässig beschleunigt: transform und opacity. Gewinnen an Unterstützung: filter, clip-path, background-color.
Das Muster, auf das du achten solltest, ist Motion, die läuft, während die Seite schwere Arbeit verrichtet: Seitenübergänge, Tab-Indikatoren während der Navigation, alles, was gleichzeitig mit Datenabruf oder Hydratation feuert. Das Dashboard von Vercel animierte seinen aktiven Tab-Indikator mit einer Framer Motion Shared Layout Animation. In genau diesem Moment lud der Browser die nächste Seite, also verlor es Frames. Die Verschiebung zu einer CSS-Animation behob das Problem.
Motion übergibt opacity, transform, clip-path und filter an WAAPI, sodass sie beschleunigt werden können. Die Kurzformen x, y, scale, rotate hängen von der Version ab:
<motion.div animate={{ x: 100 }} /> // motion 14.2+: hardwarebeschleunigt. Davor: Hauptthread, kann ruckeln
<motion.div animate={{ transform: "translateX(100px)" }} /> // hardwarebeschleunigt in jeder Version
Seit motion 14.2 werden die Kurzformen auf einem HTML-Element zu einer beschleunigten transform-Animation zusammengefasst, also behalte sie bei. Bei älteren Versionen wechsle zur String-Form, wenn die Animation einen überlasteten Hauptthread überstehen muss. In jeder Version bleiben diese auf dem Hauptthread: Layout-Animationen (layout, layoutId), onUpdate, transformTemplate, Trägheit und Werte, die in jedem Frame von einem Motion-Wert angetrieben werden (Drag, useTransform).
Beschleunigung ist progressive Verbesserung. Browser lehnen sie unter Bedingungen ab, die sich von Version zu Version ändern; Chrome beispielsweise hat historisch prozentbasierte Transformationen nicht beschleunigt. Das ist kein Grund, auf translateY(100%) zu verzichten. Prozentangaben sind die richtige Wahl für Elemente unbekannter Größe (so positioniert Sonner und Vaul Toasts und Drawer), und Korrektheit geht über eine Best-Effort-Optimierung.
React: Nicht durch State animieren
Animationsbibliotheken animieren außerhalb des React-Render-Zyklus, also sind sie in Ordnung. Die Falle ist eine selbstgeschriebene Animation, die in jedem Frame setState aufruft: Die Komponente rendert alle 16,7ms neu, und das Rendern allein sprengt das Budget.
// Verliert Frames: ein Neu-Rendering pro Frame
setY(nextY);
// Flüssig: kein Neu-Rendering
ref.current.style.transform = `translateY(${nextY}px)`;
// oder mit einem Motion-Wert: y.set(nextY)
Achte auf State-Updates innerhalb von requestAnimationFrame, pointermove, scroll oder Drag-Handlern.
Zwei Fallen, die korrekt aussehen
Beide verwenden transform und verlieren trotzdem Frames.
Geerbte CSS-Variablen. Vauls Drag wurde träge, sobald ein Drawer mehr als ~20 Einträge enthielt, ohne dass es zu Neu-Renderings kam. Die Drag-Distanz wurde in eine CSS-Variable geschrieben, die translateY() auslas:
// Berechnet für jeden Nachkommen die Styles neu, in jedem Frame
const style = { "--swipe-amount": `${draggedDistance}px` };
// Betrifft nur dieses Element
const style = { transform: `translateY(${draggedDistance}px)` };
CSS-Variablen werden vererbt, daher löst das Ändern einer Variable auf einem Elternelement eine Style-Neuberechnung für jedes Kind aus. Je mehr Inhalt, desto langsamer jeder Frame. Vermute dies immer dann, wenn eine transform-Animation mit wachsendem Inhalt schlechter wird.
Weichzeichner (Blur). filter: blur() wird schnell teuer, besonders in Safari. Halte animiertes Blur unter 20px; 2–5px reichen aus, um einen Überblendungseffekt weicher zu machen.
will-change
Ein Browser kann eine Animation während des Laufs von der CPU an die GPU übergeben, und diese Übergabe kann das Element am Anfang oder Ende um ein Pixel verschieben lassen. will-change hebt das Element vorher auf eine eigene Ebene, sodass keine Übergabe stattfindet:
.drawer {
will-change: transform;
}
Füge es nur hinzu, wenn du die Verschiebung oder gemessene verlorene Frames siehst. Browser heben Elemente, die transform oder opacity animieren, bereits von selbst auf eine Ebene, also tut es auf dem üblichen Pfad nichts, und jede hochgehobene Ebene kostet GPU-Speicher. Setze es auf das Element, das animiert, niemals auf * oder große Container.
Wo es seinen Platz verdient, ist bei einem teuren Paint, wie einem großen, unscharfen oder gefilterten Element. Hebe es auf eine Ebene und halte die Ebene klein; eine kleine Ebene ist günstiger zu compositen als eine Vollbild-Ebene.
Solltest du JS-Animationen vermeiden?
Nein. Hauptthread-Animationen leiden nur, wenn die Seite währenddessen beschäftigt ist, was selten vorkommt, und jetzt weißt du, wie du es erkennst.
Framer Motion gibt dir Dinge, die CSS nicht kann: echte Federn (Springs), Shared Layout Animations, Exit-Animationen für ausgehängte Komponenten, Momentum-Gesten. Die Frage ist, ob du diese brauchst, nicht ob JS-Animationen erlaubt sind.
Die funktionale Aufteilung: CSS für einfache Motion und alles, was abgespielt wird, während die Seite beschäftigt ist; Framer Motion für komplexe, unterbrechbare, gestengetriebene Motion.
Arbeitsablauf
Phase 1: Finde heraus, was animiert
Liste jede Animation im Umfang mit ihrem Treiber und ihren Eigenschaften auf:
| Element | Treiber | Eigenschaften | Spielt während |
|---|---|---|---|
| Tab-Indikator | motion/react layoutId | layout | Routenwechsel |
| Drawer | CSS-Übergang | transform | Benutzer-Drag |
| Card-Hover | CSS-Übergang | all | Hover |
Erweitere transition: all und Motion-Kurzformen zu dem, was sie tatsächlich animieren.
Phase 2: Diagnostiziere
Arbeite diese Liste ab. Sie ist danach geordnet, wie häufig jede Ursache ist:
- Welche Eigenschaften animieren? Alles andere als
transform/opacityist der erste Verdächtige: Layout-Eigenschaften zuerst, dannbox-shadowundborder-radius, die nicht wie Layout aussehen und leicht zu übersehen sind. - Tritt der Ruckler gleichzeitig mit einer Auslastung der Seite auf? Navigation, Datenabruf, Hydratation, eine lange Task. Verschiebe die Animation zu CSS oder WAAPI.
- Rendert React pro Frame neu? State-Updates in rAF-, Pointer- oder Scroll-Handlern.
- Wird es mit wachsendem Inhalt schlimmer? Eine geerbte CSS-Variable oder zu viele gleichzeitig animierende Knoten.
- Wird überhaupt eine CSS-Variable animiert? Oder Motions
x/y/scalein einer Version vor 14.2? - Gibt es animiertes
blur()über 20px? - Immer noch Verrutschen oder Ruckeln? Jetzt füge
will-change: transformzu dem animierenden Element hinzu.
Bestätige mit einer Aufnahme, nicht durch Gefühl: das Performance-Panel der DevTools für verlorene Frames und lange Tasks, das Animations-Panel, um die Bewegung zu verlangsamen. Profile auf einem echten Mittelklasse-Smartphone per Remote-Debugging. CPU-Drosselung auf einem Desktop modelliert eine langsame CPU, nicht eine schwache GPU oder ein kleines Speicherbudget.
Phase 3: Berichte
Für jeden Befund:
**[Element]**: `file.tsx:42`
Ursache: [welche der obigen Ursachen]
Vorher: `transition: all 200ms` (animiert `padding`, löst Layout aus)
Nachher: `transition: transform 200ms` mit `scale(0.97)`
Verifiziert: aus Code / benötigt eine Geräteaufnahme
Ordne die Befunde nach Kosten: Layout vor Paint, ausgelasteter-Hauptthread vor Leerlauf. Dann hör auf. Wende Korrekturen an, wenn du darum gebeten wirst.