Rendering · 3 Min. Lesezeit

Ein Frame, 16,7 Millisekunden: die Rendering-Pipeline

Was zwischen einer Style-Änderung und dem nächsten Bild passiert und warum transform und opacity fast nichts kosten.

Ein Bildschirm mit 60 Hz zeigt alle 16,7 ms ein neues Bild, einer mit 120 Hz sogar alle 8,3 ms. Damit eine Animation oder das Scrollen flüssig wirkt, muss der Browser in dieser Zeit alles erledigen, was für das nächste Bild nötig ist. Einen Teil des Budgets braucht er selbst; für eigenes JavaScript bleiben bei 60 Hz realistisch etwa 10 ms.

Fünf Schritte

Vereinfacht durchläuft jedes Frame diese Stationen:

  1. JavaScript – Event-Handler, Timer, requestAnimationFrame-Callbacks; hier entstehen die meisten Änderungen.
  2. Style – der Browser ermittelt, welche CSS-Regeln für welches Element gelten, und berechnet die Werte.
  3. Layout – Größe und Position jedes Elements. Weil Elemente einander beeinflussen, kann eine kleine Änderung große Teile der Seite betreffen.
  4. Paint – Pixel füllen: Text, Farben, Rahmen, Schatten, Bilder, meist aufgeteilt auf mehrere Ebenen.
  5. Composite – die Ebenen in der richtigen Reihenfolge übereinanderlegen. Das erledigt ein eigener Thread, oft mit Hilfe der GPU.

Nicht jede Änderung braucht alle fünf Schritte. Welche übersprungen werden, hängt davon ab, welche Eigenschaft sich ändert:

Je nachdem, welche Eigenschaft sich ändert, fallen Schritte weg. Geometrie kostet alle fünf, reine Farbe spart das Layout, transform und opacity brauchen weder Layout noch Paint.

Warum transform und opacity so billig sind

transform und opacity verändern weder die Geometrie noch die Pixel eines Elements, nur wie seine fertige Ebene zusammengesetzt wird. Liegt das Element auf einer eigenen Ebene, kann der Compositor-Thread die Animation allein weiterführen – auch dann, wenn der Main Thread gerade mit einem langen Task beschäftigt ist.

Beide Kästchen legen denselben Weg zurück. Das obere animiert left und braucht dafür in jedem Frame Layout und Paint; das untere animiert transform und kommt mit dem Compositor aus.

Hier sehen beide gleich aus, weil der Main Thread nichts zu tun hat. Blockiert JavaScript ihn für 200 ms, bleibt das obere Kästchen so lange stehen, das untere läuft weiter.

/* löst in jedem Frame Layout und Paint aus */
@keyframes slide-left {
  from { left: 0; }
  to   { left: 300px; }
}

/* braucht nur den Compositor */
@keyframes slide {
  from { transform: translateX(0); }
  to   { transform: translateX(300px); }
}

Ebenen gezielt einsetzen

Mit will-change: transform kann man dem Browser vorab sagen, dass ein Element gleich animiert wird; er legt es dann auf eine eigene Ebene. Das ist kein Allheilmittel: Jede Ebene kostet Grafikspeicher, und hunderte davon machen das Compositing selbst langsam. Am besten setzt man es kurz vor der Animation und entfernt es danach wieder.

Arbeit ganz sparen

Am billigsten ist ein Schritt, der gar nicht stattfindet. Bei langen Seiten lässt content-visibility: auto den Browser Style, Layout und Paint für Bereiche außerhalb des Bildschirms überspringen, bis sie in die Nähe kommen:

.kapitel {
  content-visibility: auto;
  contain-intrinsic-size: auto 800px; /* geschätzte Höhe, damit die Scrollleiste nicht springt */
}

Ob es wirkt, zeigen die Entwicklertools: Im Performance-Panel schrumpfen die violetten (Style, Layout) und grünen (Paint) Blöcke, und unter Rendering färbt Paint flashing alle Bereiche ein, die neu gemalt werden. Was passiert, wenn JavaScript das Layout mitten im Frame erzwingt, steht in Erst lesen, dann schreiben: Layout Thrashing vermeiden.

← Alle Notizen