W3docs

Performance-Optimierung in der Webentwicklung

Performance-Optimierung ist entscheidend in der Webentwicklung. Dieser Leitfaden behandelt Techniken zur Optimierung des DOM für schnelle und reaktionsschnelle Webanwendungen.

Das DOM ist die mit Abstand teuerste Ressource, die die meisten JavaScript-Anwendungen nutzen. Das Lesen einer Eigenschaft wie offsetHeight kann den Browser zwingen, anzuhalten und die Seitengeometrie neu zu berechnen. Das Schreiben in den DOM kann einen Reflow (Neuberechnung von Elementpositionen und -größen) und einen Repaint (Neuzeichnen von Pixeln) auslösen. Wer das sorglos in einer Schleife tut, bringt eine Seite, die sich sofort anfühlen sollte, ins Stottern.

Dieser Leitfaden erklärt, warum DOM-Arbeit langsam ist, und zeigt konkrete Techniken zur Behebung: Zugriffe minimieren, Layout Thrashing vermeiden, Änderungen mit requestAnimationFrame und document.createDocumentFragment() bündeln und profilieren, was sich nicht erraten lässt.

Warum DOM-Operationen langsam sind

JavaScript läuft in einer schnellen Engine, aber der DOM ist die Grenze zwischen dieser Engine und der Rendering-Pipeline des Browsers. Das wiederholte Überschreiten dieser Grenze ist das Problem:

  • Reflow (Layout) — der Browser berechnet Position und Größe von Elementen neu. Ein Reflow eines Elements kann sich auf seine Vorfahren, Nachkommen und Geschwister ausbreiten.
  • Repaint — der Browser zeichnet Pixel neu (Farben, Schatten, Sichtbarkeit), ohne die Geometrie zu verändern. Günstiger als ein Reflow, aber dennoch nicht kostenlos.

Die entscheidende Erkenntnis: Der Browser versucht, diese Vorgänge für Sie zu bündeln. Er stellt Ihre Schreibvorgänge in eine Warteschlange und verarbeitet sie einmalig, kurz bevor der nächste Frame gerendert wird. Diese Optimierung wird unterbrochen, sobald Sie eine Layout-Eigenschaft lesen, da der Browser alle ausstehenden Schreibvorgänge sofort verarbeiten muss, um eine korrekte Antwort zu liefern. Dieser erzwungene Flush wird als synchrones (erzwungenes) Layout bezeichnet, und ihn in einer Schleife zu provozieren ist die Wurzel der meisten DOM-Performance-Probleme.

DOM-Zugriffe minimieren

Jedes Lesen und Schreiben einer Eigenschaft überschreitet die JS-zu-DOM-Grenze. Die günstigste Optimierung ist daher, weniger davon zu tun.

  • Elementreferenzen zwischenspeichern. Ein Element einmal nachschlagen und in einer Variable speichern, anstatt bei jeder Iteration document.querySelector aufzurufen.
  • In lokale Variablen einlesen. Schleifenzähler, Längen und berechnete Werte gehören in JavaScript-Variablen, nicht bei jedem Durchlauf neu aus dem DOM gelesen.
  • Strings statt Knoten bauen, wenn sinnvoll. innerHTML einmalig zuzuweisen ist oft schneller als viele Knoten einzeln einzufügen — allerdings gehen dabei Event-Listener verloren, und bei nicht vertrauenswürdigen Eingaben ist es unsicher.
// Slow: re-queries and re-reads the DOM on every iteration
for (let i = 0; i < items.length; i++) {
  document.getElementById('list').appendChild(makeRow(items[i]));
}

// Fast: resolve the reference once, outside the loop
const list = document.getElementById('list');
for (let i = 0; i < items.length; i++) {
  list.appendChild(makeRow(items[i]));
}

Unter Selecting DOM Elements finden Sie Hinweise zur Wahl schneller, spezifischer Selektoren — bevorzugen Sie getElementById und gezieltes querySelector gegenüber tiefen Nachfahrenketten wie div > ul li span.

Effiziente Ereignisbehandlung

Einem langen Listenelemente jeweils einen eigenen Listener zuzuweisen, verschwendet Speicher und verlangsamt DOM-Aktualisierungen. Event Delegation hängt einen einzigen Listener an ein gemeinsames Elternelement und nutzt event.target, um festzustellen, welches Kind getroffen wurde — basierend auf Event Bubbling.

// One listener handles the whole list, including rows added later
document.getElementById('list').addEventListener('click', (event) => {
  const row = event.target.closest('li');
  if (row) console.log('clicked row:', row.dataset.id);
});

Dies skaliert auf Tausende von Elementen und erfasst automatisch Elemente, die nach dem Einrichten des Listeners hinzugefügt wurden. Mehr dazu unter Event Handling in the DOM und Introduction to Browser Events.

Layout Thrashing verstehen und vermeiden

Was ist Layout Thrashing?

Layout Thrashing tritt auf, wenn Sie Lesen und Schreiben von Layout-Eigenschaften in schneller Folge abwechseln. Jeder Lesevorgang erzwingt ein synchrones Layout, um den vorherigen Schreibvorgang zu verarbeiten. Eine Schleife aus Lesen-Schreiben-Lesen-Schreiben löst so pro Iteration einen Reflow aus, anstatt insgesamt nur einen.

// Bad: read (offsetWidth) forces layout, then write invalidates it — every loop
const boxes = document.querySelectorAll('.box');
boxes.forEach((box) => {
  box.style.width = box.offsetWidth + 10 + 'px'; // read + write interleaved
});

Die Lösung: Erst alle Lesevorgänge, dann alle Schreibvorgänge

Alle Lesevorgänge zuerst gruppieren, dann alle Schreibvorgänge ausführen. Der Browser führt einen Layout-Durchlauf für die Lesevorgänge und einen für die Schreibvorgänge durch.

const boxes = document.querySelectorAll('.box');

// 1. Read phase — collect every measurement first
const widths = [...boxes].map((box) => box.offsetWidth);

// 2. Write phase — now apply all changes; no read interrupts them
boxes.forEach((box, i) => {
  box.style.width = widths[i] + 10 + 'px';
});

Arbeit mit requestAnimationFrame planen

requestAnimationFrame führt den Callback kurz vor dem nächsten Repaint aus — der ideale Zeitpunkt, um DOM-Änderungen zu schreiben. Sie werden in einem einzigen Frame zusammengeführt, anstatt zwischenzeitliche Repaints auszulösen.

const element = document.getElementById('box');

requestAnimationFrame(() => {
  const width = element.offsetWidth; // read
  element.style.width = width + 10 + 'px'; // write, applied in the same frame
});

Halten Sie bei Animationen alle DOM-Schreibvorgänge innerhalb des requestAnimationFrame-Callbacks und lesen Sie niemals Layout-Werte mittendrin.

DOM-Änderungen bündeln

document.createDocumentFragment() verwenden

document.createDocumentFragment() ist ein leichtgewichtiger, nicht im Bildschirm sichtbarer Container. An ein Fragment angehängte Knoten sind nicht Teil des aktiven Dokuments, sodass das Aufbauen keine Reflows auslöst. Wenn das fertige Fragment der Seite hinzugefügt wird, fügt der Browser alle seine Kinder in einer einzigen Operation ein — ein Reflow statt einem pro Knoten.

Beispiel

<!DOCTYPE html>
<html>
<head>
    <title>Batching DOM Changes</title>
</head>
<body>
    <div id="container"></div>

    <script>
        const container = document.getElementById('container');
        const fragment = document.createDocumentFragment();

        for (let i = 0; i < 40; i++) {
            const div = document.createElement('div');
            div.textContent = `Item ${i}`;
            fragment.appendChild(div);
        }

        container.appendChild(fragment); // Batch update
    </script>
</body>
</html>

Die Schleife baut 40 Elemente gegen das Fragment auf, ohne die sichtbare Seite zu beeinflussen. Nur das abschließende container.appendChild(fragment) berührt den aktiven DOM, sodass der Browser einen einzigen Layout-Durchlauf statt 40 ausführt. (Moderne Engines ermöglichen auch das Einfügen eines Arrays von Knoten in einem Aufruf mit container.append(...nodes), was ähnlich gebündelt verarbeitet wird.)

Erzwungenes synchrones Layout vermeiden

Eine subtile Variante von Thrashing ist das Lesen einer Layout-Eigenschaft unmittelbar nach einer Stiländerung, was den Browser zur sofortigen Neuberechnung des Layouts zwingt:

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

box.classList.add('expanded'); // write — queues a layout change
const height = box.offsetHeight; // read — forces layout NOW to answer

Wenn der Wert nicht sofort benötigt wird, den Lesevorgang mit requestAnimationFrame auf den nächsten Frame verschieben oder den Code so umstrukturieren, dass alle Lesevorgänge vor allen Schreibvorgängen erfolgen. Häufige Eigenschaften, die beim Lesen ein erzwungenes Layout auslösen: offsetTop/offsetWidth/offsetHeight, clientWidth/clientHeight, scrollTop und getComputedStyle().

Best Practices

  1. Nicht kritisches JavaScript verzögern. Das Attribut defer zu <script>-Tags hinzufügen, damit der Browser das HTML weiter parst und das Skript erst nach Bereitstellung des DOM ausführt. async für unabhängige Drittanbieter-Skripte verwenden.
  2. Klassen-Toggles statt Inline-Styles bevorzugen. Das Ändern einer CSS-Klasse erlaubt dem Browser, viele Stilregeln in einem Reflow anzuwenden, anstatt einen Reflow pro Inline-style-Zuweisung auszulösen.
  3. transform und opacity animieren. Diese werden von der GPU zusammengesetzt und überspringen Layout und Paint vollständig — anders als das Animieren von width, top oder margin.
  4. Trennen, verändern, wieder einfügen. Bei umfangreichen Bearbeitungen einen Teilbaum aus dem Dokument entfernen (oder mit display: none verbergen), außerhalb des Bildschirms ändern und dann wieder einfügen.
  5. Erst profilieren, dann optimieren. Das Performance-Panel der Browser-DevTools nutzen, um den tatsächlichen Engpass zu finden, anstatt zu raten — siehe DOM Debugging and Tools.
Info

Die goldene Regel: Alle DOM-Lesevorgänge bündeln, dann alle Schreibvorgänge bündeln. Jedes Mal, wenn ein Lesevorgang auf einen Schreibvorgang folgt, wird der Browser gezwungen, das Layout synchron neu zu berechnen. Durch ihre Gruppierung kann er die Arbeit einmal pro Frame erledigen.

Häufige Fallstricke

  • document.querySelector innerhalb einer Schleife aufrufen, anstatt das Ergebnis zwischenzuspeichern.
  • offsetWidth/offsetHeight lesen und Styles in derselben Schleifeniteration schreiben.
  • Für jedes Element einer großen, dynamischen Liste einen eigenen Event-Listener anbringen, anstatt zu delegieren.
  • Layout-auslösende Eigenschaften (width, left, margin) animieren, anstatt transform/opacity zu nutzen.

Fazit

DOM-Performance lässt sich auf eine Idee reduzieren: Der Browser ist schnell darin, seine Rendering-Arbeit zu bündeln — Ihre Aufgabe ist es, dieses Bündeln nicht zu unterbrechen. Referenzen zwischenspeichern, Events delegieren, Lesevorgänge vor Schreibvorgängen gruppieren, große Aktualisierungen innerhalb eines DocumentFragment aufbauen und visuelle Änderungen mit requestAnimationFrame einplanen. Im Zweifelsfall profilieren — das Performance-Panel der DevTools zeigt genau, wo Reflows stattfinden. Vertiefen Sie Ihr Werkzeugset anschließend mit DOM Manipulation und Advanced DOM Techniques.

Übungen

Übung
Welche der folgenden Techniken sind wichtig für die Optimierung der DOM-Performance?
Welche der folgenden Techniken sind wichtig für die Optimierung der DOM-Performance?
Was this page helpful?