Creating a self-correcting alternative to JavaScript’s setInterval

Recently, I’ve been toying with JavaScript’s setInterval method which, if you’re unfamiliar, allows you to execute code repeatedly at a specified time interval. For example, one might create an interval which executes every second by using the following code snippet:

setInterval( function() {
    console.log( 'I execute every second!' );
}, 1000 );

Both setInterval and its close cousin setTimeout suffer from latency caused by JavaScript’s single-threaded nature. While you may intend for an interval to execute every 1000 milliseconds, in reality it could take slightly longer for the function to be triggered. This is typically only a few milliseconds and therefore might appear to be a negligible problem. You might also expect that subsequent intervals would make an effort to get the code execution back on a predictable schedule. In other words, if there was 1007 milliseconds between registering the interval and the first code execution (a delay of 7 milliseconds), you could expect the next execution to occur as close to 2000 milliseconds as possible (i.e. 993 milliseconds later).

Depending on your browser, however, this may not be the case. We can demonstrate this by tracking the number of milliseconds which have passed since setInterval was registered:

var startTime = Date.now();
setInterval( function() {
    console.log( ( Date.now() - startTime ) + 'ms elapsed' );
}, 1000 );

If you run this in current versions of Chrome, Safari, Internet Explorer, or Node.js, you’ll notice that the interval execution grows increasingly out of sync with the original setInterval function call.

setInterval increasingly grows out of sync

In my testing, I’ve found that only Firefox attempts to keep the interval execution in sync.

Regardless of whether this is the intended behavior of setInterval, I needed a means by which I could execute code as closely as possible to a predictable interval. Below is my solution to this problem:

window.setCorrectingInterval = ( function( func, delay ) {
    var instance = { };

    function tick( func, delay ) {
        if ( ! instance.started ) {
            instance.func = func;
            instance.delay = delay;
            instance.startTime = new Date().valueOf();
            instance.target = delay;
            instance.started = true;

            setTimeout( tick, delay );
        } else {
            var elapsed = new Date().valueOf() - instance.startTime,
            adjust = instance.target - elapsed;

            instance.func();
            instance.target += instance.delay;

            setTimeout( tick, instance.delay + adjust );
        }
    };

    return tick( func, delay );
} );

Including the code sample above will add a new setCorrectingInterval function to the window global that can be called using the same parameters you would normally pass to setInterval. Here’s a detailed breakdown of what’s going on in the new function:

  • To track properties related to this particular interval instance, we wrap the inner tick function inside a closure construct.
  • When the function is first called (i.e. when instance.started is false), a number of properties are stored to our instance object.
  • In place of setInterval, we repeatedly call setTimeout, passing the tick function and an adjusted delay.
  • The adjusted delay is calculated by tracking both the start time of the original function call and an incremented target execution time.

To achieve the desired behavior, we can update the broken example to use setCorrectingInterval in place of setInterval.

var startTime = Date.now();
setCorrectingInterval( function() {
    console.log( ( Date.now() - startTime ) + 'ms elapsed' );
}, 1000 );

As can be seen in the image below, the number of milliseconds elapsed does not continuously increase, but instead aims to occur as closely as possible to the intended once-per-second schedule.

setCorrectingInterval adjusts to stay in sync

If you find this useful, I’ve created a more full-featured version, which includes a matching clearCorrectingInterval and adds the option to pass arguments (refer to setInterval’s method signature for more information). You can read more about it, including download instructions, on the GitHub repository.

2 thoughts on “Creating a self-correcting alternative to JavaScript’s setInterval

  1. I found your article today (four years later! Late November 2017) after going down a rabbit hole of wondering why setInterval seems to run faster in Chrome than node.js. To update your list of browsers, it looks like Chrome and Firefox use this feature, while Safari (and node) do not.

    This is a neat function. The only problem is that, as far as I can tell, it is not possible to stop such a process. You need the ID returned from setInterval in order to stop it. In this case, it is a recursive function with no base case, and so you’ll never get an ID back.

    Instead, I’m passing a control object, { run: true }, into the function, so that tick() can decide to not trigger the next setTimeout(). It works very well, but requires that extra argument.

    Overall, this post saved me many hours. I was quite deep into the rabbit hole when I found it. It’s allowing me to more easily keep clients synced with the server in an online game. Please thank your four-years-younger self for me when you get a chance. Cheers!

    Liked by 1 person

    1. Hi Daniel, thanks for the comment, I’m glad you found it useful! If I understand the problem you describe, I think this should be satisfied in the more complete `correctingInterval` project I published to GitHub / npm, which both returns an ID from `setCorrectingInterval` and includes a `clearCorrectingInterval` to allow you to stop it.

      https://github.com/aduth/correctingInterval
      https://www.npmjs.com/package/correcting-interval

      Let me know if that helps.

      Thanks also for the pointer to updated browser support list. I’m not totally surprised that the default behavior of browsers has changed in the time since this was published.

      Like

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s