79

Something that has always bugged me is how unpredictable the setTimeout() method in Javascript is.

In my experience, the timer is horribly inaccurate in a lot of situations. By inaccurate, I mean the actual delay time seems to vary by 250-500ms more or less. Although this isn't a huge amount of time, when using it to hide/show UI elements the time can be visibly noticeable.

Are there any tricks that can be done to ensure that setTimeout() performs accurately (without resorting to an external API) or is this a lost cause?

Dan Herbert
  • 99,428
  • 48
  • 189
  • 219
  • There is! You should check precision-timeout-interval package on npm. And also you can create virtual intervals / timeouts via a single timer, check: https://github.com/ufukbakan/virtual-intervals – Ufuk Bakan Jun 05 '23 at 08:38

16 Answers16

81

Are there any tricks that can be done to ensure that setTimeout() performs accurately (without resorting to an external API) or is this a lost cause?

No and no. You're not going to get anything close to a perfectly accurate timer with setTimeout() - browsers aren't set up for that. However, you don't need to rely on it for timing things either. Most animation libraries figured this out years ago: you set up a callback with setTimeout(), but determine what needs to be done based on the value of (new Date()).milliseconds (or equivalent). This allows you to take advantage of more reliable timer support in newer browsers, while still behaving appropriately on older browsers.

It also allows you to avoid using too many timers! This is important: each timer is a callback. Each callback executes JS code. While JS code is executing, browser events - including other callbacks - are delayed or dropped. When the callback finishes, additional callbacks must compete with other browser events for a chance to execute. Therefore, one timer that handles all pending tasks for that interval will perform better than two timers with coinciding intervals, and (for short timeouts) better than two timers with overlapping timeouts!

Summary: stop using setTimeout() to implement "one timer / one task" designs, and use the real-time clock to smooth out UI actions.

Shog9
  • 156,901
  • 35
  • 231
  • 235
  • How about calling setTimeout() from a worker. Wouldn't it reduce blocking caused by rendering? – hidarikani Nov 17 '14 at 15:49
  • There's no guarantee of that, @hidarikani. FWIW, this can be just as much of a problem in native code, and in the browser you don't have the ability to demand high-resolution timers and real-time latency. Not that you'd want to; that's a recipe for wasting CPU, and you should assume that at least in some cases the browser and OS will be doing everything they can to preserve battery life and sacrificing your timer accuracy for that goal. – Shog9 Nov 17 '14 at 20:31
  • I think that this answer is now wrong actually, since we got `requestAnimationFrame` now... See also [this answer](http://stackoverflow.com/a/16103377/286685) by [Chris](http://stackoverflow.com/users/1818728/chris-gw-green). It's not that simple a solution, but it satisfies the OP's criteria of no external API. – Stijn de Witt Oct 29 '15 at 09:41
  • 1
    requestAnimationFrame offers greater *precision*, but not necessarily better accuracy @StijndeWitt - you're still at the mercy of the browser, which could be stalled by other processing or even by whatever choices you make when implementing in your callback. Chris's technique is fine, but if you wish to ensure that logic is performed as close as possible to a set time, you'll still need to decide what is due based on the value of the retrieved time; requestAnimationFrame offers even less of a guarantee as to when it will be called than setTimeout does. Also see my previous note to hidarikani. – Shog9 Feb 01 '16 at 01:35
  • "requestAnimationFrame offers even less of a guarantee" That's a very good point! Thank you – Stijn de Witt Feb 02 '16 at 09:56
36

.

REF; http://www.sitepoint.com/creating-accurate-timers-in-javascript/

This site bailed me out on a major scale.

You can use the system clock to compensate for timer inaccuracy. If you run a timing function as a series of setTimeout calls — each instance calling the next — then all you have to do to keep it accurate is work out exactly how inaccurate it is, and subtract that difference from the next iteration:

var start = new Date().getTime(),  
    time = 0,  
    elapsed = '0.0';  
function instance()  
{  
    time += 100;  
    elapsed = Math.floor(time / 100) / 10;  
    if(Math.round(elapsed) == elapsed) { elapsed += '.0'; }  
    document.title = elapsed;  
    var diff = (new Date().getTime() - start) - time;  
    window.setTimeout(instance, (100 - diff));  
}  
window.setTimeout(instance, 100);  

This method will minimize drift and reduce the inaccuracies by more than 90%.

It fixed my issues, hope it helps

user1213320
  • 650
  • 1
  • 6
  • 13
  • 1
    @Mr_Chimp, why do you need `elapsed` variable in your code? – Anton Rudeshko Nov 22 '14 at 09:08
  • 1
    @AntonRudeshko Huh...good question. My code was adapted from the link above. [I *think* it's just a useless leftover](https://github.com/mrchimp/tock/issues/17). – Mr_Chimp Nov 24 '14 at 12:01
  • 2
    The `doTimer` function at the end of that article is exactly what I needed. It's super accurate. – Luc Feb 07 '16 at 18:38
11

I had a similar problem not long ago and came up with an approach which combines requestAnimationFrame with performance.now() which works very effectively.

Im now able to make timers accurate to approx 12 decimal places:

    window.performance = window.performance || {};
    performance.now = (function() {
        return performance.now       ||
            performance.mozNow    ||
            performance.msNow     ||
            performance.oNow      ||
            performance.webkitNow ||
                function() {
                    //Doh! Crap browser!
                    return new Date().getTime(); 
                };
        })();

http://jsfiddle.net/CGWGreen/9pg9L/

ErikE
  • 48,881
  • 23
  • 151
  • 196
Chris GW Green
  • 1,145
  • 9
  • 17
5

If you need to get an accurate callback on a given interval, this gist may help you:

https://gist.github.com/1185904

function interval(duration, fn){
  var _this = this
  this.baseline = undefined
  
  this.run = function(){
    if(_this.baseline === undefined){
      _this.baseline = new Date().getTime()
    }
    fn()
    var end = new Date().getTime()
    _this.baseline += duration
 
    var nextTick = duration - (end - _this.baseline)
    if(nextTick<0){
      nextTick = 0
    }
    
    _this.timer = setTimeout(function(){
      _this.run(end)
    }, nextTick)
  }

  this.stop = function(){
    clearTimeout(_this.timer)
  }
}
Félix Paradis
  • 5,165
  • 6
  • 40
  • 49
manast
  • 96
  • 1
  • 3
4

shog9's answer is pretty much what I'd say, although I'd add the following about UI animation/events:

If you've got a box that's supposed to slide onto the screen, expand downwards, then fade in its contents, don't try to make all three events separate with delays timed to make them fire one after another - use callbacks, so once the first event is done sliding it calls the expander, once that's done it calls the fader. jQuery can do it easily, and I'm sure other libraries can as well.

matt lohkamp
  • 2,174
  • 3
  • 28
  • 47
3

If you're using setTimeout() to yield quickly to the browser so it's UI thread can catch up with any tasks it needs to do (such as updating a tab, or to not show the Long Running Script dialog), there is a new API called Efficient Script Yielding, aka, setImmediate() that may work a bit better for you.

setImmediate() operates very similarly to setTimeout(), yet it may run immediately if the browser has nothing else to do. In many situations where you are using setTimeout(..., 16) or setTimeout(..., 4) or setTimeout(..., 0) (i.e. you want the browser to run any outstanding UI thread tasks and not show a Long Running Script dialog), you can simply replace your setTimeout() with setImmediate(), dropping the second (millisecond) argument.

The difference with setImmediate() is that it is basically a yield; if the browser has sometime to do on the UI thread (e.g., update a tab), it will do so before returning to your callback. However, if the browser is already all caught up with its work, the callback specified in setImmediate() will essentially run without delay.

Unfortunately it is only currently supported in IE9+, as there is some push back from the other browser vendors.

There is a good polyfill available though, if you want to use it and hope the other browsers implement it at some point.

If you are using setTimeout() for animation, requestAnimationFrame is your best bet as your code will run in-sync with the monitor's refresh rate.

If you are using setTimeout() on a slower cadence, e.g. once every 300 milliseconds, you could use a solution similar to what user1213320 suggests, where you monitor how long it was from the last timestamp your timer ran and compensate for any delay. One improvement is that you could use the new High Resolution Time interface (aka window.performance.now()) instead of Date.now() to get greater-than-millisecond resolution for the current time.

NicJ
  • 4,070
  • 1
  • 25
  • 18
2

Here's an example demoing Shog9's suggestion. This fills a jquery progress bar smoothly over 6 seconds, then redirects to a different page once it's filled:

var TOTAL_SEC = 6;
var FRAMES_PER_SEC = 60;
var percent = 0;
var startTime = new Date().getTime();

setTimeout(updateProgress, 1000 / FRAMES_PER_SEC);

function updateProgress() {
    var currentTime = new Date().getTime();

    // 1000 to convert to milliseconds, and 100 to convert to percentage
    percent = (currentTime - startTime) / (TOTAL_SEC * 1000) * 100;

    $("#progressbar").progressbar({ value: percent });

    if (percent >= 100) {
        window.location = "newLocation.html";
    } else {
        setTimeout(updateProgress, 1000 / FRAMES_PER_SEC);
    }                 
}
Dean
  • 8,632
  • 6
  • 45
  • 61
2

You need to "creep up" on the target time. Some trial and error will be necessary but in essence.

Set a timeout to complete arround 100ms before the required time

make the timeout handler function like this:

calculate_remaining_time
if remaining_time > 20ms // maybe as much as 50
  re-queue the handler for 10ms time
else
{
  while( remaining_time > 0 ) calculate_remaining_time;
  do_your_thing();
  re-queue the handler for 100ms before the next required time
}

But your while loop can still get interrupted by other processes so it's still not perfect.

Noel Walters
  • 1,843
  • 1
  • 14
  • 20
  • This isn't a terrible idea, assuming you need to hit an exact wall-clock time *on the dot*. But if you're just after a consistent interval, you can achieve similar results by simply adjusting the timeout value on the fly to something just shy of the interval period less the time taken by your processing. And even for something like a reminder or ticking clock, 15ms accuracy is probably good enough in most cases. – Shog9 Sep 07 '10 at 18:33
  • The only time I've needed such accuracy was trying to implement a musical beat. But Javascript timers are not really suitable for the job. Even with this design the ticking is quite erratic and not really suitable for a metronome. – Noel Walters Sep 07 '10 at 23:58
1

This is a timer I made for a music project of mine which does this thing. Timer that is accurate on all devices.

var Timer = function(){
  var framebuffer = 0,
  var msSinceInitialized = 0,
  var timer = this;

  var timeAtLastInterval = new Date().getTime();

  setInterval(function(){
    var frametime = new Date().getTime();
    var timeElapsed = frametime - timeAtLastInterval;
    msSinceInitialized += timeElapsed;
    timeAtLastInterval = frametime;
  },1);

  this.setInterval = function(callback,timeout,arguments) {
    var timeStarted = msSinceInitialized;
    var interval = setInterval(function(){
      var totaltimepassed = msSinceInitialized - timeStarted;
      if (totaltimepassed >= timeout) {
        callback(arguments);
        timeStarted = msSinceInitialized;
      }
    },1);

    return interval;
  }
}

var timer = new Timer();
timer.setInterval(function(){console.log("This timer will not drift."),1000}
Code Whisperer
  • 22,959
  • 20
  • 67
  • 85
  • Turns out your code drifts about 116 milliseconds per 100 seconds: I added the following before calling timer.setInterval(): var startTime = new Date().getTime(); and added: var now = new Date().getTime(); var ms = (now - startTime) + 'ms'; inside the callback, and the result was: This timer WILL drift. ms from start: 100116ms – TriumphST Jun 10 '16 at 00:45
0

Hate to say it, but I don't think there is a way to alleviate this. I do think that it depends on the client system, though, so a faster javascript engine or machine may make it slightly more accurate.

Eric Wendelin
  • 43,147
  • 9
  • 68
  • 92
  • I have a dual core 2.8 Ghz machine and the timers are still pretty inaccurate. In my experience, CPU speed doesn't matter very much. – Dan Herbert Oct 12 '08 at 21:24
0

To my experience it is lost effort, even as the smallest reasonable amount of time I ever recognized js act in is around 32-33 ms. ...

roenving
  • 2,560
  • 14
  • 14
0

There is definitely a limitation here. To give you some perspective, the Chrome browser Google just released is fast enough that it can execute setTimeout(function() {}, 0) in 15-20 ms whereas older Javascript engines took hundreds of milliseconds to execute that function. Although setTimeout uses milliseconds, no javascript virtual machine at this point in time can execute code with that precision.

Bialecki
  • 30,061
  • 36
  • 87
  • 109
  • I heard Chrome is the first browser to be that accurate, however I still haven't been able to test that. – Dan Herbert Oct 12 '08 at 20:54
  • 2
    You're a little bit off: 15ms is the standard timer granularity on Windows, and therefore the granularity for timers in most browsers *running* on Windows. Chrome is apparently using more accurate hardware timers to provide better granularity. JS execution speed is a separate concern. – Shog9 Oct 12 '08 at 21:50
  • Chrome has got bugs for this -- increasing the setTimeout resolution breaks certain websites that assume 15ms resolution, and destroys battery life on windows (windows architecture apparently fights decent resolution timers) – olliej Oct 12 '08 at 22:16
0

Dan, from my experience (that includes implementation of SMIL2.1 language in JavaScript, where time management is in subject) I can assure you that you actually never need high precision of setTimeout or setInterval.

What does however matter is the order in which setTimeout/setInterval gets executed when queued - and that always works perfectly.

Sergey Ilinsky
  • 31,255
  • 9
  • 54
  • 56
  • While I never really NEED high precision timing, it can be bothersome when the delay I set for something UI dependent is slightly off. It doesn't break anything, but it can be noticed since it is sort of inconsistent. – Dan Herbert Oct 12 '08 at 21:27
  • Shog9 provided full insight into what I wrapped with simple statements. And yes, it is not "browser" or "javascript implementation" that cannot ensure timing, it is the OS, people, world etc. Thus rather rely on "happening" then on "timing". – Sergey Ilinsky Oct 12 '08 at 21:52
0

JavaScript timeouts have a defacto limit of 10-15ms (I'm not sure what you're doing to get 200ms, unless you're doing 185ms of actual js execution). This is due to windows having a standard timer resolution of 15ms, the only way to do better is to use Windows' higher resolution timers which is a system wide setting so can screw with other applications on the system and also chews battery life (Chrome has a bug from Intel on this issue).

The defacto standard of 10-15ms is due to people using 0ms timeouts on websites but then coding in a way that assumes that assumes a 10-15ms timeout (eg. js games which assume 60fps but ask 0ms/frame with no delta logic so the game/site/animation goes a few orders of magnitude faster than intended). To account for that, even on platforms that don't have windows' timer problems, the browsers limit timer resolution to 10ms.

olliej
  • 35,755
  • 9
  • 58
  • 55
0

Here are what I use. Since it's JavaScript, I will post both my Frontend and node.js solutions:

For both, I use the same decimal rounding function that I highly recommend you keep at arms length because reasons:

const round = (places, number) => +(Math.round(number + `e+${places}`) + `e-${places}`)

places - Number of decimal places at which to round, this should be safe and should avoid any issues with floats (some numbers like 1.0000000000005~ can be problematic). I Spent time researching the best way to round decimals provided by high-resolution timers converted to milliseconds.

that + symbol - It is a unary operator that converts an operand into a number, virtually identical to Number()

Browser

const start = performance.now()

// I wonder how long this comment takes to parse

const end = performance.now()

const result = (end - start) + ' ms'

const adjusted = round(2, result) // see above rounding function

node.js

// Start timer
const startTimer = () => process.hrtime()

// End timer
const endTimer = (time) => {
    const diff = process.hrtime(time)
    const NS_PER_SEC = 1e9
    const result = (diff[0] * NS_PER_SEC + diff[1])
    const elapsed = Math.round((result * 0.0000010))
    return elapsed
}

// This end timer converts the number from nanoseconds into milliseconds;
// you can find the nanosecond version if you need some seriously high-resolution timers.

const start = startTimer()

// I wonder how long this comment takes to parse

const end = endTimer(start)

console.log(end + ' ms')
agm1984
  • 15,500
  • 6
  • 89
  • 113
-1

You could consider using the html5 webaudio clock which uses the system time for better accuracy

Dan Herbert
  • 99,428
  • 48
  • 189
  • 219
ejectamenta
  • 1,047
  • 17
  • 20