Published 12/2/2025 · 4 min read
Tags: javascript , animation , performance
Understanding requestAnimationFrame
If you’ve ever built animations in JavaScript, you’ve probably reached for setInterval or setTimeout. They work, but there’s a better way: requestAnimationFrame.
Let’s break down what it actually does and why it matters.
The Problem with setInterval
Here’s how most developers start animating:
let position = 0;
setInterval(() => {
position += 1;
element.style.transform = `translateX(${position}px)`;
}, 16); // roughly 60fps
This has real problems:
- It keeps running even when the tab is hidden (wasting battery)
- It doesn’t sync with the browser’s actual repaint cycle
- You’re guessing at the timing (16ms ≈ 60fps, but that’s not guaranteed)
- It can cause janky animations when the browser is busy
What requestAnimationFrame Actually Does
requestAnimationFrame tells the browser: “Before you paint the next frame, run this function.”
The browser handles the timing. It knows when the screen will refresh (usually 60Hz, but could be 120Hz or 144Hz on newer displays) and schedules your code to run right before each paint.
let position = 0;
function animate() {
position += 1;
element.style.transform = `translateX(${position}px)`;
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);
The Key Differences
Timing is handled by the browser. You don’t guess at frame rates. On a 60Hz display, it runs ~60 times per second. On a 144Hz display, it runs ~144 times per second.
It pauses when the tab is hidden. Switch to another tab? The animation stops. No wasted CPU cycles, no battery drain.
It syncs with the paint cycle. Your changes happen right before the browser repaints, which means smoother animations with no tearing.
Using the Timestamp Parameter
requestAnimationFrame passes a timestamp to your callback. This is crucial for frame-rate independent animation:
let startTime = null;
const duration = 2000; // 2 seconds
const distance = 300; // pixels
function animate(timestamp) {
if (!startTime) startTime = timestamp;
const elapsed = timestamp - startTime;
const progress = Math.min(elapsed / duration, 1);
element.style.transform = `translateX(${progress * distance}px)`;
if (progress < 1) {
requestAnimationFrame(animate);
}
}
requestAnimationFrame(animate);
This moves the element 300px over exactly 2 seconds, regardless of frame rate. On a 60Hz monitor or a 144Hz monitor, the animation takes the same amount of time.
Stopping an Animation
requestAnimationFrame returns an ID you can use to cancel:
let animationId = null;
function start() {
function animate() {
// do animation work
animationId = requestAnimationFrame(animate);
}
animationId = requestAnimationFrame(animate);
}
function stop() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = null;
}
}
A Practical Pattern
Here’s a reusable animation function you can use:
function animateValue({ from, to, duration, onUpdate, onComplete }) {
const startTime = performance.now();
function tick(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
// Linear interpolation
const value = from + (to - from) * progress;
onUpdate(value);
if (progress < 1) {
requestAnimationFrame(tick);
} else if (onComplete) {
onComplete();
}
}
requestAnimationFrame(tick);
}
// Usage
animateValue({
from: 0,
to: 100,
duration: 1000,
onUpdate: (val) => {
counter.textContent = Math.round(val);
},
onComplete: () => {
console.log("Done!");
},
});
When to Use It
Use requestAnimationFrame for:
- DOM animations (position, opacity, transforms)
- Canvas drawing
- WebGL rendering
- Scroll-linked effects
- Any visual update that needs to sync with the display
Don’t use it for:
- Non-visual timers (use
setTimeout) - Precise timing that must continue in background tabs
Browser Support
It’s supported everywhere that matters. IE10+ and all modern browsers. No polyfill needed in 2025.
The Takeaway
requestAnimationFrame isn’t just a better setInterval. It’s the correct way to animate in the browser. The browser knows when it’s going to paint — let it tell you when to update.
Your animations will be smoother, your battery usage lower, and your code cleaner.
Related Articles
- Deploying Your x402 App to Production
Deploy your SvelteKit frontend and Bun backend to production. Environment setup, mainnet configuration, and monitoring.
- Advanced x402: Pricing Strategies and Session Management
Implement dynamic pricing, session tokens for repeat access, and usage tracking for your x402 APIs.
- x402 with SvelteKit: Full-Stack Example
Build a complete SvelteKit application with x402 payments - wallet connection, protected routes, and automatic payment handling.