JS
() =>
JavaScript · Browser-APIs · Performance · Animation
requestAnimationFrame meistern
flüssige Animationen ohne Jank

requestAnimationFrame ist die einzige korrekte Methode, um Animationen im Browser zu implementieren. Wer setTimeout oder setInterval nutzt, kämpft gegen den Rendering-Zyklus statt mit ihm. Dieser Artikel zeigt, wie rAF funktioniert, wie man einen robusten Render-Loop baut und welche Fallen den Frame-Rhythmus zerstören.

10 Min. Lesezeit rAF · Render-Loop · Frame-Budget · cancelAnimationFrame Alle modernen Browser

1. Warum requestAnimationFrame statt setTimeout?

Die intuitive Antwort auf die Frage „Wie animiere ich etwas in JavaScript?" ist für viele Entwickler: setInterval(update, 16) – denn 1000 ms / 60 fps ≈ 16 ms. Diese Rechnung klingt plausibel, ist aber fundamental falsch. requestAnimationFrame synchronisiert die Callback-Ausführung mit dem Refresh-Zyklus des Displays, während setInterval völlig blind gegenüber dem Browser-Renderer operiert. Das Ergebnis bei setInterval: Der Browser rendert einen Frame, während JavaScript gerade eine DOM-Mutation berechnet – das führt zu unvollständigen Frames, die als Jank (Ruckeln) sichtbar werden.

Ein weiteres kritisches Problem: setInterval läuft weiter, wenn der Tab im Hintergrund ist. Das verschwendet CPU-Ressourcen und Akku bei Mobilgeräten. requestAnimationFrame pausiert automatisch, wenn der Tab nicht sichtbar ist, und nimmt den Rhythmus beim Wiederherstellen der Sichtbarkeit nahtlos auf. Browser können rAF außerdem auf die native Display-Frequenz anpassen – 60 Hz, 90 Hz oder 120 Hz – ohne dass der Entwickler irgendetwas anpassen muss. Diese drei Vorteile zusammen machen requestAnimationFrame zur einzig vertretbaren Wahl für JavaScript-Animationen.

2. Die Browser-Rendering-Pipeline verstehen

Um requestAnimationFrame richtig einzusetzen, muss man verstehen, wo es in der Browser-Rendering-Pipeline sitzt. Ein Frame beginnt mit Input-Events (Maus, Touch, Tastatur), danach werden rAF-Callbacks ausgeführt. Dann folgt Style-Recalculation, Layout, Paint und Composite. requestAnimationFrame-Callbacks laufen also bewusst vor dem Render-Schritt – das gibt dem Entwickler die Möglichkeit, DOM und CSSOM zu verändern, bevor der Browser den Frame berechnet. Eine Änderung in einem rAF-Callback sieht der Nutzer garantiert im nächsten Frame.

Die Faustregel für das 16-ms-Frame-Budget bei 60 Hz: rAF-Callback plus alle daraus resultierenden Style/Layout-Berechnungen müssen zusammen unter 10 ms bleiben, um dem Browser genug Zeit für Paint und Composite zu lassen. Überschreitet man das Budget, wird ein Frame übersprungen – das ist der „Dropped Frame", der als Ruckeln wahrnehmbar ist. requestAnimationFrame gibt keine Garantie, dass jeder Callback in jedem Frame ausgeführt wird: Bei zu langer Ausführungszeit priorisiert der Browser den Render-Schritt und verschiebt den nächsten rAF-Callback.

3. Den ersten Render-Loop schreiben

Das grundlegende Muster für einen requestAnimationFrame-Loop besteht aus einer Funktion, die sich am Ende selbst wieder per requestAnimationFrame registriert. Diese rekursive Struktur ist keine echte Rekursion – jeder Aufruf kehrt sofort zurück und der Browser ruft den Callback zum nächsten Frame-Zeitpunkt auf. Das Muster ist bewusst einfach gehalten, damit der Browser den Callback zu einem für ihn optimalen Zeitpunkt einplanen kann.

Wichtig ist, den Loop-State in einer äußeren Variablen zu halten, nicht innerhalb des Callbacks. Animationseigenschaften wie aktuelle Position, Geschwindigkeit und Richtung leben im Closure-Scope. Der Callback selbst berechnet den neuen Zustand basierend auf dem vergangenen Delta und schreibt das Ergebnis in den DOM. Das Lesen von DOM-Properties und das Schreiben in den DOM sind getrennte Operationen – niemals abwechselnd lesen und schreiben innerhalb desselben Callbacks, sonst provoziert man Layout-Thrashing.


// Basic requestAnimationFrame render loop with state management
let rafId = null;

const state = {
  x: 0,
  velocity: 2,       // pixels per frame (60 fps baseline)
  maxX: 800,
};

function update(timestamp) {
  // Move element, reverse direction at boundaries
  state.x += state.velocity;
  if (state.x >= state.maxX || state.x <= 0) {
    state.velocity *= -1;
  }

  // Write to DOM — only after all reads are done
  element.style.transform = `translateX(${state.x}px)`;

  // Schedule next frame — loop continues until cancelled
  rafId = requestAnimationFrame(update);
}

// Start loop
rafId = requestAnimationFrame(update);

// Stop loop (e.g. on component unmount or user pause)
function stopLoop() {
  if (rafId !== null) {
    cancelAnimationFrame(rafId);
    rafId = null;
  }
}

4. Der DOMHighResTimeStamp-Parameter

Jeder requestAnimationFrame-Callback erhält als erstes Argument einen DOMHighResTimeStamp – eine hochauflösende Zeitmarke in Millisekunden relativ zum Seitenaufruf. Diese Zeitmarke ist identisch für alle rAF-Callbacks, die im selben Frame-Flush ausgeführt werden. Das bedeutet: Mehrere unabhängige requestAnimationFrame-Loops, die im selben Frame aktiv sind, erhalten denselben Timestamp. Das ermöglicht präzise Synchronisierung zwischen verschiedenen Animationskomponenten ohne explizite Kommunikation.

Der entscheidende Vorteil gegenüber Date.now() liegt in der Genauigkeit und in der Entkopplung von der Systemuhr. Der Timestamp ist garantiert monoton steigend und wird nicht durch Systemuhr-Anpassungen beeinflusst. Für frame-rate-unabhängige Animationen berechnet man das Delta zum vorherigen Frame (delta = timestamp - lastTimestamp) und multipliziert die Bewegung mit diesem Delta. So läuft die Animation auf einem 120-Hz-Display doppelt so oft, aber in halber Schrittweite – das Ergebnis ist dieselbe wahrnehmbare Geschwindigkeit auf allen Display-Frequenzen.

5. cancelAnimationFrame und sauberes Aufräumen

requestAnimationFrame gibt einen numerischen Handle zurück, der für cancelAnimationFrame(handle) benötigt wird. Wer diesen Handle nicht speichert, kann den Loop nicht von außen stoppen. Das ist das häufigste Memory-Leak-Muster bei rAF-basierten Animationen: Eine React-Komponente oder ein Web-Component wird entfernt, der Loop läuft aber weiter, weil der Handle nicht in der Cleanup-Logik gespeichert wurde.

Das sichere Muster: Handle immer in einer Variable außerhalb des Callbacks speichern. In React-Komponenten im useEffect-Cleanup aufräumen. In Web-Components im disconnectedCallback. Bei Page-Visibility-Changes (document.addEventListener('visibilitychange', ...)) den Loop pausieren, wenn der Tab unsichtbar wird – requestAnimationFrame tut das bereits automatisch auf System-Ebene, aber explizites Pausieren verhindert auch unnötige State-Berechnungen im JavaScript selbst.

6. Frame-Budget und Layout-Thrashing vermeiden

Layout-Thrashing ist der häufigste Performance-Killer in requestAnimationFrame-Loops. Es entsteht, wenn JavaScript abwechselnd DOM-Eigenschaften liest und schreibt: Der Browser muss nach jeder Schreiboperation das Layout neu berechnen, bevor er eine Leseoperation zurückgeben kann. Eine Schleife, die für zehn Elemente jeweils el.offsetWidth liest und dann el.style.width setzt, erzwingt zehn Layout-Recalculations in einem einzigen Frame – statt einer einzigen am Ende.

Die Lösung folgt dem Batch-Muster: zuerst alle Reads, dann alle Writes. In komplexen Animations-Szenarien hilft FastDOM oder eine manuelle Read/Write-Queue. Für rein visuelle Transformationen ist transform und opacity die erste Wahl, weil beide Eigenschaften auf dem Compositor-Thread laufen und kein Layout auslösen. Änderungen an width, height, top oder left hingegen lösen immer Layout aus und sollten in Animations-Loops vermieden werden.


// Frame-rate-independent animation using DOMHighResTimeStamp delta
let lastTimestamp = null;
let rafId = null;
let posX = 0;
const SPEED_PX_PER_MS = 0.3; // 300 px/s — consistent across 60/120 Hz

function animate(timestamp) {
  if (lastTimestamp === null) {
    lastTimestamp = timestamp; // First frame: initialize
  }

  const delta = timestamp - lastTimestamp; // ms since last frame
  lastTimestamp = timestamp;

  // Cap delta to avoid huge jumps after tab was hidden
  const safeDelta = Math.min(delta, 100);
  posX = (posX + SPEED_PX_PER_MS * safeDelta) % 800;

  // GOOD: use transform — no layout, runs on compositor thread
  element.style.transform = `translateX(${posX.toFixed(2)}px)`;

  rafId = requestAnimationFrame(animate);
}

// Pause when tab is hidden — saves CPU
document.addEventListener('visibilitychange', () => {
  if (document.hidden) {
    cancelAnimationFrame(rafId);
    rafId = null;
    lastTimestamp = null; // Reset so delta is clean on resume
  } else {
    rafId = requestAnimationFrame(animate);
  }
});

rafId = requestAnimationFrame(animate);

7. Frame-Throttling für niedrig-frequente Updates

Nicht jede Aufgabe braucht 60 Updates pro Sekunde. Ein Scroll-Fortschrittsbalken, eine Canvas-Visualisierung von Echtzeitdaten oder ein Audio-Visualizer können oft mit 30 fps oder weniger betrieben werden. Frame-Throttling mit requestAnimationFrame ist die saubere Lösung: Der Loop läuft weiterhin mit nativer Frame-Rate, aber der tatsächliche Update-Code wird nur ausgeführt, wenn das akkumulierte Delta den gewünschten Interval-Wert überschreitet.

Dieses Muster ist dem naive Ansatz mit setInterval überlegen, weil der rAF-Loop weiterhin mit dem Browser-Rendering-Zyklus synchronisiert ist. Wenn ein Update übersprungen wird, weil das Delta noch nicht ausreicht, kehrt der Callback sofort zurück – ohne DOM-Schreiboperationen, ohne Layout-Berechnung. Das ist deutlich günstiger als ein setInterval, das unabhängig vom Rendering-Zyklus feuert und dessen Updates eventuell mit einem laufenden Paint-Schritt kollidieren.

8. rAF vs. CSS-Animationen vs. Web Animations API

Die Wahl zwischen requestAnimationFrame, CSS-Animationen und der Web Animations API hängt vom Anwendungsfall ab. CSS-Animationen und CSS-Transitions sind für einfache, deklarativ beschreibbare Effekte die erste Wahl: Sie laufen auf dem Compositor-Thread, brauchen kein JavaScript und sind am performantesten. Sobald aber die Animation auf Echtzeit-Daten, Benutzereingaben oder komplexe Physik-Berechnungen reagieren muss, reicht CSS nicht mehr aus.

Die Web Animations API (element.animate()) bietet einen programmatischen Zugang zu CSS-Animations-Mechanismen und ist für viele mittlere Komplexitätsstufen ideal. Sie liefert Playback-Control (play, pause, reverse, cancel) und Promise-based Completion-Callbacks. requestAnimationFrame ist die tiefste Ebene: maximale Kontrolle, maximaler Aufwand. Für Canvas-Rendering, WebGL, komplexe Partikel-Systeme oder Physics-Engines gibt es keine Alternative zu rAF.

Methode Thread Kontrolle Ideal für
CSS Transition/Animation Compositor Niedrig (deklarativ) Hover-Effekte, einfache Übergänge
Web Animations API Compositor Mittel (Playback-Control) Sequenzen, Pause/Play, async
requestAnimationFrame Main Thread Maximal (imperativ) Canvas, Physik, Echtzeit-Daten
setInterval/setTimeout Main Thread Niedrig (unsynced) Nicht für Animationen geeignet

9. Praxisbeispiel: Scroll-Progress-Indikator

Ein Scroll-Progress-Indikator ist das ideale Praxisbeispiel für requestAnimationFrame: Der Fortschrittsbalken soll flüssig reagieren, ohne jeden einzelnen Scroll-Event direkt zu verarbeiten. Das Muster: Scroll-Events setzen nur eine Flag oder speichern den aktuellen Scroll-Wert. Der rAF-Loop liest diese Werte einmal pro Frame und aktualisiert die Anzeige. So werden mehrere Scroll-Events, die zwischen zwei Frames auftreten, zu einem einzigen DOM-Update zusammengefasst.

Dieser Ansatz ist als „Passive Event Listener mit rAF-Scheduling" bekannt und ist die empfohlene Methode für alle Scroll-, Mouse- und Resize-Handler, die DOM-Änderungen auslösen. Der Scroll-Event-Listener wird als passive: true registriert, was dem Browser signalisiert, dass der Handler preventDefault() nicht aufruft. Das erlaubt dem Browser, das Scrollen auf dem Compositor-Thread fortzusetzen, ohne auf den JavaScript-Main-Thread warten zu müssen – und vermeidet Scroll-Jank vollständig.


// Scroll-progress indicator — passive listener + rAF scheduling
class ScrollProgress {
  constructor(barElement) {
    this.bar = barElement;
    this.rafId = null;
    this.scrollY = 0;
    this.ticking = false;

    // Passive: browser need not wait for JS before scrolling
    window.addEventListener('scroll', this.onScroll.bind(this), { passive: true });
  }

  onScroll() {
    this.scrollY = window.scrollY;

    // Only schedule one rAF per frame — guard with ticking flag
    if (!this.ticking) {
      this.rafId = requestAnimationFrame(this.update.bind(this));
      this.ticking = true;
    }
  }

  update() {
    const docHeight = document.documentElement.scrollHeight - window.innerHeight;
    const progress = docHeight > 0 ? this.scrollY / docHeight : 0;
    // transform: scaleX — no layout, compositor-only
    this.bar.style.transform = `scaleX(${progress})`;
    this.ticking = false;
  }

  destroy() {
    window.removeEventListener('scroll', this.onScroll);
    if (this.rafId) cancelAnimationFrame(this.rafId);
  }
}

const progressBar = document.getElementById('progress-bar');
progressBar.style.transformOrigin = 'left center';
const indicator = new ScrollProgress(progressBar);

10. Zusammenfassung

requestAnimationFrame ist das zentrale Werkzeug für alle JavaScript-Animationen, die über einfache CSS-Transitions hinausgehen. Es synchronisiert Callbacks mit dem Browser-Rendering-Zyklus, pausiert bei versteckten Tabs und passt sich automatisch an die native Display-Frequenz an. Der DOMHighResTimeStamp-Parameter ermöglicht frame-rate-unabhängige Animationen über Delta-Berechnungen. cancelAnimationFrame ist kein optionales Cleanup – es ist Pflicht, um Memory-Leaks in Komponenten-Architekturen zu vermeiden.

Die wichtigsten Leitlinien: Lesen vor Schreiben, um Layout-Thrashing zu vermeiden. transform und opacity bevorzugen, weil sie auf dem Compositor-Thread laufen. Scroll- und Input-Handler auf rAF-Scheduling delegieren, nicht direkt DOM-Operationen ausführen. Frame-Throttling einsetzen, wenn 60 fps für den Anwendungsfall überdimensioniert ist. Und immer: Den Loop-Handle speichern und sauber aufräumen.

Mironsoft

JavaScript Performance, Frontend-Architektur und Browser-APIs

Animationen, die wirklich flüssig laufen?

Wir analysieren bestehende Animation-Implementierungen, erkennen Layout-Thrashing und setInterval-Missbrauch, und ersetzen sie durch saubere requestAnimationFrame-Loops mit korrektem Frame-Budget-Management.

Performance-Audit

Layout-Thrashing und Dropped Frames in DevTools identifizieren

rAF-Refactoring

setTimeout-Animationen durch korrekte rAF-Loops ersetzen

Canvas & WebGL

Hochperformante Render-Loops für datenintensive Visualisierungen

requestAnimationFrame — Das Wichtigste auf einen Blick

Render-Synchronisierung

rAF läuft vor dem Render-Schritt des Browsers – Callbacks sehen den nächsten Frame. Pausiert automatisch bei unsichtbaren Tabs und passt sich an Display-Frequenz an.

Frame-Rate-Unabhängigkeit

DOMHighResTimeStamp-Delta verwenden: Bewegung in px/ms berechnen, nicht px/frame. So läuft die Animation auf 60 Hz und 120 Hz identisch schnell.

Layout-Thrashing verhindern

Erst alle DOM-Reads, dann alle DOM-Writes. transform und opacity bevorzugen – beide laufen auf dem Compositor-Thread ohne Layout-Trigger.

Sauberes Aufräumen

Handle speichern, cancelAnimationFrame im Cleanup aufrufen. Ohne Cancel: Memory-Leaks und CPU-Verschwendung in Komponenten-Architekturen.

11. FAQ: requestAnimationFrame

1Was macht requestAnimationFrame anders als setTimeout?
rAF synchronisiert mit dem Browser-Rendering-Zyklus und pausiert bei unsichtbaren Tabs automatisch. setTimeout läuft blind und verschwendet Ressourcen im Hintergrund.
2Wie stoppe ich einen rAF-Loop?
Handle speichern: rafId = requestAnimationFrame(fn), dann cancelAnimationFrame(rafId). Ohne Handle kein sauberer Stop möglich.
3Was ist Layout-Thrashing?
Abwechselndes Lesen und Schreiben im DOM – erzwingt Layout-Neuberechnungen. Alle Reads zuerst, dann Writes. transform/opacity statt width/top.
4Wie macht man Animationen frame-rate-unabhängig?
Delta-Berechnung: delta = timestamp - lastTimestamp. Bewegung in px/ms statt px/frame definieren – gleiche Geschwindigkeit auf 60 Hz und 120 Hz.
5Wann ist CSS-Animation besser?
Für einfache, deklarative Effekte: Compositor-Thread, kein JavaScript-Overhead, kein Main-Thread-Block.
6rAF für nicht-visuelle Aufgaben?
Nicht sinnvoll – dafür requestIdleCallback verwenden. rAF ist für Arbeit gedacht, die vor dem nächsten Render abgeschlossen sein muss.
7Was passiert bei zu langem Callback?
Frame-Budget (≈16 ms) überschritten → Dropped Frame → sichtbares Ruckeln. Nächster Callback beim nächstmöglichen Frame-Zeitpunkt.
8Was ist Frame-Throttling?
Loop mit nativer Rate, Update-Code nur bei ausreichendem Delta. 30-fps-Updates im 60-fps-Loop – synchronisiert, ohne setInterval.
9Loop nach Tab-Wechsel ruckelt?
lastTimestamp auf null setzen beim Resume. Delta wird als 0 initialisiert statt als riesige Zeitspanne. Alternativ: delta auf max. 100 ms begrenzen.
10rAF in React integrieren?
useEffect: Handle in useRef speichern (nicht State), Loop starten, Cleanup-Return mit cancelAnimationFrame. Kein Re-render durch den Handle selbst.