Why Javascript timer is unreliable, and how can you fix it
If you are a Javascript developer, at some point in your career, you must have used setTimeout or setInterval. They are extremely handy if you wish to perform an operation after some time, or you want to repeat an operation multiple times after a certain interval.
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.
As you can see, when we call the function to run the loop a hundred million times, the timer actually stops. Even more concerning is the fact that it resumes from the same time it paused on, missing the time period in between. So, if we create any mission critical application in Javascript which uses timer, the result would be catastrophic.
The reason why this happens is because Javascript is single threaded. Which means unlike other programming languages where intensive processes run in a background thread, in Javascript, everything runs in the main thread. Hence, when we call the function hangTheBrowser, the running loop blocks the main thread, thereby pausing the timer until it finishes executing.
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
This is the simplest, albeit very effective approach. Basically, our implementation remains the same as in earlier code, however, the only change we do is use Javascript Date method to maintain count using epoch time instead of our own count variable.
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.
Using requestAnimationFrame
Javascript provides a handy method called requestAnimationFrame to re-render the DOM on demand. Uday Hirawale has written an interesting post on Medium explaining the working of requestAnimationFrame. TL;DR version is that every time your browser window repaints due to any draw operation, it generates a new frame.
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)
Remember when I said Javascript is single threaded, well… that’s true, but not strictly. Say when you run a block of code inside setTimeout, what happens? The block of code executes after the timeout expires. But what happens when the timeout is ongoing is that javascript offloads the block of code to Web API which runs in a separate thread in the background. There is an awesome video in which Philip Roberts has explained how Web API works.
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.