If your UI feels off but your code looks fine, this might be why.
Ever added a setTimeout(fn, 16) to fix a janky animation or squeeze in a DOM update? It mostly worked, right?
Until it didn’t.
Maybe the animation stuttered. Maybe the UI felt sluggish. Maybe the browser seemed to skip a beat for no obvious reason.
The problem? You weren’t syncing with the browser’s heartbeat. You were guessing. And that guess probably landed you somewhere between frame drops and layout thrash.
Let’s talk about the tool that aligns your updates with the browser’s rendering cycle: requestAnimationFrame() . And if you need to stop a scheduled frame before it runs? cancelAnimationFrame(id) is its cleanup partner.
The Frame Budget: Why 16ms Matters
Browsers aim to paint at 60 frames per second. That gives you about 16.66ms per frame to:
Handle input
Run JavaScript
Apply style changes
Perform layout and paint
Go over budget, and the frame drops. The animation stutters. The UI lags.
This is where requestAnimationFrame shines. It schedules your callback to run right before the next repaint. That’s your moment to update the DOM or tweak styles — because the browser hasn’t drawn yet. You’re getting in just under the wire.
Compare that to setTimeout(fn, 16) . It’s not aligned with the rendering cycle. It might run too early. Or too late. Or twice in a single frame if you’re unlucky. You’re not syncing with the browser — you’re racing it.
Oh, and on high refresh rate displays? You’ve got even less time. At 120Hz, your frame budget drops to ~8ms. requestAnimationFrame adjusts automatically. Your manual timer doesn't.
What requestAnimationFrame Actually Does
You might’ve used performance.now() to track time before — and that’s exactly what the timestamp parameter in requestAnimationFrame(callback) gives you. It’s a high-resolution time value (in milliseconds) based on the same clock as performance.now() , making it ideal for frame-by-frame calculations or animations.
requestAnimationFrame(fn) tells the browser:
“Hey, run this function before the next paint.”
That function:
Runs after the current JS stack clears
Runs before the browser paints
Gets passed a high-res timestamp
function step ( timestamp ) { // do something with timestamp, like tween animation requestAnimationFrame ( step ); } requestAnimationFrame ( step ); Enter fullscreen mode Exit fullscreen mode
This is why rAF is perfect for smooth animations and coordinated updates. It’s part of the render loop.
What It’s Not
It’s not a delay function
a delay function It’s not guaranteed to run every 16ms (especially in background tabs)
guaranteed to run every 16ms (especially in background tabs) It’s not just for fancy canvas animations
You can (and should) use it for:
Debouncing layout changes
Batching DOM writes
Polling scroll position without jank
Using requestAnimationFrame in React
React handles a lot of scheduling internally, but there are key places where requestAnimationFrame adds control and polish, especially when working outside the strict boundaries of render cycles.
Let’s walk through some practical examples:
- Smooth scroll tracking
When updating state based on scroll position, you don’t want to re-render on every scroll event. rAF gives you a way to throttle with the frame rate:
useEffect (() => { let animationFrameId ; const handleScroll = () => { if ( animationFrameId ) return ; animationFrameId = requestAnimationFrame (() => { const scrollTop = window . scrollY ; setScrollPosition ( scrollTop ); // assuming setScrollPosition is in scope animationFrameId = null ; }); }; window . addEventListener ( ' scroll ' , handleScroll ); return () => { window . removeEventListener ( ' scroll ' , handleScroll ); if ( animationFrameId ) cancelAnimationFrame ( animationFrameId ); }; }, []); Enter fullscreen mode Exit fullscreen mode
- Debounced visual feedback after rapid state changes
If you're applying CSS classes or triggering transitions in response to user actions, rAF helps you wait for layout to settle before updating visual state:
useEffect (() => { const id = requestAnimationFrame (() => { setShowTooltip ( true ); }); return () => cancelAnimationFrame ( id ); }, [ hovered ]); Enter fullscreen mode Exit fullscreen mode
- Custom animation loop with React state
If you’re using state to drive animation (e.g. motion trails, physics), rAF ensures updates align with paint:
const frame = useRef (); useEffect (() => { const tick = ( ts ) => { setAnimatedValue (( prev ) => prev + 1 ); frame . current = requestAnimationFrame ( tick ); }; frame . current = requestAnimationFrame ( tick ); return () => cancelAnimationFrame ( frame . current ); }, []); Enter fullscreen mode Exit fullscreen mode
⚠️ A Note on rAF and Re-renders
requestAnimationFrame doesn’t cancel or debounce React renders — it simply helps you align when you read and write data. If you’re using it to prevent over-rendering, pair it with useRef or non-state variables when possible.
Also worth noting: high refresh rate displays (e.g. 120Hz) call rAF more often. React doesn’t automatically throttle that. Be careful if you're triggering lots of state updates inside a loop.
Nested Patterns and Fallback Timing
Sometimes you’ll see requestAnimationFrame used inside setTimeout , or vice versa. This usually happens when developers want to account for inactive tabs or intentionally delay frame updates.
rAF inside setTimeout can help you run an animation every few hundred milliseconds instead of every frame:
setTimeout (() => { requestAnimationFrame ( runAnimation ); }, 250 ); Enter fullscreen mode Exit fullscreen mode
setTimeout inside rAF can serve as a fallback in background tabs where rAF doesn’t fire:
function loop () { if ( document . hidden ) { setTimeout ( loop , 100 ); } else { requestAnimationFrame ( loop ); } } Enter fullscreen mode Exit fullscreen mode
While these combos can be helpful, use them thoughtfully. They can introduce timing inconsistencies or blur your intent, especially in complex UI systems.
Performance Gotchas and Wins
✅ Use it for DOM write batching
let needsUpdate = false ; window . addEventListener ( ' resize ' , () => { needsUpdate = true ; }); function updateLayout () { if ( needsUpdate ) { // expensive DOM write repositionSidebar (); needsUpdate = false ; } requestAnimationFrame ( updateLayout ); } requestAnimationFrame ( updateLayout ); Enter fullscreen mode Exit fullscreen mode
❌ Don’t stack rAF on rAF unless needed
Avoid creating feedback loops unless you’re intentionally animating.
❌ Don’t expect it in background tabs
Browsers pause it to save resources. If your app relies on rAF for timers or metrics, back it up with setTimeout .
Live Comparison: rAF vs setTimeout
To see the difference in timing, jank, and scroll behavior, try this CodePen demo. with side-by-side comparisons:
A moving box animated with requestAnimationFrame
A second box using setTimeout(fn, 16)
Scroll tracking updates done naively vs. throttled with rAF
You’ll see that requestAnimationFrame not only looks smoother but responds better under load.
Bonus: Why Input Latency Drops with rAF
Ever notice a lag between a click and a visual response? If your code runs after the paint, your visual update won’t show until the next frame.
With requestAnimationFrame , you update before paint. That means smoother UI, lower perceived latency, and a faster-feeling app.
requestAnimationFrame is how you coordinate your updates with the browser’s rendering process. Use it when:
You’re animating
You’re batching DOM changes
You want visual changes to feel instant
You’re managing layout-affecting state outside React’s commit cycle
Don’t reach for setTimeout(fn, 16) and hope for the best. Sync with the render loop. Let the browser tell you when to act.
Because when you stop guessing, and start syncing, everything just feels smoother.
News blog on best practices: requestAnimationFrame for synchronous rendering.
requestAnimationFrame saves your UI! Still using setTimeout for animations?
Новостной блог о передовых практиках: requestAnimationFrame для синхронного рендеринга.
requestAnimationFrame спасает UI! Используешь setTimeout для анимаций?
Подробнее в ТГ: @DevPulseAI
Top comments (0)