Reflow, Repaint und Layout Thrashing vermeiden
60fps
ms
Performance · Rendering-Pipeline · Main Thread · DevTools
Reflow, Repaint und Layout Thrashing vermeiden
Die Rendering-Pipeline verstehen und den Main Thread entlasten

Jeder DOM-Zugriff, der eine Layout-Eigenschaft liest, kann eine komplette Neuberechnung der Seitengeometrie erzwingen, bevor der Browser überhaupt zeichnet. Wer Lese- und Schreibzugriffe in Schleifen mischt, erzeugt Layout Thrashing und blockiert den Main Thread für hunderte Millisekunden. Dieser Artikel erklärt die Rendering-Pipeline, Forced Synchronous Layout und Batching-Patterns, mit denen Interfaces flüssig bei 60 Bildern pro Sekunde bleiben.

14 Min. Lesezeit Reflow · Repaint · Composite · Forced Layout Chrome DevTools · Hyvä Theme · Alpine.js

1. Warum Layout-Kosten die Interaktivität bestimmen

Ein Browser hat bei 60 Bildern pro Sekunde nur 16,66 Millisekunden Zeit pro Frame, um JavaScript auszuführen, Style zu berechnen, Layout zu bestimmen, zu zeichnen und zu compositen. Wird dieses Budget überschritten, fällt die Seite unter 60fps und Nutzer nehmen Scrollen, Hover-Effekte und Klick-Feedback als ruckelig wahr. Layout-Berechnungen sind dabei mit Abstand die teuerste Phase, weil sie über den gesamten betroffenen Teilbaum des DOM propagieren können und nicht nur ein einzelnes Element betreffen.

Der entscheidende Punkt: Layout, Paint und Composite laufen synchron auf dem Main Thread, außer bei bestimmten composite-only Eigenschaften. Ein teurer Reflow blockiert deshalb nicht nur das Rendering, sondern auch die Verarbeitung von Klick- und Scroll-Events, die auf demselben Thread warten. Genau das macht Layout-Kosten zu einem Interaktivitätsproblem und nicht nur zu einem optischen Ruckeln: Eingaben fühlen sich verzögert an, weil der Main Thread mit Geometrie-Berechnung beschäftigt ist, statt den Event-Handler auszuführen.

2. Die Rendering-Pipeline: Layout, Paint und Composite im Kostenvergleich

Die Rendering-Pipeline eines Browsers durchläuft vier Phasen: Style-Berechnung, Layout, Paint und Composite. Die Style-Phase ermittelt, welche CSS-Regeln auf welches Element zutreffen. Die Layout-Phase, oft auch Reflow genannt, berechnet die exakte Geometrie: Position, Breite, Höhe und wie sich das auf Geschwister- und Elternelemente auswirkt. Diese Phase ist algorithmisch am teuersten, weil eine Änderung an einem Element Kaskadeneffekte auf den gesamten sichtbaren Teilbaum haben kann.

Paint rastert die berechnete Geometrie in Pixel auf einer oder mehreren Ebenen, sogenannten Layern. Composite kombiniert diese Layer abschließend zum finalen Bild, meist direkt auf dem GPU-Compositor-Thread. Der Kostenunterschied ist erheblich: Layout auf einem mittelgroßen DOM-Teilbaum kann mehrere Millisekunden kosten, Paint je nach Komplexität ähnlich viel, während reines Compositing bei composite-only Eigenschaften wie transform oder opacity oft unter einer Millisekunde bleibt und komplett ohne Main-Thread-Beteiligung läuft.

3. Was konkret einen Reflow auslöst

Ein Reflow wird durch zwei Arten von Operationen ausgelöst: Schreibzugriffe, die die Geometrie ändern, und Lesezugriffe, die eine aktuelle Geometrie benötigen. Zu den Schreibzugriffen zählen Änderungen an width, height, margin, padding, border, display, position, top, left, font-size sowie das Hinzufügen oder Entfernen von DOM-Knoten. Diese Änderungen markieren den Layout-Baum als "dirty", der Browser plant aber normalerweise eine einzige Neuberechnung am Ende des Frames, statt sofort zu reagieren.

Kritisch wird es bei Lesezugriffen: Eigenschaften wie offsetHeight, offsetWidth, offsetTop, offsetLeft, clientHeight, clientWidth, clientTop, clientLeft, scrollHeight, scrollWidth, scrollTop, scrollLeft sowie die Methoden getBoundingClientRect(), getComputedStyle() und sogar focus() benötigen eine garantiert aktuelle Geometrie. Existiert ein pending Layout, erzwingt der Zugriff eine sofortige, synchrone Neuberechnung, bevor der gelesene Wert zurückgegeben wird. Diese Kombination aus verzögertem Schreiben und erzwungenem Lesen ist die Wurzel jedes Layout-Thrashing-Problems.

4. Was nur einen Repaint auslöst, ohne Layout

Nicht jede visuelle Änderung ist gleich teuer. Eigenschaften, die die Geometrie eines Elements nicht verändern, aber sein Erscheinungsbild beeinflussen, lösen nur einen Repaint aus, überspringen also die teure Layout-Phase komplett. Dazu gehören color, background-color, background-image, visibility, outline, box-shadow und border-radius. Der Browser weiß aus der CSS-Spezifikation, dass diese Eigenschaften keinen Einfluss auf Position oder Größe anderer Elemente haben, und kann die Layout-Berechnung deshalb komplett auslassen.

Trotzdem bleibt Repaint nicht kostenlos: Auf großen oder komplexen Flächen, etwa bei einem box-shadow mit weichem Blur-Radius über einer breiten Fläche, kann das Rastern in Pixel selbst mehrere Millisekunden dauern, weil jedes betroffene Pixel neu berechnet werden muss. Der praktische Unterschied zu Layout ist trotzdem entscheidend: Repaint betrifft nur die eigene Paint-Fläche des Elements, propagiert nicht durch den DOM-Baum und lässt sich in vielen Fällen durch eine eigene Compositor-Ebene weiter beschleunigen.

5. Forced Synchronous Layout: Die Mechanik von Layout Thrashing

Layout Thrashing entsteht, wenn Code abwechselnd schreibt und liest, ohne die beiden Operationen zu trennen. Ein einzelner Schreibzugriff auf style.height markiert den Baum als dirty, kostet für sich genommen aber wenig, weil der Browser die Neuberechnung normalerweise bis zum Ende des Frames aufschiebt. Folgt jedoch direkt danach ein Lesezugriff wie offsetHeight, kann der Browser die veraltete Geometrie nicht mehr verwenden und erzwingt eine sofortige, synchrone Layout-Berechnung, bekannt als Forced Synchronous Layout oder Forced Reflow.

In einer Schleife mit hundert Elementen, die jeweils schreiben und direkt danach lesen, entstehen so hundert vollständige, synchrone Layout-Durchläufe statt eines einzigen am Frame-Ende. Jeder dieser Durchläufe kann bei einem größeren DOM-Baum mehrere Millisekunden kosten, wodurch aus einer eigentlich billigen Operation schnell ein Main-Thread-Block von hunderten Millisekunden wird. Das Tückische daran: Der einzelne Lese- oder Schreibaufruf sieht im Code völlig harmlos aus, erst die Reihenfolge über die Iteration hinweg erzeugt das Problem.


// BAD: forced synchronous layout inside a loop (classic layout thrashing)
function resizeCardsToTallest(cards) {
  cards.forEach((card) => {
    // READ: offsetHeight forces the browser to flush any pending layout work
    const currentHeight = card.offsetHeight;

    // WRITE: changing height invalidates layout again
    card.style.height = (currentHeight + 24) + 'px';

    // The next iteration's READ (offsetHeight) now forces a fresh
    // synchronous layout recalculation, because the previous WRITE
    // dirtied the tree again right before it.
  });
}

6. Reads und Writes bündeln: Das FastDOM-Pattern

Die Lösung für Layout Thrashing ist strukturell einfach: Alle Lesezugriffe einer Operation zuerst ausführen, danach alle Schreibzugriffe. Damit entsteht maximal eine einzige synchrone Layout-Berechnung pro Batch, unabhängig davon, wie viele Elemente betroffen sind. Diese Trennung ist als FastDOM-Pattern bekannt geworden, benannt nach der gleichnamigen JavaScript-Bibliothek, die Lese- und Schreib-Callbacks in getrennten Warteschlangen sammelt und sie am nächsten Animation-Frame gebündelt abarbeitet.

Das Prinzip lässt sich ohne externe Bibliothek umsetzen: Statt read-write-read-write in einer Schleife zu mischen, werden zuerst alle benötigten Werte in ein Array geschrieben, danach folgt eine zweite Schleife, die nur noch schreibt. requestAnimationFrame eignet sich als Scheduler, um Schreibzugriffe gezielt in den nächsten Frame zu verschieben und so mit dem natürlichen Rendering-Takt des Browsers zu synchronisieren, statt Layout-Arbeit unkontrolliert über mehrere Event-Handler zu verteilen.


// GOOD: separate the read phase from the write phase completely
function resizeCardsToTallest(cards) {
  // READ PHASE: collect all layout values first, one synchronous layout total
  const heights = cards.map((card) => card.offsetHeight);

  // WRITE PHASE: apply all writes only after every read is done
  cards.forEach((card, index) => {
    card.style.height = (heights[index] + 24) + 'px';
  });
}

7. Praxis-Patterns gegen Thrashing in Schleifen

Neben der grundlegenden Read-then-Write-Trennung helfen weitere Patterns, Layout Thrashing systematisch zu vermeiden. Ein zentraler Scheduler, der Mess- und Mutations-Callbacks sammelt und in einem einzigen requestAnimationFrame abarbeitet, verhindert, dass mehrere unabhängige Komponenten unwissentlich abwechselnd lesen und schreiben. Genau dieses Problem tritt in komponentenbasierten Frontends häufig auf: Komponente A liest offsetHeight, Komponente B schreibt style.width, Komponente C liest wieder, ohne dass die drei Komponenten voneinander wissen.

Für Massenänderungen am DOM ist ein DocumentFragment oder das Entfernen eines Elements aus dem Dokument vor mehreren Schreibzugriffen ein bewährtes Pattern, weil ein losgelöster Knoten kein Layout im sichtbaren Baum auslöst. Ebenso hilfreich: Statt einzelner Style-Zuweisungen in einer Schleife eine CSS-Klasse mit allen nötigen Eigenschaften vorab definieren und nur einmal per classList.add() zuweisen, statt mehrere style.xyz-Zugriffe zu kumulieren. Ein zentraler Scheduler nach dem Vorbild aus Abschnitt sechs ist der robusteste Ansatz für größere Anwendungen.


// Minimal FastDOM-style read/write scheduler for a component-based frontend
class DomScheduler {
  constructor() {
    this.reads = [];
    this.writes = [];
    this.scheduled = false;
  }

  measure(fn) {
    this.reads.push(fn);
    this._schedule();
  }

  mutate(fn) {
    this.writes.push(fn);
    this._schedule();
  }

  _schedule() {
    if (this.scheduled) return;
    this.scheduled = true;
    requestAnimationFrame(() => this._flush());
  }

  _flush() {
    const reads = this.reads.splice(0, this.reads.length);
    const writes = this.writes.splice(0, this.writes.length);

    // Run all pending reads first, in a single synchronous layout pass
    reads.forEach((fn) => fn());

    // Then run all pending writes, after every measurement is captured
    writes.forEach((fn) => fn());

    this.scheduled = false;
  }
}

const scheduler = new DomScheduler();
export default scheduler;

8. Diagnose im Chrome DevTools Performance-Panel

Das Chrome DevTools Performance-Panel visualisiert die Rendering-Pipeline im Main-Thread-Flame-Chart. Layout-Arbeit erscheint dort als violetter Balken mit der Beschriftung "Layout", Paint-Arbeit als grüner Balken mit der Beschriftung "Paint". Ein einzelner, breiter violetter Balken am Frame-Ende ist unproblematisch, das ist der erwartete, gebündelte Reflow. Kritisch wird es, wenn viele schmale violette Balken abwechselnd mit Skript-Ausführung auftauchen, ein klares Muster für Forced Synchronous Layout in einer Schleife.

Chrome markiert erzwungene Reflows zusätzlich explizit: Ein kleines rotes Warndreieck über dem Layout-Balken mit dem Tooltip "Forced reflow" oder "Forced synchronous layout is a possible performance bottleneck" weist direkt auf die problematische Stelle im Code hin, inklusive Stacktrace bis zur auslösenden Zeile. Über die Bottom-Up-Ansicht lässt sich anschließend nach "Layout" filtern, um zu sehen, welche Funktion kumulativ am meisten Layout-Zeit verursacht. Die Summary-Leiste am unteren Rand zeigt zusätzlich die Gesamtzeit für Rendering im Verhältnis zu Scripting, Painting und Idle-Zeit über die gesamte Aufzeichnung.

9. Checkliste: Composite-only Properties als Prävention

Die wirksamste Prävention gegen Layout- und Paint-Kosten ist, Animationen von Anfang an auf composite-only Eigenschaften zu beschränken. transform und opacity lassen sich vollständig auf dem Compositor-Thread animieren, ganz ohne Beteiligung des Main Threads, sofern das Element eine eigene Compositor-Ebene erhält, etwa durch will-change: transform oder eine bereits bestehende 3D-Transformation. Bewegungen, die bisher über top/left realisiert wurden, lassen sich fast immer 1:1 durch transform: translate() ersetzen, Größenänderungen über width/height durch transform: scale().

Zusätzlich hilft die CSS-Eigenschaft contain, um einen Teilbaum bewusst vom restlichen Layout-Baum zu isolieren: contain: layout verhindert, dass Layout-Änderungen innerhalb des Containers Geschwisterelemente außerhalb beeinflussen, und begrenzt so den Umfang jeder Neuberechnung. content-visibility: auto geht noch weiter und pausiert Rendering-Arbeit für Inhalte, die aktuell nicht im Viewport sichtbar sind, komplett. Eine kurze Checkliste vor jedem neuen UI-Feature: Welche Eigenschaft wird animiert, löst sie Layout, nur Paint oder nur Composite aus, und lässt sich eine composite-only Alternative verwenden.


/* BAD: animating layout-triggering properties forces reflow every frame */
.card-bad {
  position: relative;
  transition: top 0.3s ease, left 0.3s ease, width 0.3s ease;
}
.card-bad:hover {
  top: -8px;
  left: 4px;
  width: 110%;
}

/* GOOD: transform and opacity are composite-only, no layout, no paint */
.card-good {
  position: relative;
  transition: transform 0.3s ease, opacity 0.3s ease;
  will-change: transform;
}
.card-good:hover {
  transform: translateY(-8px) scale(1.1);
  opacity: 0.95;
}

/* Isolate a subtree so its layout changes never leak into siblings */
.widget-isolated {
  contain: layout paint;
}

<!-- Hyva phtml: batch reads and writes across an Alpine component -->
<div x-data="stickyHeaderHeight()" x-init="init()">
    <header x-ref="header" class="sticky top-0"></header>
</div>

<script>
    function stickyHeaderHeight() {
        return {
            init() {
                // First rAF batches this component's READ with any other
                // component's READ scheduled in the same animation frame
                requestAnimationFrame(() => {
                    const headerHeight = this.$refs.header.offsetHeight; // READ

                    // Second rAF defers the WRITE to the following frame,
                    // so it never interleaves with a READ from this or
                    // another component in the same pass
                    requestAnimationFrame(() => {
                        document.documentElement.style.setProperty(
                            '--header-height',
                            headerHeight + 'px'
                        );
                    });
                });
            }
        };
    }
</script>

Die folgende Übersicht zeigt, welche gängigen Animationsziele über welche Eigenschaft mit welchen Kosten erreicht werden, und welche composite-only Alternative denselben visuellen Effekt ohne Layout- oder Paint-Kosten liefert.

Ziel Layout-auslösend Composite-only Alternative Effekt
Position ändern top / left transform: translate() Nur Compositor-Thread aktiv
Größe ändern width / height transform: scale() Kein Reflow, GPU-beschleunigt
Element verstecken display: none opacity: 0 + visibility Kein Layout-Trigger
Abstand vergrößern margin-top transform: translateY() Keine Neuberechnung der Geschwister
Offscreen-Inhalt rendern normales Rendering aller Knoten content-visibility: auto Layout- und Paint-Kosten pausiert

In der Praxis lohnt sich der Wechsel zu composite-only Eigenschaften besonders bei Animationen, die häufig ausgelöst werden, etwa Hover-Effekte auf Produktkarten in einer langen Liste oder Scroll-gebundene Sticky-Elemente. Der Unterschied zwischen einem top-Übergang und einem transform-Übergang ist auf einem einzelnen Element kaum spürbar, summiert sich aber bei hunderten gleichzeitig animierten Elementen zu einem klar messbaren Unterschied in der Frame-Rate.

Mironsoft

Rendering-Performance, Main-Thread-Analyse und Hyvä-Optimierung für Magento-Shops

Layout Thrashing im eigenen Shop aufspüren?

Wir analysieren die Rendering-Pipeline eures Frontends im DevTools Performance-Panel, finden Forced-Synchronous-Layout-Stellen und batchen DOM-Zugriffe nach dem FastDOM-Pattern, für flüssige Interaktionen bei 60fps.

Rendering-Audit

Performance-Panel-Aufzeichnung, Identifikation von Forced-Reflow-Stellen im Code

DOM-Batching

Read/Write-Scheduler nach FastDOM-Vorbild in Alpine.js-Komponenten integrieren

Animations-Refactoring

Layout-auslösende CSS-Eigenschaften durch composite-only Alternativen ersetzen

10. Zusammenfassung

Die Vermeidung von Reflow, Repaint und Layout Thrashing löst ein zentrales Interaktivitätsproblem: Der Main Thread hat pro Frame nur 16,66 Millisekunden Zeit für Skript, Layout, Paint und Composite zusammen. Layout ist die teuerste Phase, weil sie durch den DOM-Baum propagiert. Reflow wird durch geometrieverändernde Schreibzugriffe und durch Lesezugriffe wie offsetHeight oder getBoundingClientRect() ausgelöst, Repaint durch rein optische Änderungen wie color oder box-shadow. Werden Lese- und Schreibzugriffe in einer Schleife gemischt, entsteht Forced Synchronous Layout, auch Layout Thrashing genannt, mit hunderten Millisekunden Main-Thread-Blockade als Folge.

Die Lösung liegt in konsequenter Trennung: Erst alle Reads, dann alle Writes, idealerweise über einen zentralen Scheduler nach dem FastDOM-Vorbild mit requestAnimationFrame. Für Animationen sind transform und opacity die composite-only Eigenschaften der Wahl, weil sie komplett ohne Main-Thread-Beteiligung auf dem Compositor laufen. Das Chrome DevTools Performance-Panel macht mit violetten Layout-Balken und dem "Forced reflow"-Warndreieck genau sichtbar, wo im Code die Trennung fehlt.

Reflow, Repaint und Layout Thrashing vermeiden - Das Wichtigste auf einen Blick

Kostenhierarchie

Layout > Paint > Composite. Layout propagiert durch den DOM-Baum, Composite läuft oft komplett auf dem GPU-Thread.

Forced Synchronous Layout

Read nach Write in einer Schleife erzwingt sofortige, synchrone Neuberechnung statt einer gebündelten am Frame-Ende.

FastDOM-Pattern

Alle Reads zuerst sammeln, dann alle Writes ausführen. Ein zentraler Scheduler mit requestAnimationFrame bündelt beides.

Composite-only

transform und opacity statt top/left/width/height für flüssige Animationen ohne Layout-Kosten.

11. FAQ: Reflow, Repaint und Layout Thrashing

1Was ist der Unterschied zwischen Reflow und Repaint?
Reflow berechnet die Geometrie neu und kann Geschwister- und Elternelemente betreffen. Repaint zeichnet nur das Erscheinungsbild neu, ohne die teure Layout-Phase zu durchlaufen.
2Was ist Layout Thrashing?
Abwechselndes Schreiben und Lesen von Layout-Werten in einer Schleife, das jedes Mal eine sofortige, synchrone Neuberechnung statt einer gebündelten erzwingt.
3Welche DOM-Eigenschaften lösen einen Reflow aus?
offsetHeight, offsetWidth, clientHeight, scrollTop, getBoundingClientRect() und getComputedStyle() bei pending Layout, sowie Schreibzugriffe auf width, height, margin oder position.
4Was ist Forced Synchronous Layout genau?
Ein Lesezugriff bei ausstehenden Layout-Änderungen erzwingt eine sofortige synchrone Neuberechnung, statt sie wie üblich bis zum Frame-Ende aufzuschieben.
5Wie hilft das FastDOM-Pattern gegen Layout Thrashing?
Reads und Writes werden in zwei getrennten Warteschlangen gesammelt und gebündelt in dieser Reihenfolge abgearbeitet, meist in einem requestAnimationFrame.
6Warum sind transform und opacity schneller als top/left?
Sie laufen komplett auf dem Compositor-Thread, sofern eine eigene Ebene existiert, etwa via will-change. top/left erzwingen bei jeder Änderung Layout auf dem Main Thread.
7Wie erkenne ich Layout Thrashing im Performance-Panel?
Viele schmale violette Layout-Balken abwechselnd mit Skript-Ausführung statt eines einzigen breiten Balkens am Frame-Ende sind das typische Muster.
8Was bedeutet die Warnung Forced reflow?
Ein rotes Warndreieck markiert eine Stelle, an der ein Lesezugriff eine sofortige synchrone Layout-Berechnung erzwungen hat, inklusive Stacktrace zur auslösenden Zeile.
9Was ist der Compositor-Thread?
Ein separater Thread, der gerasterte Layer zum finalen Bild kombiniert. Composite-only Animationen bleiben flüssig, auch wenn der Main Thread beschäftigt ist.
10Wie viel Zeit-Budget hat ein Frame bei 60fps?
16,66 Millisekunden insgesamt, geteilt zwischen JavaScript, Style, Layout, Paint und Composite. Layout und Paint sollten nur einen kleinen Anteil davon beanspruchen.