What Is the Reason JavaScript Settimeout Is So Inaccurate

What is the reason JavaScript setTimeout is so inaccurate?

It's not supposed to be particularly accurate. There are a number of factors limiting how soon the browser can execute the code; quoting from MDN:

In addition to "clamping", the timeout can also fire later when the page (or the OS/browser itself) is busy with other tasks.

In other words, the way that setTimeout is usually implemented, it is just meant to execute after a given delay, and once the browser's thread is free to execute it.

However, different browsers may implement it in different ways. Here are some tests I did:

var date = new Date();
setTimeout(function(e) {
var currentDate = new Date();
console.log(currentDate-date);
}, 1000);

// Browser Test1 Test2 Test3 Test4
// Chrome 998 1014 998 998
// Firefox 1000 1001 1047 1000
// IE 11 1006 1013 1007 1005

Perhaps the < 1000 times from Chrome could be attributed to inaccuracy in the Date type, or perhaps it could be that Chrome uses a different strategy for deciding when to execute the code—maybe it's trying to fit it into the a nearest time slot, even if the timeout delay hasn't completed yet.

In short, you shouldn't use setTimeout if you expect reliable, consistent, millisecond-scale timing.

setTimeout timing precision

Js has 3 microtask queues, these are(were) setTimeout/Interval/Immediate (some people call these macrotask, etc whatever), requestAnimationFrame (rAF) and the new child Promises. Promises resolve asap, setTimeouts have 4ms min difference between successive invocations if they are nested (& more than 5 layers deep), rAF will execute around 60 frames per second.

Amongst these rAF is aware of document.hidden state and roughly executes every ~17ms (16.67 theoretically). If your desired intervals are larger than this value, settle with rAF.

The problem with rAF is, since it executes every ~17ms, if I would want to execute something with 100 ms intervals,then after 5 ticks I would be at ~85ms, at the sixth tick I'd be at 102ms. I can execute at 102ms, but then I need to drop down 2ms from the next invocation time. This will prevent accidental 'phasing out' of the callback with respect to the frames you specify. You can roughly design a function that accepts an options object:

function wait(t,options){
if(!options){
options = t;
window.requestAnimationFrame(function(t){
wait(t,options);
});
return options;
}
if(!options._set) {
options.startTime = options.startTime || t;
options.relativeStartTime = options.startTime;
options.interval = options.interval || 50;
options.frame = options.frame || 0;
options.callback = options.callback || function(){};
options.surplus = options.surplus || 0;
options._set = true;
}
options.cancelFrame = window.requestAnimationFrame(function(t){
wait(t,options);
});
options.elapsed = t - options.relativeStartTime + options.surplus;
if (options.elapsed >= options.interval) {
options.surplus = options.elapsed % options.interval;
options.lastInvoked = t;
options.callback.call(options);
options.frame++;
options.relativeStartTime = t;
}
return options;
}

The object gets recycled and updated at every invocation. Copy paste to your console the above and try:

var x = wait({interval:190,callback:function(){console.log(this.lastInvoked - this.relativeStartTime)}})

The callback executes with this pointing to the options object. The returned x is the options object itself. To cancel it from running:

window.cancelAnimationFrame(x.cancelFrame);

This doesn't always have to act like interval, you can also use it like setTimeout. Let's say you have variable frames with multiples of 32 as you said,in that case extend the options object:

var x = wait({frameList:[32,32,64,128,256,512,1024,2048,32,128],interval:96,callback:function(){
console.log(this.lastInvoked - this.relativeStartTime);
window.cancelAnimationFrame(this.cancelFrame);
this.interval = this.frameList[this.frame];
if(this.interval){
wait(this);
}
}})

I added a frameList key to the options object. Here are some time values that we want to execute the callback. We start with 96, then go inside the frameList array, 32,32, 64 etc. If you run the above you'll get:

99.9660000000149
33.32199999992736
33.32199999992736
66.64400000008754
133.28799999994226
249.91499999980442
517.7960000000894
1016.5649999999441
2049.7950000001583
33.330000000074506
133.31999999983236

So these are my thoughts about what I'd do in your situation.

it runs as close as possible to the specified interval. If you put very close intervals such as 28,30,32 you will not be able to inspect the difference by eye. perhaps try console logging the 'surplus' values like this:

var x = wait({interval:28,callback:function(){
console.log(this.surplus);
}})

You will see slightly different numbers for different intervals, and these numbers will shift in time, because we are preventing 'phasing out'. The ultimate test would be to look at the average time takes for certain amount of 'frames':

var x = wait({interval:28,callback:function(){
if(this.frame === 99){
console.log(this.lastInvoked - this.startTime);
window.cancelAnimationFrame(this.cancelFrame);
}
}}) //logs something around 2800

if you change the interval to 32 for instance, it will log something around 3200ms etc. In conclusion, the function we design should not depend on what the real time is, it should get from the js engine which frame we are currently at, and should base its pace on that.

setTimeout() triggers a little bit earlier than expected

From the NodeJS docs:

The callback will likely not be invoked in precisely delay milliseconds. Node.js makes no guarantees about the exact timing of when callbacks will fire, nor of their ordering. The callback will be called as close as possible to the time specified.

As you increase the number of intervals (you have 100) the accuracy decreases, e.g., with 1000 intervals accuracy is even worse. With ten it's much better. As NodeJS has to track more intervals its accuracy will decrease.

We can posit the algorithm has a "reasonable delta" that determines final accuracy, and it does not include checking to make sure it's after the specified interval. That said, it's easy enough to find out with some digging in the source.

See also How is setTimeout implemented in node.js, which includes more details, and preliminary source investigation seems to confirm both this, and the above.

Javascript - Why does measuring time becomes inaccurate in a iterating function?

It's not supposed to be very accurate as you might expect since the timeout can fire an event later when the page is busy with other tasks.

setTimeout is implemented in a way it's meant to execute after a minimum given delay, and once the browser's thread is free to execute it. so, for an example, if you specify a value of 0 for the delay parameter and you think it will execute "immediately", it won't. it will, more accurately, run in the next event cycle (which is part of the event loop - concurrency model which is responsible for executing the code).

So for conclusion, you can't use setTimeout if you expect a consistent, reliable
and accurate timing in scale of millisecond.

Please read reason for delays longer then specified - https://developer.mozilla.org/en-US/docs/Web/API/setTimeout#reasons_for_delays_longer_than_specified

More about the JS event loop - https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop

How to fix setTimeout/requestAnimationFrame accuracy

When you do calculate the current progress of your timer, you are not taking the pause time into consideration. Hence the jumps: This part of your code is only aware of the startTime and currentTime, it won't be affected by the pauses.

To circumvent it, you can either accumulate all this pause times in the startTimer function

class Timer {  constructor() {    this.progress = 0;    this.totalPauseDuration = 0;    const d = this.timerFinishesAt = new Date(Date.now() + 10000);    this.timerStarted = new Date();    this.timerPausedAt = new Date();  }  timerStart() {    const pauseDuration = (Date.now() - this.timerPausedAt.getTime())
this.totalPauseDuration += pauseDuration;
// new future date = future date + elapsed time since pausing this.timerFinishesAt = new Date(this.timerFinishesAt.getTime() + pauseDuration); // set new timeout this.timerId = window.setTimeout(this.toggleVisibility.bind(this), (this.timerFinishesAt.getTime() - Date.now())); // animation start this.progressId = requestAnimationFrame(this.progressBar.bind(this)); } timerPause() { // stop notification from closing window.clearTimeout(this.timerId); // set to null so animation won't stay in a loop this.timerId = null; // stop loader animation from progressing cancelAnimationFrame(this.progressId); this.progressId = null;
this.timerPausedAt = new Date(); } progressBar() { if (this.progress < 100) { let elapsed = (Date.now() - this.timerStarted.getTime()) - this.totalPauseDuration; let wholeTime = this.timerFinishesAt.getTime() - this.timerStarted.getTime(); this.progress = Math.ceil((elapsed / wholeTime) * 100); log.textContent = this.progress; if (this.timerId) { this.progressId = requestAnimationFrame(this.progressBar.bind(this)); }
} else { this.progressId = cancelAnimationFrame(this.progressId); } } toggleVisibility() { console.log("done"); }};
const timer = new Timer();
btn.onclick = e => { if (timer.timerId) timer.timerPause(); else timer.timerStart();};
<pre id="log"></pre>
<button id="btn">toggle</button>

Is there ever a good reason to pass a string to setTimeout?

You can always use global variables by accessing them as properties of the window object, like window.globalVar (though using globals is indeed not a good practice), so no, I don't think there is ever a good reason to use the deprecated syntax.

It is probably allowed for historical reasons: as Felix Kling mentioned, the original syntax did only allow to pass a string of code:

Introduced with JavaScript 1.0, Netscape 2.0. Passing a Function object reference was introduced with JavaScript 1.2, Netscape 4.0; supported by the MSHTML DOM since version 5.0. [source, my emphasis]

If browsers don't support the use of a string as first argument to setTimeout and setInterval anymore, there will be lots of code on the internet that doesn't function anymore.

setInterval will become unreliable and inaccurate sometimes

setTimeout and setInterval in javascript only promise that they won't be run before the specific time. they try to execute your function as soon as possible but they sometimes can't do that. They are not timers. They are some functions that call a callback after a time which is the minimum amount of waiting before calling the callback function. Unfortunately there is not an accurate timer in javascript. you can implement it yourself.

Edit: you may ask how:

read the following thread:
https://stackoverflow.com/a/29972322/12337783



Related Topics



Leave a reply



Submit