Zurück zum Blog
Aug. 2026·3 Min. LesezeitKI-generiert
Sprache:DeutschEnglishFrançaisÖsterreichischKlingonisch Originalfassung: English

Reflows und Repaints: Warum sich deine Seite ruckelig anfühlt

#performance#browser#frontend

Du misst die Höhe eines Elements mit offsetHeight, setzt damit die Breite eines anderen Elements, liest diese Breite zurück und setzt die Höhe eines dritten – und irgendwie ruiniert diese fünfzehnzeilige Funktion ganz allein deine Framerate. Der Schuldige taucht bei naivem Lesen des Codes fast nie als „langsames JavaScript" auf. Es ist der Browser, der stillschweigend das Layout deiner gesamten Seite mehrfach neu berechnet, wegen der Reihenfolge, in der du Dinge angefragt hast.

Was ein Reflow tatsächlich ist

Ein Reflow (auch Layout genannt) ist die Neuberechnung der Seitengeometrie durch den Browser – wo jedes Element sitzt, wie groß es ist, wie Text umbricht –, weil sich etwas geändert hat, das Positionen oder Größen betrifft: ein Klassenwechsel, der width ändert, ein neu ins DOM eingefügtes Element, sogar ein Font, dessen Download abgeschlossen wurde. Ein Repaint ist billiger: Die Geometrie blieb gleich, aber Pixel müssen neu gezeichnet werden, weil sich Farbe oder Schatten geändert haben. Reflows sind die teuren, weil eine einzelne Layoutänderung eine Kaskade auslösen kann – ein Kasten in der Größe zu ändern kann alles darunter und daneben verschieben.

Browser wissen das, daher bündeln sie Layoutarbeit standardmäßig. Ändert man fünf Style-Eigenschaften hintereinander, berechnet der Browser das Layout nicht fünfmal neu – er wartet bis zum nächsten Frame, macht einen Durchlauf und macht weiter.

Die Falle: Layout-Eigenschaften lesen

Dieses Bündeln bricht in dem Moment zusammen, in dem man nach einem Schreiben eine layoutabhängige Eigenschaft liest. Eigenschaften wie offsetHeight, offsetWidth, getBoundingClientRect() und scrollHeight können nur korrekt beantwortet werden, wenn das interne Layout des Browsers aktuell ist – ihr Lesen erzwingt also eine sofortige, synchrone Neuberechnung und wirft die Bündelungsoptimierung weg.

const box = document.querySelector('.box');

box.style.width = '300px';       // write
const height = box.offsetHeight; // read — forces a synchronous reflow right here
box.style.height = height + 'px'; // write

Ein einzelnes Schreiben-dann-Lesen-Paar wie dieses ist nicht katastrophal. Der eigentliche Schaden entsteht, wenn dieses Muster in einer Schleife läuft:

const items = document.querySelectorAll('.item');

items.forEach((item) => {
  item.style.width = '100px';         // write
  const height = item.offsetHeight;   // read — forces reflow, every iteration
  item.style.height = `${height * 2}px`;
});

Das nennt man „Layout Thrashing": schreiben, lesen, schreiben, lesen – jedes Lesen zwingt den Browser, Layoutarbeit, die er gerade erledigt hat, immer wieder zu wiederholen, einmal pro Element. Bei hundert Elementen sind das hundert erzwungene synchrone Reflows für etwas, das einer hätte sein sollen.

Die Lösung: erst alle Lesevorgänge bündeln, dann alle Schreibvorgänge

Die Lösung ist fast mechanisch, wenn man das Muster einmal sieht: alle Lesevorgänge von allen Schreibvorgängen trennen.

const items = document.querySelectorAll('.item');

// Read phase — collect everything first
const heights = Array.from(items).map((item) => item.offsetHeight);

// Write phase — apply everything after
items.forEach((item, i) => {
  item.style.width = '100px';
  item.style.height = `${heights[i] * 2}px`;
});

Jetzt gibt es genau einen Stapel Lesevorgänge (der Browser kann alle aus dem bereits vorhandenen Layout beantworten, da sich dazwischen nichts geändert hat) und einen Stapel Schreibvorgänge (die der Browser zurückstellen und vor dem nächsten Paint zu einem einzigen Reflow zusammenfassen kann).

Schreibvorgänge mit requestAnimationFrame planen

Bei Layoutänderungen, die durch Dinge wie Scroll- oder Resize-Handler getrieben werden, wickle den Schreibvorgang in requestAnimationFrame, damit er direkt vor dem Paint-Zyklus des Browsers landet, statt mit ihm zu konkurrieren:

let ticking = false;

window.addEventListener('scroll', () => {
  if (ticking) return;
  ticking = true;

  requestAnimationFrame(() => {
    const scrollY = window.scrollY; // read
    header.style.transform = `translateY(${Math.min(scrollY, 100)}px)`; // write
    ticking = false;
  });
});

Das Flag ticking verhindert, dass mehr als die Arbeit eines einzigen ausstehenden Frames eingereiht wird – ohne es kann ein schnelles Scrollen Dutzende Callbacks einreihen, die alle direkt hintereinander feuern.

Eigenschaften bevorzugen, die das Layout ganz überspringen

Manche CSS-Eigenschaften lassen sich animieren, ohne Layout oder sogar Paint auszulösen – der Browser kann sie direkt an den Compositor übergeben. transform und opacity sind die zwei, die in der Praxis am wichtigsten sind:

/* Triggers layout on every frame — avoid animating this */
.slide-in {
  transition: left 0.3s ease;
}

/* Compositor-only — cheap regardless of how large the element is */
.slide-in {
  transition: transform 0.3s ease;
}
// Instead of animating `left`, `top`, `width`, or `height`...
el.style.left = '200px';

// ...animate transform, which doesn't touch layout at all
el.style.transform = 'translateX(200px)';

Für ein bewegtes oder ausblendendes Element ist das meist ein Drop-in-Austausch und die wirkungsvollste Einzeländerung, die man an einer ruckeligen Animation vornehmen kann.

Thrashing finden, von dem du nichts wusstest

Das Performance-Panel der DevTools markiert das direkt – aufgezeichnete Reflows erscheinen als violette „Layout"-Balken, und Chrome weist in der Konsole ausdrücklich auf „Forced reflow" hin, wenn ein Skript einen auslöst, mit einem Stacktrace auf die genaue Zeile, die mitten im Stapel eine Layout-Eigenschaft gelesen hat. Zeichne ein paar Sekunden der Interaktion auf, die sich langsam anfühlt, und suche nach einem Sägezahnmuster violetter Balken; eine durchgehende gelbe Strecke (Skript) ohne Violett bis zum Ende des Frames bedeutet, dass du nicht thrashst, sondern einfach langsam rechnest – ein anderes Problem mit einer anderen Lösung.

Fazit

Der Browser bündelt Layoutarbeit gern für dich – bis du mitten im Stapel eine Geometrie-Eigenschaft liest und ihn zwingst, vorzeitig aufzuholen. Trenne Lesen von Schreiben, verschiebe Schreibvorgänge nach requestAnimationFrame, wenn sie von kontinuierlichen Events getrieben werden, und nimm transform/opacity statt left/width/height, wo der visuelle Effekt es erlaubt. Nichts davon erfordert ein Framework oder eine Bibliothek; es geht ausschließlich um die Reihenfolge, in der Operationen passieren.

NeuerEine einfache State Machine in TypeScript bauenÄlterHTTP-Caching-Header verstehen