requestAnimationFrame – die Animationsschleife, die zum Bildschirm passt
setInterval(schritt, 16) läuft auf einem 144-Hz-Monitor anders als auf einem alten Handy – und im Hintergrundtab einfach weiter. requestAnimationFrame löst beides, wenn man die verstrichene Zeit mitrechnet statt Bilder zu zählen.
Der naheliegende Weg für eine Bewegung im Browser:
// ⚠️ zwei Probleme auf einmal
setInterval(() => {
x += 2;
zeichne();
}, 16);Problem eins: 16 ist geraten. Auf einem 60-Hz-Bildschirm passt es ungefähr, auf einem 120-Hz-Gerät ruckelt es, weil deine Schritte nicht zum Bildaufbau passen. Problem zwei: Das läuft weiter, wenn der Tab im Hintergrund liegt — und frisst Akku für Bilder, die niemand sieht.
Die richtige Schleife
let letzte = 0;
function schritt(jetzt) {
const dt = letzte ? (jetzt - letzte) / 1000 : 0; // Sekunden seit dem letzten Bild
letzte = jetzt;
aktualisiere(dt);
zeichne();
requestAnimationFrame(schritt);
}
requestAnimationFrame(schritt);Der Browser ruft schritt genau dann auf, wenn er ohnehin ein Bild zeichnet — passend zur Bildwiederholrate des Geräts. Und im Hintergrundtab pausiert er von selbst.
Der eigentliche Kern: in Sekunden rechnen, nicht in Bildern
// ⚠️ hängt von der Bildrate ab
x += 2;
// ✅ überall gleich schnell
x += 200 * dt; // 200 Pixel pro SekundeDas ist der Unterschied zwischen „läuft bei mir gut" und „läuft überall gleich". Mit dt bewegt sich dein Objekt auf einem 144-Hz-Monitor genauso schnell wie auf einem Fünf-Jahre-alten Handy — es wird dort nur flüssiger dargestellt.
Der Sprung nach dem Tab-Wechsel
Kommt der Nutzer nach drei Minuten zurück, ist dt plötzlich riesig, und alles springt auf einen Schlag durch die halbe Szene. Deshalb deckle ich den Wert immer:
const dt = Math.min((jetzt - letzte) / 1000, 0.05); // höchstens 50 msLieber eine kurze Zeitlupe als ein Objekt, das durch eine Wand teleportiert.
Sauber anhalten
requestAnimationFrame gibt eine ID zurück, mit der sich die Schleife stoppen lässt:
let id = null;
function start() { if (!id) id = requestAnimationFrame(schritt); }
function stopp() { cancelAnimationFrame(id); id = null; letzte = 0; }Das letzte = 0 beim Stoppen ist wichtig: Beim nächsten Start wäre dt sonst die gesamte Pause.
Wann ich es *nicht* benutze
Für reine Gestaltungseffekte — Einblenden, Hover, Aufklappen — nehme ich CSS-Transitions und -Animationen. Die laufen im Browser auf einer eigenen Ebene, oft ganz ohne den Haupt-Thread, und sind praktisch immer flüssiger als alles, was ich in JavaScript nachbaue.
requestAnimationFrame ist für das, was CSS nicht kann: Spiellogik, Canvas, Physik, alles, wo der nächste Zustand vom vorherigen abhängt. In meinen Browserspielen ist genau das die Hauptschleife — und die dt-Rechnung der Grund, warum sie auf dem Handy und am Rechner gleich schnell laufen.
Du hast eine Animation, die auf manchen Geräten viel zu schnell ist? Das ist fast immer eine fehlende dt-Rechnung. Schreib mir.
Quellen
Du brauchst mehr als ein Snippet?
Ich entwickle Android-Apps in Kotlin und moderne Websites für Selbstständige und kleine Unternehmen — von der ersten Idee bis zum Release.
Projekt anfragen →Verwandte Snippets
ResizeObserver – auf die Größe eines Elements reagieren, nicht auf die des Fensters
window.onresize feuert nur, wenn sich das Fenster ändert – nicht, wenn eine Seitenleiste aufklappt oder Text nachlädt. ResizeObserver meldet dir die Größe des Elements selbst, und zwar ohne Endlosschleife.
Aufzählungen richtig verketten mit Intl.ListFormat
"a, b und c" von Hand zusammenzubauen geht fast immer schief – bei einem Element, bei zwei Elementen, beim letzten Komma. Intl.ListFormat macht es richtig, in jeder Sprache, ganz ohne Bibliothek.
Set und Map in JavaScript – doppelte Einträge loswerden und richtig zählen
Ein Objekt als Nachschlagetabelle geht – bis der Schlüssel eine Zahl ist, oder ein Objekt, oder zufällig "constructor" heißt. Set und Map machen genau das, wofür man Objekte bisher zweckentfremdet hat, nur ohne die Überraschungen.