So when I stumbled upon the task of creating a countdown timer and stopwatch in one of my projects, setInterval felt right at home to me. So, I went ahead and created a simple function.
As you can see, it works perfectly. But wait, before we proceed towards making our new time machine, let’s run a simple test. We will create a loop to run a hundred million times to simulate an event which blocks browser main thread.
However, there are a few clever techniques which we can employ to make the timer behave as we expect. Below I am listing four such techniques
- Using Date Object
- Using requestAnimationFrame
- Using web worker (async timer)
- Using web worker (async function)
Using Date Object
A notable difference you can see here is that the timer, although frozen due to function call, resumes at the correct time as soon as the main thread gets free. This ensures the timer always remains accurate irrespective of any heavy processing on the main thread.
So while you are watching this kitty move, the motion is actually an illusion of your browser creating frames quickly enough for you to find it smooth.
Calling requestAnimationFrame performs such draw operation and takes a single parameter, a function which it calls back before the next repaint. Why should you use it? Well, to save precious resources. The problem with setInterval is that it keeps on triggering, irrespective of whether the app is running in foreground, background, maximised, minimised or scrolled out of view. This is not a big issue for desktops. However, if you are running your app on a mobile device like a laptop or smartphone, requestAnimationFrame is your knight in shining armour which runs only when your app is in foreground. Also, if you are using requestAnimationFrame at multiple places, your browser batch updates them all during the next paint operation.
What we are doing in this code is calling requestAnimationFrame which returns time in milliseconds elapsed since the page loaded as a parameter to callback function. For the sake of simplicity, we are displaying the same time as start time. Callback calls rAFCallback function which displays the same time and performs a recursive call to requestAnimationFrame. Another interesting advantage of this is, there is no frame loss. Using setInterval can generate data faster than your browser can render, but requestAnimationFrame gets called only when your browser is ready to render next frame.
Using web worker (async timer)
We can use the same Web API to spawn a process which runs in the background thread and returns response using postMessage method.
Here, we have used a setInterval method, which, though deemed unreliable in a single threaded environment, is extremely accurate when running on a separate thread.
Using web worker (async function)
If you have followed along till this section, you might have realised we are getting a bit redundant here, right? Everything we have done so far involves some technique to prevent the timer from going out of sync due to a blocking operation. But have you noticed that in all the approaches listed above, the timer stops anyway. Although I get the correct time, the effect where my timer freezes for a few seconds when I am running some intensive computation is annoying and undesirable. Even worse, this behaviour is also observable in most popular timer libraries, both paid and free ones.
In their defence, there’s only so much a library can do when you choke up the main thread. So how can we get a timer which doesn’t freeze on running heavy operations. The answer is simple, by running all heavy operations in the background thread.
This is the most efficient approach we have used so far. It uses requestAnimationFrame to prevent any frame loss, uses Web worker to run a heavy process in the background, and has the ability to resume on track if some other heavy process runs in the main thread.
I hope you’ve enjoyed this article and got to learn a thing or two from it. To summarise everything, I have created a Plunker which you can fiddle with
I’d love to hear your thoughts and questions on this in the comments below.