Talking about requestAnimationFrame

I am participating in the “Nuggets·Starting Plan”

Foreword

Timers and timed executions have been the state-of-the-art tools for JavaScript animations for a long time. While CSS transitions and animations have made some animations easier for developers, the JavaScript animation space has seen little progress over the years. The requestAnimationFrame() method came into being. This method will tell the browser to perform animation, so the browser can determine the timing of redrawing in an optimal way.

Early timing animation

Previously, creating animations in JavaScript was basically using setInterval() to control the execution of the animation:

(function () {
  function updateAnimations() {
    doAnimation1();
    doAnimation2();
    //...
  }
  setInterval(updateAnimations, 100);
})();

The problem with this timed animation is that the delay between loops is not known exactly.

Neither setInterval() nor setTimeout() can guarantee time precision. The delay as the second parameter only guarantees when the code will be added to the browser’s task queue, and does not guarantee that it will be executed immediately when added to the queue. If there are other tasks in front of the queue, then it is necessary to wait for these tasks to be executed before executing them.

To put it simply, the millisecond delay here does not mean when these codes will be executed, but when the callback will be added to the task queue. If the main thread is occupied by other tasks after being added to the queue, the callback will not be executed immediately.

Knowing when to draw the next frame is key to creating smooth animations, so the imprecision of setInterval() and setTimeout() is a big problem.

The precision of the browser’s own timers adds to the problem. Browser timers are sub-millisecond accurate, with the most powerful Chrome timers being 4ms accurate. To make things even more troublesome, the browser starts throttling the timers in background or inactive tabs again, so even setting the interval to an optimal one inevitably yields only approximate results.

Screen refresh rate

The screen refresh rate of a general computer monitor is 60HZ, which basically means that it needs to be redrawn 60 times per second. Most browsers will limit the frequency of redrawing so that it does not exceed the refresh rate of the screen, because the user will not perceive the refresh rate of the screen.

So, the best redraw interval for a smooth animation is 1000ms/60, which is about 17 ms. Redrawing at this rate allows for the smoothest animations, since this is already the browser’s limit.

requestAnimationFrame

requestAnimationFrame() This method can notify the browser that some JavaScript code is going to perform animation, so that the browser can perform appropriate optimization after running some code.

requestAnimationFrame() This method receives a parameter, which is a function to be called before redrawing the screen. This function is where the DOM styles are modified to reflect what changes on the next repaint. To implement an animation loop, multiple requestAnimationFrame() calls can be chained together, just as you did with setTimeout() before:

function updateProgress() {
  var div = document. getElementById("status");
  div.style.width = parseInt(div.style.width, 10) + 5 + "%";
  if (div. style. left != "100%") {
    requestAnimationFrame(updateProgress);
  }
}

requestAnimationFrame(updateProgress);

Since requestAnimationFrame() only calls the passed function once, it needs to be called again manually every time the UI is updated. At the same time, you also need to control when the animation stops. The result is a very smooth animation.

requestAnimationFrame() already solves the problem of the browser not knowing when the JavaScript animation starts, and what the optimal interval is. But what if we want to know the actual execution time of our code? There are also solutions.

The function passed to requestAnimationFrame() can actually receive a parameter, which indicates the time of the next redraw. This is very important: requestAnimationFrame() actually arranges the redrawing task at a known time point in the future, and tells the developer through this parameter, then based on this parameter, you can make more Good time to decide how to tune the animation:

function foo(t) {
  console. log(t);
  requestAnimationFrame(foo);
}

requestAnimationFrame(foo);

cancelAnimationFrame

Similar to setTimeout(), requestAnimationFrame() also returns a request ID, which can be used to cancel redrawing through another method cancelAnimationFrame() Task:

const requestID = window.requestAnimationFrame((t) => {<!-- -->
  console. log(t);
});
window.cancelAnimationFrame(requestID);

Throttling via requestAnimationFrame

Browsers that support this method will actually expose the callback queue as a hook. The so-called hook is a point before the browser performs the next redraw. This callback queue is a modifiable list of functions that should be called before redrawing. Each call to requestAnimationFrame() pushes a callback function on the queue, which has an unlimited length.

The behavior of this callback queue is not necessarily related to animation. Adding callback functions to the queue recursively through requestAnimationFrame() can ensure that the callback function is called at most once per redraw, which is a very good throttling tool. When frequently executing code that affects the appearance of the page (such as scrolling event listeners), you can use this callback queue to throttle.

Let’s look at a native implementation first, where the scroll event listener will call a function named expensiveOperation() (time-consuming operation) every time it is triggered. This event is quickly fired and executed hundreds of times when scrolling down the page:

function expensiveOperation() {<!-- -->
  console.log("Invoked at", Date.now());
}

window.addEventListener("scroll", () => {<!-- -->
  expensiveOperation();
});

If you want to limit the invocation of the event handler to just before each repaint, you can wrap it in a requestAnimationFrame() call:

function expensiveOperation() {<!-- -->
  console.log("Invoked at", Date.now());
}

window.addEventListener("scroll", () => {<!-- -->
  window.requestAnimationFrame(expensiveOperation);
});

This will concentrate the execution of all callbacks in the redraw hook, but will not filter out redundant calls for each redraw. We can define a flag variable and set its state in the callback to block redundant calls:

let enqueued = false;

function expensiveOperation() {
  console.log("Invoked at", Date.now());
  enqueued = false;
}

window. addEventListener("scroll", () => {
  if (!enqueued) {
    enqueued = true;
    window.requestAnimationFrame(expensiveOperation);
  }
});

Since redrawing is a very frequent operation, this is not really a throttling. A better approach is to use a timer to limit the frequency of operation execution. In this way, the timer can limit the actual operation execution interval, and requestAnimationFrame() controls which rendering cycle of the browser is executed:

let enabled = true;

function expensiveOperation() {<!-- -->
  console.log("Invoked at", Date.now());
}

window.addEventListener("scroll", () => {<!-- -->
  if (enabled) {<!-- -->
    enqueued = false;
    window.requestAnimationFrame(expensiveOperation);
    window.setTimeout(() => (enabled = true), 50);
  }
});

The example above limits the callback to execute every 50ms or so.

Last

If there are any mistakes or deficiencies in the article, please correct me in the comment area.

Your praise is a great encouragement to me! Thanks for reading~