2022-07-26

setInterval in Javascript doesn't run as expected

Early this year, we had to implement a countdown timer for a survey. When the timer is up, participants will not be able to continue answering it. We used the setInterval and implemented a function that minus the time by the second.

When our client tested it, she felt like the timer got paused when her browser idled or whenever she switched to another window. It seems that setInterval is not triggered, and it happens quickly after a few minutes of starting the survey.

We took it back to check the issue on our environment. At first, we couldn't replicate the bug, but we thought it's because our machine had more power, therfore it may be running fine. But as we kept it running for some duration (at about the hour mark), the issue started to appear. Instead of calling every second, our method is called every minute!

lalt

You could argue that this is only aesthetic as no one can see the clock ticking down if the browser is idled or switches tab. But we implemented other checks on each second such as kicking out from the survey if the expected time is up. There is also an implementation where it will auto pause the survey if the expected reponse from the client to the server is delayed. This is an indication if client loses the internet connection while in the survey.

A quick Google search led us to this article. Since Javascript is a single-threaded language, any callback methods is queued in an event loop. Which includes the setInterval and setTimeout methods. Any queues in an event loop, will not be guaranteed to be executed based on the time to execute, as that is only a minimum guaranteed time.

As an example, below is a code that I slightly modified from the article I pasted earlier.

(function() {

  console.log('this is the start');

  setTimeout(function cb() {
    console.log('Callback 1: this is a msg from call back');
  }); // has a default time value of 0

  console.log('message 2');

  setTimeout(function cb1() {
    console.log('Callback 2: this is a msg from call back');
  }, 0);

  console.log('message 3');

})();

We would have thought that Callback 1 is called first as the setTimeout callback time defaults to 0, followed by message 2, and so on. But the actual output will be as follows:

// "this is the start"
// "message 2"
// "message 3"
// "Callback 1: this is a msg from call back"
// "Callback 2: this is a msg from call back"

There are various ways to mitigate this. One way is to use Window.requestAnimationFrame(). It will call a specific method to update the frame and is separated from the main event loop. However, if the current window or tab is idle, it will be suppressed to save battery and improve performance. (article)

requestAnimationFrame() calls are paused in most browsers when running in background tabs or hidden <iframe>s in order to improve performance and battery life.

We end up with using setTimeout still. It is not the perfect solution, but it still keeps the timer running as if it is running down by the second. We checked the drift value, the time difference between the expected call time versus the actual time the method is called. When the drift exceeds a certain threshold, we set the next setTimeout a little quicker, so we maintain the method calls does not drift further away.

Here is an example of how we implemented it. The code is a standard timer tick instead of a countdown, but the concept is similar.

I still think that we could improve it, such as implementing requestAnimationFrame() for the UI while implementing the lock and extension of time at the backend. I also saw other examples on the internet that handles the drift values more aggressive, but this implementation suffice our needs so far. Let me know what you think, and feel free to show me a better way of handling it.