Comment puis-je faire en sorte que setInterval fonctionne également lorsqu'un onglet est inactif dans Chrome?

190

J'ai setIntervalun morceau de code en cours d'exécution 30 fois par seconde. Cela fonctionne très bien, cependant lorsque je sélectionne un autre onglet (afin que l'onglet avec mon code devienne inactif), le setIntervalest mis à un état inactif pour une raison quelconque.

J'ai fait ce cas de test simplifié ( http://jsfiddle.net/7f6DX/3/ ):

var $div = $('div');
var a = 0;

setInterval(function() {
    a++;
    $div.css("left", a)
}, 1000 / 30);

Si vous exécutez ce code, puis passez à un autre onglet, attendez quelques secondes et revenez en arrière, l'animation continue au point où elle se trouvait lorsque vous êtes passé à l'autre onglet. Ainsi, l'animation ne s'exécute pas 30 fois par seconde au cas où l'onglet serait inactif. Cela peut être confirmé en comptant le nombre de fois où la setIntervalfonction est appelée chaque seconde - ce ne sera pas 30 mais juste 1 ou 2 si l'onglet est inactif.

Je suppose que cela est fait par conception afin d'améliorer les performances, mais existe-t-il un moyen de désactiver ce comportement? C'est en fait un inconvénient dans mon scénario.

pimvdb
la source
3
Probablement pas, à moins que vous ne l'ayez piraté avec l' Dateobjet pour vraiment voir le temps écoulé.
alex
Pouvez-vous expliquer plus en détail votre scénario? Ce n'est peut-être pas un tel désavantage.
James
2
Vous voulez dire ce changement de code, et ce que vous demandez est également discuté ici - Oliver Mattos a posté des solutions, peut-être que c'est également valable dans votre cas?
Shadow Wizard est une oreille pour vous
6
Il est presque toujours préférable de saisir les animations en fonction du temps réel écoulé depuis le début de l'animation, tel que lu à partir de Date. Ainsi, lorsque les intervalles ne se déclenchent pas rapidement (pour cela ou pour d'autres raisons), l'animation devient plus saccadée, pas plus lente.
bobince
1
Le titre de la question m'a conduit ici, mais mon cas d'utilisation était un peu différent: j'avais besoin d'un jeton d'authentification actualisé indépendamment de l'inactivité d'un onglet. Si cela est pertinent pour vous, consultez ma réponse ici
Erez Cohen

Réponses:

153

Sur la plupart des navigateurs, les onglets inactifs ont une exécution de faible priorité, ce qui peut affecter les minuteries JavaScript.

Si les valeurs de votre transition ont été calculées en utilisant le temps réel écoulé entre les images au lieu d'incréments fixes sur chaque intervalle, vous pouvez non seulement contourner ce problème, mais également obtenir une animation plus douce en utilisant requestAnimationFrame car il peut atteindre 60fps si le processeur ne l'est pas très occupé.

Voici un exemple JavaScript vanille d'une transition de propriété animée utilisant requestAnimationFrame:

var target = document.querySelector('div#target')
var startedAt, duration = 3000
var domain = [-100, window.innerWidth]
var range = domain[1] - domain[0]

function start() {
  startedAt = Date.now()
  updateTarget(0)
  requestAnimationFrame(update)
}

function update() {
  let elapsedTime = Date.now() - startedAt

  // playback is a value between 0 and 1
  // being 0 the start of the animation and 1 its end
  let playback = elapsedTime / duration

  updateTarget(playback)
  
  if (playback > 0 && playback < 1) {
  	// Queue the next frame
  	requestAnimationFrame(update)
  } else {
  	// Wait for a while and restart the animation
  	setTimeout(start, duration/10)
  }
}

function updateTarget(playback) {
  // Uncomment the line below to reverse the animation
  // playback = 1 - playback

  // Update the target properties based on the playback position
  let position = domain[0] + (playback * range)
  target.style.left = position + 'px'
  target.style.top = position + 'px'
  target.style.transform = 'scale(' + playback * 3 + ')'
}

start()
body {
  overflow: hidden;
}

div {
    position: absolute;
    white-space: nowrap;
}
<div id="target">...HERE WE GO</div>


Pour les tâches en arrière-plan (non liées à l'interface utilisateur)

Commentaire de @UpTheCreek:

Bien pour les problèmes de présentation, mais il y a encore certaines choses dont vous avez besoin pour continuer à fonctionner.

Si vous avez des tâches d'arrière-plan qui doivent être exécutées avec précision à des intervalles donnés, vous pouvez utiliser HTML5 Web Workers . Jeter un coup d'œil à la réponse de Möhre ci-dessous pour plus de détails ...

"Animations" CSS vs JS

Ce problème et bien d'autres pourraient être évités en utilisant des transitions / animations CSS au lieu d'animations basées sur JavaScript, ce qui ajoute une surcharge considérable. Je recommanderais ce plugin jQuery qui vous permet de profiter des transitions CSS tout comme les animate()méthodes.

kbtzr
la source
1
J'ai essayé ceci sur FireFox 5 et 'setInterva'l fonctionne toujours même lorsque l'onglet n'est pas au point. Mon code dans 'setInterval' fait glisser un diaporama en utilisant 'animate ()'. Il semble que l'animation soit mise en file d'attente par FireFox. Lorsque je reviens sur l'onglet, FireFox fait apparaître la file d'attente immédiatement l'une après l'autre, ce qui entraîne un diaporama rapide jusqu'à ce que la file d'attente soit vide.
nthpixel
@nthpixel ajouterait .stop (selon la réponse de www) aide en situation de tat (comme chaque fois que cela
@gordatron - .stop n'aide pas ma situation. Même comportement. Et je suis sur FireFox 6.0.2 maintenant. Je me demande si c'est ainsi que jQuery implémente .animate.
nthpixel le
@cvsguimaraes - oui, bon point. Connaissez-vous quelque chose dans la spécification qui garantit que les web workers fonctionneront même dans un onglet d'arrière-plan? Je crains un peu que les fournisseurs de navigateurs décident arbitrairement de les
bloquer
Je changerais la prochaine ligne du code à lire before = now;pour obtenir le temps écoulé depuis le début au lieu de la fin de l'appel précédent. C'est généralement ce que vous voulez lorsque vous animez des choses. Dans l'état actuel des choses, si une exécution de la fonction d'intervalle prend 15 ms, elapsedTimeelle donnera 35 ms (au lieu de 50 ms qui est l'intervalle) la prochaine fois qu'elle sera appelée (lorsque l'onglet est actif).
Jonas Berlin
71

J'ai rencontré le même problème avec la décoloration audio et le lecteur HTML5. Il est resté bloqué lorsque l'onglet est devenu inactif. J'ai donc découvert qu'un WebWorker est autorisé à utiliser des intervalles / délais sans limitation. Je l'utilise pour poster des "ticks" sur le javascript principal.

Code WebWorkers:

var fading = false;
var interval;
self.addEventListener('message', function(e){
    switch (e.data) {
        case 'start':
            if (!fading){
                fading = true;
                interval = setInterval(function(){
                    self.postMessage('tick');
                }, 50);
            }
            break;
        case 'stop':
            clearInterval(interval);
            fading = false;
            break;
    };
}, false);

Javascript principal:

var player = new Audio();
player.fader = new Worker('js/fader.js');
player.faderPosition = 0.0;
player.faderTargetVolume = 1.0;
player.faderCallback = function(){};
player.fadeTo = function(volume, func){
    console.log('fadeTo called');
    if (func) this.faderCallback = func;
    this.faderTargetVolume = volume;
    this.fader.postMessage('start');
}
player.fader.addEventListener('message', function(e){
    console.log('fader tick');
    if (player.faderTargetVolume > player.volume){
        player.faderPosition -= 0.02;
    } else {
        player.faderPosition += 0.02;
    }
    var newVolume = Math.pow(player.faderPosition - 1, 2);
    if (newVolume > 0.999){
        player.volume = newVolume = 1.0;
        player.fader.postMessage('stop');
        player.faderCallback();
    } else if (newVolume < 0.001) {
        player.volume = newVolume = 0.0;
        player.fader.postMessage('stop');
        player.faderCallback();
    } else {
        player.volume = newVolume;
    }
});
Möhre
la source
6
Soigné! Maintenant, espérons qu'ils ne "régleront" pas cela.
pimvdb
2
Génial! Cette approche doit être utilisée lorsque vous avez exactement besoin de minuteries pour fonctionner, mais pas seulement pour résoudre certains problèmes d'animation!
Konstantin Smolyanin
1
J'ai fait un super métronome avec ça. Il est intéressant de noter que toutes les boîtes à rythmes html5 les plus populaires n'utilisent pas cette méthode et utilisent à la place le setTimeout régulier inférieur. skyl.github.io/grimoire/fretboard-prototype.html - chrome ou safari joue le meilleur, en ce moment.
Skylar Saveland
Ils le «répareront» si le développeur commence à en abuser. À de rares exceptions près, votre application n'est pas si importante qu'elle devrait consommer des ressources en arrière-plan pour fournir une amélioration minimale de l'expérience utilisateur.
Gunchars
41

Il existe une solution pour utiliser les Web Workers (comme mentionné précédemment), car ils s'exécutent dans un processus séparé et ne sont pas ralentis

J'ai écrit un petit script qui peut être utilisé sans modification de votre code - il remplace simplement les fonctions setTimeout, clearTimeout, setInterval, clearInterval.

Incluez-le juste avant tout votre code.

Plus d'infos ici

Ruslan Tushov
la source
1
Je l'ai testé sur un projet comprenant des bibliothèques qui utilisaient des minuteries (donc je ne pouvais pas implémenter des workers moi-même). Ça a marché comme sur des roulettes. Merci pour cette bibliothèque
ArnaudR
@ ruslan-tushov vient de l'essayer sur chrome 60 mais cela semble ne pas avoir d'effet du tout ... J'appelle quelques fonctions de plusieurs setTimeout pour ajouter / supprimer des éléments au DOM qui sont animés par des animations css, et je les vois figés avec un peu de retard après le changement de tabulation (comme d'habitude sans plugins)
neoDev
@neoDev Chargez-vous le HackTimer.js (ou HackTimer.min.js) avant tout autre JavaScript?
Davide Patti
@neoDev cela semble vraiment étrange, car j'ai récemment essayé et cela fonctionne
Davide Patti
@DurdenP oui j'ai essayé aussi de le mettre avant tout autre JavaScript. Peut-être parce que j'utilisais des animations css? Je me souviens aussi que j'ai essayé des webworkers mais sans chance
neoDev
13

Faites juste ceci:

var $div = $('div');
var a = 0;

setInterval(function() {
    a++;
    $div.stop(true,true).css("left", a);
}, 1000 / 30);

Les onglets de navigateur inactifs mettent en mémoire certaines des fonctions setIntervalou setTimeout.

stop(true,true) arrêtera tous les événements mis en mémoire tampon et n'exécutera immédiatement que la dernière animation.

La window.setTimeout()méthode empêche désormais d'envoyer plus d'un délai d'expiration par seconde dans les onglets inactifs. De plus, il limite désormais les délais d'attente imbriqués à la plus petite valeur autorisée par la spécification HTML5: 4 ms (au lieu des 10 ms auxquels il était habitué).

www
la source
1
C'est bon à savoir - par exemple pour les mises à jour ajax où les dernières données sont toujours correctes - mais pour la question posée, cela ne signifie pas que l'animation a été considérablement ralentie / mise en pause car "a" n'aurait pas été incrémenté le nombre de fois approprié ?
5

Je pense que la meilleure compréhension de ce problème est dans cet exemple: http://jsfiddle.net/TAHDb/

Je fais une chose simple ici:

Avoir un intervalle de 1 seconde et à chaque fois masquer le premier intervalle et le déplacer en dernier, et afficher le 2e intervalle.

Si vous restez sur la page, cela fonctionne comme prévu. Mais si vous cachez l'onglet pendant quelques secondes, lorsque vous reviendrez, vous verrez une chose bizarre.

C'est comme si tous les événements qui ne se sont pas produits pendant le temps où vous étiez inactif maintenant se produiront en une seule fois. donc pendant quelques secondes, vous obtiendrez des événements comme X. ils sont si rapides qu'il est possible de voir les 6 travées à la fois.

Donc, il semble que le chrome ne retarde que les événements, donc lorsque vous reviendrez, tous les événements se produiront mais tous à la fois ...

Une application pratique était celle-ci pour un simple diaporama. Imaginez que les nombres soient des images, et si l'utilisateur reste avec l'onglet caché à son retour, il verra toutes les images flottantes, totalement mesurées.

Pour résoudre ce problème, utilisez stop (true, true) comme indiqué par pimvdb. Cela effacera la file d'attente des événements.

1st4ck
la source
4
En fait, c'est à cause de requestAnimationFramece jQuery utilisé. En raison de certaines bizarreries (comme celle-ci), ils l'ont supprimé. Dans la version 1.7, vous ne voyez pas ce comportement. jsfiddle.net/TAHDb/1 . Parce que l'intervalle est une fois par seconde, cela n'affecte en fait pas le problème que j'ai publié, car le maximum pour les onglets inactifs est également une fois par seconde - donc cela ne fait aucune différence.
pimvdb
4

Pour moi, ce n'est pas important de jouer de l'audio en arrière-plan comme pour d'autres ici, mon problème était que j'avais des animations et elles agissaient comme des fous quand vous étiez dans d'autres onglets et y reveniez. Ma solution était de mettre ces animations à l'intérieur si cela empêche l' onglet inactif :

if (!document.hidden){ //your animation code here }

grâce à cela, mon animation ne fonctionnait que si l'onglet était actif. J'espère que cela aidera quelqu'un avec mon cas.

Tomaaszq
la source
1
Je l'utilise comme ça setInterval(() => !document.hidden && myfunc(), 1000)et ça marche parfaitement
Didi Bear
4

Les deux setIntervalet requestAnimationFramene fonctionnent pas lorsque l'onglet est inactif ou fonctionne mais pas aux bonnes périodes. Une solution consiste à utiliser une autre source pour les événements temporels. Par exemple, les sockets Web ou les travailleurs Web sont deux sources d'événements qui fonctionnent correctement lorsque l'onglet est inactif. Donc, pas besoin de déplacer tout votre code vers un web worker, utilisez simplement worker comme source d'événement temporel:

// worker.js
setInterval(function() {
    postMessage('');
}, 1000 / 50);

.

var worker = new Worker('worker.js');
var t1 = 0;
worker.onmessage = function() {
    var t2 = new Date().getTime();
    console.log('fps =', 1000 / (t2 - t1) | 0);
    t1 = t2;
}

jsfiddle lien de cet exemple.

Iman
la source
"utilisez simplement le travailleur comme source d'événements temporels" Merci pour l'idée
Promod Sampath Elvitigala
1

Fortement influencé par la bibliothèque de Ruslan Tushov, j'ai créé ma propre petite bibliothèque . Ajoutez simplement le script dans le <head>et il corrigera setIntervalet setTimeoutavec ceux qui utilisent WebWorker.

Mariy
la source
je viens de l'essayer sur chrome 60 mais cela semble ne pas avoir d'effet du tout ... J'appelle quelques fonctions de plusieurs setTimeout pour ajouter / supprimer des éléments au DOM qui sont animés par des animations css, et je les vois figés avec un certain retard après commutateur de tabulation (comme d'habitude sans plugins)
neoDev
Je l'utilise avec Chrome 60. Pouvez-vous faire une démo et la poster sur les problèmes de github?
Mariy
1

La lecture d'un fichier audio garantit pour le moment des performances Javascript complètes en arrière-plan

Pour moi, c'était la solution la plus simple et la moins intrusive - à part jouer un son faible / presque vide, il n'y a pas d'autres effets secondaires potentiels

Vous pouvez trouver les détails ici: https://stackoverflow.com/a/51191818/914546

(À partir d'autres réponses, je vois que certaines personnes utilisent différentes propriétés de la balise Audio, je me demande s'il est possible d'utiliser la balise Audio pour des performances optimales, sans réellement jouer quelque chose)

Kaan Soral
la source
0

Remarque: cette solution ne convient pas si vous aimez que votre intervalle fonctionne en arrière-plan, par exemple en jouant de l'audio ou ... mais si vous êtes confus par exemple par le fait que votre animation ne fonctionne pas correctement en revenant sur votre page (onglet) c'est une bonne solution.

Il existe de nombreuses façons d'atteindre cet objectif, peut-être que le "WebWorkers" est le plus standard mais ce n'est certainement pas le plus simple et le plus pratique, surtout si vous n'avez pas assez de temps, vous pouvez donc essayer de cette façon:

►CONCEPT DE BASE:

1- Construisez un nom pour votre intervalle (ou animation) et définissez votre intervalle (animation), afin qu'il s'exécute lorsque l'utilisateur ouvre votre page pour la première fois: var interval_id = setInterval(your_func, 3000);

2- par $(window).focus(function() {});et $(window).blur(function() {});vous pouvez à clearInterval(interval_id)chaque fois que le navigateur (onglet) est désactivé et réexécutez votre intervalle (animation) à chaque fois que le navigateur (onglet) serait réactivé parinterval_id = setInterval();

►EXEMPLE DE CODE:

var interval_id = setInterval(your_func, 3000);

$(window).focus(function() {
    interval_id = setInterval(your_func, 3000);
});
$(window).blur(function() {
    clearInterval(interval_id);
    interval_id = 0;
});
ashkan nasirzadeh
la source
0

C'est une question assez ancienne mais j'ai rencontré le même problème.
Si vous exécutez votre site Web sur Chrome, vous pouvez lire cet article Onglets d'arrière-plan dans Chrome 57 .

Fondamentalement, le minuteur d'intervalle pourrait s'exécuter s'il n'a pas épuisé le budget du minuteur.
La consommation de budget est basée sur l'utilisation du temps CPU de la tâche dans le minuteur.
Sur la base de mon scénario, je dessine une vidéo sur un canevas et je la transporte vers WebRTC.
La connexion vidéo webrtc continuerait à se mettre à jour même si l'onglet est inactif.

Cependant, vous devez utiliser à la setIntervalplace de requestAnimationFrame.
il n'est cependant pas recommandé pour le rendu de l'interface utilisateur.

Il vaudrait mieux écouter visibilityChange événement et changer le rendu du mécénisme en conséquence.

De plus, vous pouvez essayer @ kaan-soral et cela devrait fonctionner sur la base de la documentation.

鄭元傑
la source
-1

Voici ma solution approximative

(function(){
var index = 1;
var intervals = {},
    timeouts = {};

function postMessageHandler(e) {
    window.postMessage('', "*");

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

    sysFunc._each.call(timeouts, function(ind, obj) {
        var targetTime = obj[1];

        if (now >= targetTime) {
            obj[0]();
            delete timeouts[ind];
        }
    });
    sysFunc._each.call(intervals, function(ind, obj) {
        var startTime = obj[1];
        var func = obj[0];
        var ms = obj[2];

        if (now >= startTime + ms) {
            func();
            obj[1] = new Date().getTime();
        }
    });
}
window.addEventListener("message", postMessageHandler, true);
window.postMessage('', "*");

function _setTimeout(func, ms) {
    timeouts[index] = [func, new Date().getTime() + ms];
    return index++;
}

function _setInterval(func, ms) {
    intervals[index] = [func, new Date().getTime(), ms];
    return index++;
}

function _clearInterval(ind) {
    if (intervals[ind]) {
        delete intervals[ind]
    }
}
function _clearTimeout(ind) {
    if (timeouts[ind]) {
        delete timeouts[ind]
    }
}

var intervalIndex = _setInterval(function() {
    console.log('every 100ms');
}, 100);
_setTimeout(function() {
    console.log('run after 200ms');
}, 200);
_setTimeout(function() {
    console.log('closing the one that\'s 100ms');
    _clearInterval(intervalIndex)
}, 2000);

window._setTimeout = _setTimeout;
window._setInterval = _setInterval;
window._clearTimeout = _clearTimeout;
window._clearInterval = _clearInterval;
})();
Tengiz
la source
Le postMessageHandlerinvoque son propre événement qu'il gère, ayant ainsi une boucle infinie qui ne bloque pas l'interface utilisateur. Et pendant qu'il boucle à l'infini, il vérifie avec chaque gestion d'événement s'il y a une fonction de temporisation ou d'intervalle à exécuter.
foobored
-1

J'ai pu appeler ma fonction de rappel à au moins 250 ms en utilisant une balise audio et en gérant son événement ontimeupdate. Son appelé 3-4 fois en une seconde. C'est mieux qu'une seconde ensemble de retard

user7356139
la source