Why requestAnimationFrame Beats setTimeout: Deep Dive into the Browser Event Loop

requestAnimationFrame lets browsers schedule animation callbacks just before the next repaint, offering smoother 60 Hz updates compared to setTimeout; this article explains its API, demonstrates code examples, explores the event loop, task and microtask queues, rendering timing, cross‑browser quirks, and best practices for efficient animation and task splitting.

Aotu Lab
Aotu Lab
Aotu Lab
Why requestAnimationFrame Beats setTimeout: Deep Dive into the Browser Event Loop

1. What is requestAnimationFrame

window.requestAnimationFrame()

tells the browser that you want to run an animation and asks it to invoke the supplied callback before the next repaint. The callback runs just before the browser redraws the page.

const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
function animation() {
  if (i > 200) return;
  test.style.marginLeft = `${i}px`;
  window.requestAnimationFrame(animation);
  i++;
}
window.requestAnimationFrame(animation);

On a typical 60 Hz display the code runs about 60 times per second (≈16.6 ms per frame). The API returns a request ID that can be passed to cancelAnimationFrame() to stop the callback.

const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
let requestId: number;
function animation() {
  test.style.marginLeft = `${i}px`;
  requestId = requestAnimationFrame(animation);
  i++;
  if (i > 200) {
    cancelAnimationFrame(requestId);
  }
}
animation();
requestAnimationFrame animation result
requestAnimationFrame animation result

2. Comparison with setTimeout

Using a zero‑delay setTimeout can mimic requestAnimationFrame, but the visual result differs because setTimeout may fire many times before the browser gets a chance to render.

const test = document.querySelector<HTMLDivElement>("#test")!;
let i = 0;
let timerId: number;
function animation() {
  test.style.marginLeft = `${i}px`;
  timerId = setTimeout(animation, 0);
  i++;
  if (i > 200) {
    clearTimeout(timerId);
  }
}
animation();
setTimeout animation result
setTimeout animation result

3. Event Loop and requestAnimationFrame

The browser’s event loop coordinates tasks, microtasks, rendering and idle periods on the main thread.

3.1 Task queue

Each event loop has one or more task queues (sets of tasks). The loop selects the first runnable task, executes it, then runs the microtask checkpoint.

3.2 Task sources

Tasks can originate from DOM operations, user interactions, network I/O, history traversal, timers ( setTimeout, setInterval), IndexedDB, etc.

3.3 Microtasks

Microtasks are a true FIFO queue. Common sources are promises, MutationObserver, Object.observe (deprecated), and Node.js’s process.nextTick.

3.4 Event‑loop processing steps

Select a task queue that contains at least one runnable task; otherwise, proceed to microtasks.

Take the oldest task from the selected queue and mark it as the currently running task.

Execute the task.

Clear the running‑task reference.

Run the microtasks checkpoint (execute all microtasks).

Set hasARenderingOpportunity to false.

Update rendering.

If there are no remaining tasks, an empty microtask queue, and no rendering opportunity, run an idle period via requestIdleCallback.

Return to step 1.

During the rendering step the browser processes resize/scroll events, runs CSS animations, executes requestAnimationFrame callbacks, and finally paints the document. Therefore requestAnimationFrame callbacks are invoked just before the style/layout/paint phases, ensuring smooth visual updates.

Event loop processing diagram
Event loop processing diagram

4. Cross‑browser differences

The following example demonstrates a discrepancy between Chrome/Firefox and Safari.

test.style.transform = 'translate(0, 0)';
document.querySelector('button').addEventListener('click', () => {
  const test = document.querySelector('.test');
  test.style.transform = 'translate(400px, 0)';
  requestAnimationFrame(() => {
    test.style.transition = 'transform 3s linear';
    test.style.transform = 'translate(200px, 0)';
  });
});

In Chrome and Firefox the element moves right by 200 px because the requestAnimationFrame callback runs before CSS style calculation. Safari executes the callback after the current frame, so the element starts at 400 px and moves left to 200 px.

Chrome vs Safari animation difference
Chrome vs Safari animation difference

5. Reproducing Safari timing

To obtain Safari‑like behaviour in a standards‑compliant environment, nest an additional requestAnimationFrame call so that the inner callback runs in the next frame.

test.style.transform = 'translate(0, 0)';
document.querySelector('button').addEventListener('click', () => {
  const test = document.querySelector('.test');
  test.style.transform = 'translate(400px, 0)';
  requestAnimationFrame(() => {
    requestAnimationFrame(() => {
      test.style.transition = 'transform 3s linear';
      test.style.transform = 'translate(200px, 0)';
    });
  });
});

6. Other uses of requestAnimationFrame

Because setTimeout can cause dropped frames, requestAnimationFrame should be preferred for visual updates. It can also be used to split large JavaScript tasks across frames, preventing a single long task from blocking rendering.

var taskList = breakBigTaskIntoMicroTasks(monsterTaskList);
requestAnimationFrame(processTaskList);
function processTaskList(taskStartTime) {
  var taskFinishTime;
  do {
    var nextTask = taskList.pop();
    processTask(nextTask);
    taskFinishTime = window.performance.now();
  } while (taskFinishTime - taskStartTime < 3);
  if (taskList.length > 0) {
    requestAnimationFrame(processTaskList);
  }
}

7. References

MDN – https://developer.mozilla.org/zh-CN/docs/Web/API/window/requestAnimationFrame

CodeSandbox example – https://codesandbox.io/s/raf-ycqc3

HTML spec – Event Loop – https://html.spec.whatwg.org/multipage/webappapis.html#event-loop

Generic task sources – https://html.spec.whatwg.org/multipage/webappapis.html#generic-task-sources

Event loop processing model – https://html.spec.whatwg.org/multipage/webappapis.html#event-loop-processing-model

Medium article on JavaScript main thread – https://medium.com/@francesco_rizzi/javascript-main-thread-dissected-43c85fce7e23

Jake Archibald’s site – https://jakearchibald.com/

JSConf talk – https://www.youtube.com/watch?v=cCOL7MC4Pl0

Google Web Fundamentals – Optimize JavaScript Execution – https://developers.google.com/web/fundamentals/performance/rendering/optimize-javascript-execution

Browser RenderingEvent LoopJavaScript animationrequestAnimationFrame
Aotu Lab
Written by

Aotu Lab

Aotu Lab, founded in October 2015, is a front-end engineering team serving multi-platform products. The articles in this public account are intended to share and discuss technology, reflecting only the personal views of Aotu Lab members and not the official stance of JD.com Technology.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.