Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

setTimeout/setInterval 1000ms lag in background tabs (Chrome and Firefox)

When events are queued with setTimeout/setInterval, and the user is viewing a separate tab, Chrome and Firefox enforce a minimum 1000ms lag before the event is executed. This article details the behaviour.

This has been discussed on StackOverflow previously, but the questions and answers only applied to animations. Obviously, an animation can just be forced to update to the latest state when a user re-enters the tab.

But the solution does not work for sequenced audio. I have Web Audio API playing several audio files in sequence, and setTimeout is used to countdown to when the next audio file plays. If you put the tab in the background, you get an annoying 1 second gap between each pattern -- an extreme flaw in an API designed for advanced audio.

You can witness this behaviour in various HTML5 sequencers, e.g. with PatternSketch -- just by entering a pattern, playing, and going to another tab.

So I'm in need of a workaround: a way to queue events without the 1000ms clamp. Does anyone know of a way?

  • The only solution I can think of is to have window.postMessage run every single millisecond and check each time if the event is to execute. That is definitely detrimental to performance. Is this the only option?

  • Apparently there is no event system planned for Web Audio API, so that is out of question.

like image 941
FutureNature Avatar asked Oct 20 '13 09:10

FutureNature


1 Answers

EDIT: Another answer is to use WebWorkers per https://stackoverflow.com/a/12522580/1481489 - this answer is a little specific, so here's something more generic:

interval.js

var intervalId = null;
onmessage = function(event) {
    if ( event.data.start ) {
        intervalId = setInterval(function(){
            postMessage('interval.start');
        },event.data.ms||0);
    }
    if ( event.data.stop && intervalId !== null ) {
        clearInterval(intervalId);
    }
};

and your main program:

var stuff = { // your custom class or object or whatever...
    first: Date.now(),
    last: Date.now(),
    callback: function callback() {
        var cur = Date.now();
        document.title = ((cur-this.last)/1000).toString()+' | '+((cur-this.first)/1000).toString();
        this.last = cur;
    }
};

var doWork = new Worker('interval.js');
doWork.onmessage = function(event) {
    if ( event.data === 'interval.start' ) {
        stuff.callback(); // queue your custom methods in here or whatever
    }
};
doWork.postMessage({start:true,ms:250}); // tell the worker to start up with 250ms intervals
// doWork.postMessage({stop:true}); // or tell it just to stop.

Totally ugly, but you could open up a child popup window. However, all this does is transfer some of the caveats to the child window, i.e. if child window is minimized the 1000ms problem appears, but if it is simply out of focus, there isn't an issue. Then again, if it is closed, then it stops, but all the user has to do is click the start button again.

So, I suppose this doesn't really solve your problem... but here's a rough draft:

var mainIntervalMs = 250;

var stuff = { // your custom class or object or whatever...
    first: Date.now(),
    last: Date.now(),
    callback: function callback(){
        var cur = Date.now();
        document.title = ((cur-this.last)/1000).toString()+' | '+((cur-this.first)/1000).toString();
        this.last = cur;
    }
};

function openerCallbackHandler() {
    stuff.callback(); // queue your custom methods in here or whatever
}

function openerTick(childIntervalMs) { // this isn't actually used in this window, but makes it easier to embed the code in the child window 
    setInterval(function() {
        window.opener.openerCallbackHandler();
    },childIntervalMs);
}

// build the popup that will handle the interval
function buildIntervalWindow() {
    var controlWindow = window.open('about:blank','controlWindow','width=10,height=10');
    var script = controlWindow.document.createElement('script');
    script.type = 'text/javascript';
    script.textContent = '('+openerTick+')('+mainIntervalMs+');';
    controlWindow.document.body.appendChild(script);
}

// write the start button to circumvent popup blockers
document.write('<input type="button" onclick="buildIntervalWindow();return false;" value="Start" />');

I'd recommend working out a better way to organize, write, etc. but at the least it should point you in the right direction. It should also work in a lot of diff browsers (in theory, only tested in chrome). I'll leave you to the rest.

Oh, and don't forget to build in auto-closing of the child window if the parent drops.

like image 75
zamnuts Avatar answered Sep 23 '22 06:09

zamnuts