Implementing setTimeout using requestAnimationFrame

Ceci García García
Trabe
Published in
2 min readMar 9, 2020
Photo by Brianna Santellan on Unsplash

Lately, I’ve been doing some work on improving the performance of a web application and I came up against an odd problem: a delayed onClick handler took around 1200ms, 300ms of which were expended on the defined delay and 100ms on executing the callback. So who is to blame for wasting the other 800ms? The JavaScript event loop 😑.

The setTimeout function sets a timer which executes a function on the next event cycle once the timer expires. This means, the final delay will depend on the current status of the event loop.

How can we make the callback function be executed right after the delay time has passed?

requestAnimationFrame to force immediate execution

With requestAnimationFrame method, you can tell the browser that you want it to perform a specified function before the next repaint. Even though the aim of the method is to perform animations, we can use it to prevent our callback being enqueued in the event loop, forcing the browser to give it high priority.

Here is our implementation of a “high priority” setTimeout:

We require a registerCancel function to provide the option to cancel the request. The common way to do that is by returning an id and providing a function to cancel a request using its id, like setTimeout does:

let t = setTimeout(work, 200);
clearTimeout(t);

However our implementation of requestTimeout isn’t really a single request but a loop of requests. Every request creates a new request if the delay time hasn’t passed. This way the request id changes on every loop so we need to have the id updated to cancel the current request. To do that, we can create a local variable or an immutable object to store the current request id and a function to update it:

let t = null;
requestTimeout(work, 200, id => (t = id));
cancelAnimationFrame(t);

The same way we store the id, we can store the scoped function to cancel the current request so we don’t need to export a cancel function:

let cancel = null;
requestTimeout(work, 200, fn => (cancel = fn));
cancel();

Every time the loop creates a new request, the scoped cancel function is reinjected. Looking back at our code:

Legacy browsers compliant

Most browsers support requestAnimationFrame but if you need to support those that don’t, you can adapt the implementation to delegate to setTimeout in these cases:

Wrapping up

Sometimes we need to delay an execution but when the delay time has passed we need to perform the execution as soon as possible, skipping the event loop. This article describes an alternative implementation of the setTimeout function that uses the requestAnimationFrame under the hood to give our callback a higher priority.

--

--