跪拜 Guibai
← All articles
Frontend

requestAnimationFrame vs. setTimeout: Why Frame Sync Matters More Than You Think

By Hyyy ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A timer-driven animation on a background tab keeps burning CPU cycles for no visible output; rAF stops that automatically. Getting the execution slot right also avoids an entire class of layout thrashing bugs that are easy to create and hard to diagnose.

Summary

Timers like setTimeout and setInterval run as macro-tasks in the event loop with no connection to the rendering cycle, so a callback can fire mid-frame, miss a frame entirely, or stack up multiple times per frame. requestAnimationFrame instead hooks into the render pipeline, executing its callback queue after JS tasks finish but before style calculation and layout begin. That placement guarantees style changes are processed in the current frame.

The API also auto-pauses when a tab is hidden, passes a microsecond-precision DOMHighResTimeStamp for progress calculation, and returns a handle that cancelAnimationFrame can abort. The standard pattern uses elapsed time rather than fixed pixel increments, keeping total duration consistent even when frame rate dips.

Common traps include assuming rAF fires on a fixed 16.6ms interval (it tracks the display's actual refresh rate and yields when the main thread is blocked) and believing rAF alone makes animation smooth. If per-frame work exceeds the budget, jank still happens. The API is also useful beyond animation for chunked DOM updates and avoiding forced synchronous layouts.

Takeaways
setTimeout and setInterval fire as macro-tasks decoupled from the render cycle, so a callback can land anywhere within a frame and cause jank or dropped frames.
requestAnimationFrame executes its callback queue after JS tasks finish and before style calculation, so style changes are picked up by the current frame's layout and paint.
rAF automatically aligns its firing rate to the display's refresh rate and pauses entirely when the tab is hidden or minimized.
The callback receives a DOMHighResTimeStamp with microsecond precision; using it for progress calculation keeps animation duration consistent across devices, unlike Date.now().
cancelAnimationFrame(id) aborts a scheduled callback, mirroring clearTimeout, and should be called on component unmount or animation stop to prevent leaks.
Using rAF does not guarantee smooth animation if per-frame work exceeds the frame budget; heavy computation still needs to be moved to a Web Worker or requestIdleCallback.
Reading then writing DOM styles inside a single rAF callback triggers a forced synchronous layout and tanks performance.
rAF is not limited to animation; it works for chunked DOM rendering, splitting long tasks, and any logic that benefits from aligning with the render rhythm.
Conclusions

The mental model shift is subtle but consequential: rAF is not a timer replacement but a render-pipeline hook. Developers who treat it as a faster setTimeout miss the scheduling guarantee that makes it valuable.

The 4ms minimum delay on setTimeout is often cited as a precision problem, but the real damage is the unpredictable slot within the event loop. Even a 0ms timeout can land after the current frame's paint, creating a full frame of latency.

High-refresh-rate displays (120Hz, 144Hz) shrink the per-frame budget to 8.3ms or less. On those devices, the difference between a correctly scheduled rAF callback and a timer callback that misses the frame boundary becomes visibly sharper.

Concepts & terms
Browser rendering pipeline (single frame)
The sequence of stages a browser completes to produce one screen update: input events, JS macro/micro-tasks, rAF callbacks, style calculation, layout (reflow), paint (repaint), and compositing.
Forced synchronous layout
A performance pitfall where reading a DOM property (e.g., offsetWidth) immediately after a write forces the browser to compute layout synchronously before the next frame, blocking the main thread.
DOMHighResTimeStamp
A high-precision timestamp measured in milliseconds from the start of the page's origin, with microsecond accuracy. It is passed to rAF callbacks and is immune to system clock changes.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗