Performance and DevTools
Use requestAnimationFrame for visual updates
Schedule work that changes the current frame near the browser’s next rendering opportunity.
requestAnimationFrame() asks the browser to call your function right before it paints the next frame. That makes it the right place for any code that changes what the frame looks like.
Compare it with setTimeout(). A timer fires whenever its delay is up, with no relation to when the screen refreshes. Set it to 16 milliseconds and it drifts: sometimes two callbacks land in one frame, sometimes a frame gets none, and the animation stutters. requestAnimationFrame() runs exactly once per frame, in sync with the display.
Here’s a box sliding 300 pixels to the right:
let start
function move(timestamp) {
start ??= timestamp
const elapsed = timestamp - start
const x = Math.min(elapsed / 4, 300)
box.style.transform = `translateX(${x}px)`
if (x < 300) requestAnimationFrame(move)
}
requestAnimationFrame(move)
The browser passes a timestamp to the callback. We store the first one and compute the position from the elapsed time, not from a frame counter. elapsed / 4 means 1 pixel every 4 milliseconds, so the trip takes 1.2 seconds on any screen.
That last point matters. A 60 Hz laptop gives you 60 callbacks per second. A 120 Hz phone gives you 120. A busy page drops frames. If you moved the box 5 pixels per callback, it would run twice as fast on the phone and slow down whenever the page struggled. Time-based math keeps the animation correct everywhere.
Browsers also pause or slow down these callbacks in background tabs. That’s a feature: a tab nobody is looking at shouldn’t burn CPU.
It doesn’t make slow work fast
requestAnimationFrame() schedules your code well. It doesn’t make it cheaper. A callback that takes 30 milliseconds still blows through the 16-millisecond frame budget, and the frame is late anyway.
Keep the callback focused on the state this frame needs. Fetch data, parse JSON, and crunch numbers somewhere else.
Coalesce repeated events
Scroll and pointer events can fire faster than the screen refreshes. Updating the DOM on each one is wasted work, because only the last position before the paint matters.
The pattern is to remember that a frame is already scheduled and skip the rest:
let scheduled = false
window.addEventListener('scroll', () => {
if (scheduled) return
scheduled = true
requestAnimationFrame(() => {
updateHeader(window.scrollY)
scheduled = false
})
})
No matter how many scroll events arrive, updateHeader() runs at most once per frame, with the freshest scrollY.
Record the animation in the Performance panel and open the Frames track. Each frame should take about the same time, and each callback should be short. If frames are still late, the problem is inside the callback or the rendering it triggers. Adding another scheduling layer won’t help.
Lesson completed