ResizeObserver: Elementgrößen reaktiv beobachten in JavaScript
AI generated
JS
() =>
JavaScript · DOM · Responsive Design · Browser-APIs
ResizeObserver
Elementgrößen reaktiv beobachten

Das window-resize-Event war der erste, aber fehlerhafte Ansatz für größenabhängiges JavaScript. ResizeObserver beobachtet einzelne DOM-Elemente direkt – unabhängig davon, ob das Fenster sich ändert, ein Sidebar-Panel sich öffnet oder ein Elternelement dynamisch wächst.

10 Min. Lesezeit ResizeObserver · contentRect · borderBoxSize · devicePixelContentBoxSize Alle modernen Browser

1. Warum window.resize nicht ausreicht

Das window.resize-Event ist ein grundlegendes Missverständnis als Lösung für größenabhängige UI-Logik. Es feuert nur, wenn das Browser-Fenster selbst seine Größe ändert – nicht wenn ein Container durch CSS-Grid-Reflow, das Ein- und Ausblenden eines Sidebars, das Hinzufügen von Kindknoten oder das Ändern von CSS-Variablen seine Größe verändert. Komplexe Layouts mit dynamischen Seitenpanels, kollabierbaren Navigations-Elementen oder JavaScript-gesteuerten Grid-Layouts erfordern eine Lösung, die auf Elementebene reagiert.

ResizeObserver schließt genau diese Lücke. Es ist eine Browser-API, die Größenänderungen eines oder mehrerer DOM-Elemente beobachtet und einen Callback aufruft, wann immer sich die Größe ändert – unabhängig von der Ursache. Das können Fenstergrößenänderungen sein, aber auch CSS-Transitions, DOM-Mutationen, Font-Größenänderungen durch Zoom oder dynamisch injizierte Inhalte. ResizeObserver ist die einzig korrekte Lösung für das Problem „Mein Component muss auf seine eigene Größe reagieren, unabhängig davon, warum sie sich ändert".

2. ResizeObserver – Grundprinzip und Syntax

Die Grundsyntax von ResizeObserver folgt dem Observer-Pattern: Man erstellt eine Observer-Instanz mit einem Callback, ruft observe(element) auf und erhält bei jeder Größenänderung einen Aufruf des Callbacks mit einem Array von ResizeObserverEntry-Objekten. Jeder Entry enthält Informationen über das beobachtete Element und seine neue Größe in verschiedenen Box-Models. Der Callback erhält auch eine Referenz auf den Observer selbst, was In-Callback-Disconnect ermöglicht.

Wichtig: ResizeObserver-Callbacks werden nicht bei jeder Pixel-Änderung synchron aufgerufen. Der Browser batcht mehrere Größenänderungen und liefert sie zusammen in einem Callback-Aufruf. Das ist analog zu MutationObserver und IntersectionObserver: Die Browser-Implementierung bündelt Notifications, um Overhead zu reduzieren. Der Callback läuft nach dem Layout-Schritt und vor dem Paint – das gibt die Möglichkeit, als Reaktion auf eine Größenänderung weitere Layout-Änderungen vorzunehmen, ohne einen zusätzlichen Frame zu verursachen.


// Basic ResizeObserver setup with entry inspection
const container = document.getElementById('responsive-container');

const observer = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const { width, height } = entry.contentRect;

    console.log(`[ResizeObserver] ${entry.target.id}: ${width}x${height}px`);

    // React to size changes — e.g. switch layout modes
    if (width < 600) {
      entry.target.classList.add('compact-layout');
      entry.target.classList.remove('full-layout');
    } else {
      entry.target.classList.add('full-layout');
      entry.target.classList.remove('compact-layout');
    }
  }
});

// Start observing
observer.observe(container);

// Optional: stop observing one element
// observer.unobserve(container);

// Disconnect all observed elements
// observer.disconnect();

3. Die drei Box-Models: contentRect, borderBoxSize, devicePixelContentBoxSize

Ein ResizeObserverEntry liefert Größeninformationen in drei verschiedenen Box-Models. contentRect ist das älteste und am weitesten unterstützte: Es gibt die Größe des Content-Bereichs zurück, ohne Padding, Border und Margin. Für die meisten Layout-Entscheidungen ist das der richtige Wert. borderBoxSize ist ein Array von ResizeObserverSize-Objekten und gibt die Gesamtgröße inklusive Padding und Border zurück – entspricht dem, was offsetWidth/offsetHeight zurückgibt, aber ohne Layout-Thrashing zu erzwingen.

devicePixelContentBoxSize ist das präziseste und speziellste Box-Model: Es gibt die Größe in physischen Gerätepixeln zurück, nicht in CSS-Pixeln. Das ist entscheidend für Canvas-Elemente, die auf High-DPI-Displays (Retina) korrekt skaliert werden müssen. Statt window.devicePixelRatio manuell zu multiplizieren, liefert ResizeObserver mit devicePixelContentBoxSize den exakten Wert direkt. Nicht alle Browser unterstützen alle drei Box-Models gleichzeitig – contentRect ist die sichere Basis, borderBoxSize und devicePixelContentBoxSize sollten mit Existenzprüfung genutzt werden.

4. Mehrere Elemente mit einem Observer beobachten

Ein ResizeObserver kann beliebig viele Elemente beobachten. Das ist effizienter als einen separaten Observer pro Element zu erstellen, weil die Overhead-Kosten für einen Observer fester Natur sind. In einer Komponenten-Architektur mit zehn Chart-Widgets auf einer Dashboard-Seite: Ein einziger ResizeObserver beobachtet alle zehn Container. Im Callback identifiziert man das geänderte Element über entry.target und reagiert spezifisch.

Das Batching-Verhalten kommt hier besonders zum Tragen: Wenn der Nutzer das Fenster auf einem breiten Monitor sehr schnell zieht, können mehrere Größenänderungen für dasselbe Element innerhalb kurzer Zeit entstehen. ResizeObserver liefert nicht jeden Zwischenschritt, sondern nur den letzten Stand pro Element im aktuellen Batch. Das reduziert die Callback-Aufrufe erheblich und verbessert die Performance im Vergleich zu einem window-resize-Handler, der bei jedem Pixel feuert und durch Debouncing manuell gedrosselt werden muss.


// Observing multiple elements with one ResizeObserver instance
// Map to store per-element state (avoid closure complexity)
const elementStates = new WeakMap();

const sharedObserver = new ResizeObserver((entries) => {
  for (const entry of entries) {
    const state = elementStates.get(entry.target);
    if (!state) continue;

    // Use borderBoxSize if available, fall back to contentRect
    let width, height;
    if (entry.borderBoxSize && entry.borderBoxSize.length > 0) {
      width = entry.borderBoxSize[0].inlineSize;
      height = entry.borderBoxSize[0].blockSize;
    } else {
      width = entry.contentRect.width;
      height = entry.contentRect.height;
    }

    // Call element-specific resize handler stored in state
    state.onResize({ width, height, target: entry.target });
  }
});

// Register a chart container to be observed
function observeContainer(element, onResize) {
  elementStates.set(element, { onResize });
  sharedObserver.observe(element);
}

function unobserveContainer(element) {
  elementStates.delete(element);
  sharedObserver.unobserve(element);
}

5. Den Observer-Resize-Loop verstehen und vermeiden

Der ResizeObserver-Resize-Loop ist der häufigste Fehler beim Einsatz dieser API. Er entsteht, wenn der Resize-Callback eine Aktion ausführt, die selbst eine Größenänderung des beobachteten Elements auslöst. Beispiel: Der Callback legt Text in ein Element, das dadurch höher wird, was den Callback erneut aufruft, was mehr Text hinzufügt, was das Element weiter wachsen lässt. Der Browser erkennt solche Loops und wirft eine ResizeObserver loop completed with undelivered notifications-Warnung in der Konsole.

Die Lösung hängt vom Anwendungsfall ab. In vielen Fällen reicht es, die Logik mit einem Guard zu versehen: Wenn die neue Größe sich nicht wesentlich von der letzten bekannten Größe unterscheidet, keine Aktion ausführen. Für Fälle, wo der Callback tatsächlich Größenänderungen auslösen muss, gibt es den Trick, die Größenänderung an ein Element vorzunehmen, das im DOM tiefer liegt als das beobachtete Element. ResizeObserver erlaubt Größenänderungen von Nachfolgern, aber nicht von Vorfahren oder dem Element selbst – das ist die Grundregel des Loop-Prevention-Mechanismus des Browsers.

6. unobserve und disconnect – sauber aufräumen

Wie alle Observer-Patterns muss ResizeObserver sauber aufgeräumt werden, wenn beobachtete Elemente aus dem DOM entfernt werden. observer.unobserve(element) entfernt ein spezifisches Element aus der Beobachtungsliste, lässt den Observer aber für andere Elemente aktiv. observer.disconnect() stoppt die Beobachtung aller Elemente und gibt die Observer-Ressourcen frei. Letzteres ist der richtige Aufruf im Cleanup-Lifecycle einer Komponente.

Ein subtiles Problem: Wenn ein beobachtetes Element aus dem DOM entfernt wird, ohne unobserve aufzurufen, hält der Observer eine Referenz auf das Element. Das Element selbst wird garbage-collected, wenn keine anderen Referenzen bestehen – ResizeObserver nutzt intern schwache Referenzen, um das zu ermöglichen. Trotzdem ist explizites Cleanup die beste Praxis: Es macht den Code nachvollziehbar, vermeidet Debugging-Rätsel und signalisiert die Absicht des Entwicklers klar. In React: cleanup im useEffect-Return. In Web Components: disconnectedCallback.

7. ResizeObserver in React: useResizeObserver Hook

Die Integration von ResizeObserver in React folgt dem üblichen Hook-Pattern. Ein useResizeObserver-Hook erstellt eine Observer-Instanz, verknüpft sie mit einem Element-Ref und gibt die aktuelle Größe als State zurück. Wichtig: Die Observer-Instanz sollte nicht bei jedem Render neu erstellt werden. Sie wird einmalig im useEffect erzeugt und im Cleanup-Return disconnected. Der Callback aktualisiert den React-State über setState, was den Component neu rendert.

Ein Performance-Aspekt: Wenn der ResizeObserver-Callback setState aufruft, rendert React die Komponente neu. Das ist bei jedem Resize-Event der Fall. Für Chart-Komponenten, die bei jedem Resize ihre Canvas-Größe neu berechnen, ist das korrekt. Für Fälle, wo der State nur selten gebraucht wird, kann man die Observer-Größe in einem useRef speichern (kein Re-render) und nur für spezifische Berechnungen lesen. Die Wahl zwischen Ref und State hängt davon ab, ob die Größenänderung einen Re-render auslösen soll.


// useResizeObserver React hook — clean lifecycle management
import { useEffect, useRef, useState } from 'react';

function useResizeObserver(ref) {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const element = ref.current;
    if (!element) return;

    const observer = new ResizeObserver((entries) => {
      // Entries is an array — we only observe one element here
      const entry = entries[0];
      if (!entry) return;

      const { width, height } = entry.contentRect;
      setSize({ width, height });
    });

    observer.observe(element);

    // Cleanup: disconnect observer when component unmounts
    return () => observer.disconnect();
  }, [ref]);

  return size;
}

// Usage in a Chart component
function AdaptiveChart({ data }) {
  const containerRef = useRef(null);
  const { width, height } = useResizeObserver(containerRef);

  return (
    <div ref={containerRef} style={ { width: '100%' } }>
      <canvas
        width={width}
        height={height || 300}
        data-chart-width={width}
      />
    </div>
  );
}

8. ResizeObserver vs. window.resize vs. matchMedia vs. CSS Container Queries

Die vier Ansätze für größenabhängige Layouts haben klar unterschiedliche Stärken. window.resize ist die älteste Lösung und nur dann korrekt, wenn tatsächlich auf Fenstergrößenänderungen reagiert werden soll – nicht auf Element-Größen. matchMedia ist für Breakpoint-basierte Logik in JavaScript ideal, da es Callbacks nur bei Breakpoint-Übergängen aufruft, nicht bei jedem Pixel. ResizeObserver ist die einzige Lösung, die auf individuelle Elementgrößen reagiert, unabhängig von der Ursache der Änderung.

CSS Container Queries sind seit 2023 in allen modernen Browsern verfügbar und erledigen für viele Anwendungsfälle, was früher JavaScript benötigte: Sie ermöglichen es, CSS-Regeln basierend auf der Größe eines Container-Elements anzuwenden. Für rein visuelle Anpassungen (Spaltenanzahl, Schriftgröße, Abstände) sind CSS Container Queries die überlegene Lösung – kein JavaScript, kein Observer, kein Layout-Thrashing. ResizeObserver bleibt unverzichtbar, wenn JavaScript tatsächlich auf die Größe reagieren muss: Canvas-Skalierung, D3-Chart-Neuberechnungen, dynamische Daten-Layouts.

Methode Reagiert auf Granularität Ideal für
ResizeObserver Elementgröße (jede Ursache) Per Element Canvas, Charts, adaptive Components
window.resize Fenstergrößenänderung Global Viewport-abhängige Berechnungen
matchMedia Breakpoint-Übergänge Breakpoints JS-Breakpoint-Logik ohne Pixel-Spam
CSS Container Queries Container-Breite (CSS only) Per Container Rein visuelle Anpassungen ohne JS

9. Praxisbeispiel: Adaptiver Chart mit automatischer Neuberechnung

Charts auf Basis von Canvas oder SVG müssen bei Größenänderungen des Containers neu gezeichnet werden. Das ist der klassische Einsatzfall für ResizeObserver: Der Chart-Container wird beobachtet, bei jeder Größenänderung werden Canvas-Attribute und Chart-Skalen neu berechnet, und der Chart wird ohne sichtbares Flackern neu gerendert. Ohne ResizeObserver würde man entweder auf window.resize setzen (falsch: reagiert nicht auf Panel-Änderungen) oder Dimensionen per offsetWidth im Animation-Frame pollen (sehr ineffizient).

Ein wichtiger Detail: Das Canvas-Element muss sowohl das HTML-Attribut width/height als auch das CSS-Attribut width: 100% korrekt gesetzt haben. Das HTML-Attribut bestimmt die interne Auflösung des Canvas-Buffers, das CSS-Attribut die angezeigte Größe im Dokument. Für Retina-Displays muss der ResizeObserver-Wert mit devicePixelContentBoxSize oder window.devicePixelRatio multipliziert werden, um scharfe Ausgabe zu erhalten. Der ResizeObserver-Callback bekommt mit devicePixelContentBoxSize diesen Wert ohne weitere Berechnung.


// Adaptive canvas chart with ResizeObserver and DPR scaling
class AdaptiveChart {
  constructor(container) {
    this.container = container;
    this.canvas = container.querySelector('canvas');
    this.ctx = this.canvas.getContext('2d');
    this.data = [];

    this.observer = new ResizeObserver((entries) => {
      const entry = entries[0];

      // Use devicePixelContentBoxSize for sharp rendering on HiDPI
      let physicalWidth, physicalHeight;
      if (entry.devicePixelContentBoxSize) {
        physicalWidth = entry.devicePixelContentBoxSize[0].inlineSize;
        physicalHeight = entry.devicePixelContentBoxSize[0].blockSize;
      } else {
        // Fallback: manual DPR calculation
        const dpr = window.devicePixelRatio || 1;
        physicalWidth = Math.round(entry.contentRect.width * dpr);
        physicalHeight = Math.round(entry.contentRect.height * dpr);
      }

      // Set canvas buffer size to physical pixels
      this.canvas.width = physicalWidth;
      this.canvas.height = physicalHeight;
      // CSS size via contentRect (CSS pixels)
      this.canvas.style.width = `${entry.contentRect.width}px`;
      this.canvas.style.height = `${entry.contentRect.height}px`;

      this.render(); // Redraw after resize
    });

    this.observer.observe(container);
  }

  render() {
    const { width, height } = this.canvas;
    this.ctx.clearRect(0, 0, width, height);
    // ... draw chart scaled to width/height
  }

  destroy() {
    this.observer.disconnect();
  }
}

10. Zusammenfassung

ResizeObserver ist die moderne, korrekte Lösung für alle Szenarien, in denen JavaScript auf Elementgrößenänderungen reagieren muss. Es ersetzt fragile window-resize-Event-Handler mit Debouncing und offsetWidth-Polling durch eine Browser-native Observer-API mit Batching und drei Box-Models. Die drei Box-Models decken alle Anwendungsfälle ab: contentRect für Layout-Entscheidungen, borderBoxSize für Gesamtgrößen, devicePixelContentBoxSize für HiDPI-Canvas-Rendering.

Die wichtigsten Regeln: Observer und beobachtete Elemente immer sauber trennen – unobserve oder disconnect im Cleanup-Lifecycle aufrufen. Den Observer-Resize-Loop durch Guards oder durch das Prinzip „nur Nachfolger-Elemente ändern" vermeiden. Für rein visuelle Anpassungen CSS Container Queries bevorzugen. ResizeObserver für Fälle reservieren, wo JavaScript tatsächlich auf die Größe reagieren muss: Canvas-Skalierung, programmatische Layout-Berechnungen, adaptive Datenvisualisierungen.

Mironsoft

JavaScript DOM-APIs, Responsive Architekturen und Canvas-Performance

Layouts, die auf jede Größenänderung reagieren?

Wir analysieren eure Layout-Logik, ersetzen window-resize-Hacks durch saubere ResizeObserver-Implementierungen und integrieren adaptive Chart- und Canvas-Komponenten in eure React- oder Web-Component-Architektur.

Observer-Audit

window-resize-Handler und offsetWidth-Polling durch ResizeObserver ersetzen

Canvas & HiDPI

devicePixelContentBoxSize-Integration für scharfe Retina-Canvas-Ausgabe

React Hooks

Saubere useResizeObserver-Hooks mit korrektem Lifecycle-Management

ResizeObserver — Das Wichtigste auf einen Blick

Element-Level Beobachtung

Reagiert auf Größenänderungen einzelner Elemente, unabhängig von der Ursache. Korrekte Lösung für dynamische Layouts mit Sidebars, Panels und flexiblen Containern.

Drei Box-Models

contentRect für Layout-Entscheidungen. borderBoxSize für Gesamtgrößen. devicePixelContentBoxSize für HiDPI-Canvas-Rendering ohne manuelle DPR-Multiplikation.

Loop-Prevention

Callback darf keine Größenänderung am beobachteten Element oder seinen Vorfahren auslösen. Nur Nachfolger-Elemente ändern. Guard-Checks für geringfügige Änderungen verwenden.

Sauberes Cleanup

observer.unobserve(el) für einzelne Elemente. observer.disconnect() im Unmount/disconnectedCallback. WeakMap für per-Element-State – kein Speicherleck bei DOM-Entfernung.

11. FAQ: ResizeObserver

1Was ist ResizeObserver?
Browser-API für Größenänderungen einzelner DOM-Elemente – unabhängig von der Ursache. Korrekte Alternative zu window-resize für Element-Level-Reaktionen.
2contentRect vs. borderBoxSize?
contentRect: Content ohne Padding/Border. borderBoxSize: Gesamtgröße mit Padding/Border – entspricht offsetWidth ohne Layout-Thrashing.
3devicePixelContentBoxSize wofür?
Canvas-Größe in physischen Pixels für HiDPI-Displays (Retina). Kein manuelles DPR-Multiplizieren nötig.
4Was ist der Observer-Resize-Loop?
Callback löst Größenänderung am beobachteten Element aus → erneuter Callback → Endlosschleife. Nur Nachfolger-Elemente im Callback ändern.
5Mehrere Elemente mit einem Observer?
Ja – observe() mehrfach aufrufen. entry.target identifiziert das geänderte Element. Effizienter als separate Observer pro Element.
6Sauberes Aufräumen?
unobserve(el) für einzelne Elemente. disconnect() im Unmount/disconnectedCallback. Verhindert Manipulation entfernter DOM-Elemente.
7CSS Container Queries besser?
Für rein visuelle Anpassungen: ja. ResizeObserver nur wenn JavaScript tatsächlich auf die Größe reagieren muss.
8Feuert bei jedem Pixel?
Nein – Browser batcht Änderungen. Callback erhält immer aktuellsten Stand, nicht jeden Zwischenschritt. Automatisches Throttling.
9Integration in React?
useEffect: Observer erstellen, ref.current beobachten, Größe via setState. Cleanup-Return: observer.disconnect(). Handle in useRef statt State.
10WeakMap für per-Element-State?
WeakMap hält schwache Referenzen – entferntes Element wird ohne manuelles Löschen garbage-collected. Kein Speicherleck bei DOM-Mutation.