如何使setInterval在Chrome浏览器的标签页不活动时也能工作?

268

我有一个setInterval在每秒钟运行30次的代码片段。这个很好用,但是当我选择另一个标签页(使得我的代码所在的标签页成为非活动状态)时,setInterval会因某些原因进入空闲状态。

我创建了这个简化的测试用例 (http://jsfiddle.net/7f6DX/3/):

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

setInterval(function() {
    a++;
    $div.css("left", a)
}, 1000 / 30);
如果您运行此代码,然后切换到另一个标签页,在等待几秒钟后返回,动画将在您切换到其他标签页时的位置继续进行。
因此,如果标签页处于非活动状态,则动画不会以每秒30次的频率运行。这可以通过计算setInterval函数每秒调用的次数来确认-如果标签页处于非活动状态,则此数字将不会是30,而只有1或2次。
我想这是出于设计考虑而这样做以提高系统性能,但是否有任何方法可以禁用此行为?
实际上,在我的场景中,这是一个缺点。

4
可能不行,除非你将它与“日期”对象一起使用,以确实看到经过了多长时间。 - alex
你能更详细地解释一下你的情况吗?也许这并不是什么劣势。 - James
2
你是指这个代码更改(http://codereview.chromium.org/6577021),你所询问的也在这里讨论(http://groups.google.com/a/chromium.org/group/chromium-dev/browse_thread/thread/ff827011f2d1f0f6)- Oliver Mattos发布了一些解决方法,也许在你的情况下也有效? - Shadow The Spring Wizard
7
最好根据动画开始后经过的实际时间(从“Date”中读取)来制作动画关键帧,这样即使间隔不快(出于任何原因),动画也只会变得更加卡顿,而不是放慢。 - bobince
1
问题的标题引导我来到这里,但我的用例有些不同——我需要刷新身份验证令牌,而不管选项卡是否处于非活动状态。如果这对您有帮助,请查看我的答案此处 - Erez Cohen
显示剩余3条评论
17个回答

183

在大多数浏览器中,非活动选项卡具有低优先级的执行顺序,这可能会影响JavaScript计时器。

如果您的过渡值是使用帧之间的实际时间差而不是每个间隔固定增量来计算的,那么您不仅可以解决此问题,还可以通过使用requestAnimationFrame实现更流畅的动画效果,因为它可以在处理器不太繁忙的情况下达到最高60fps。

以下是一个使用requestAnimationFrame的原始JavaScript示例,用于进行动画属性过渡:

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>


针对后台任务(非 UI 相关)

@UpTheCreek 评论:

处理演示问题很好,但还是有一些需要持续运行的任务。

如果您有需要精确按给定时间间隔执行的后台任务,则可以使用 HTML5 Web Workers。查看下面的 Möhre 的答案 以获取更多详细信息…

CSS vs JS "animations"

使用 CSS 转换/动画而不是基于 JavaScript 的动画可避免此问题和许多其他问题,因为后者会增加相当大的开销。我推荐使用这个 jQuery 插件 ,它使您可以像使用 animate() 方法一样利用 CSS 转换。


1
我在FireFox 5上尝试过这个,即使选项卡不在焦点中,“setInterval”仍然运行。我的“setInterval”代码使用“animate()”滑动幻灯片。看起来Firefox会将动画排队。当我回到选项卡时,Firefox立即弹出队列,一个接一个地导致快速移动的幻灯片直到队列为空。 - nthpixel
@nthpixel,根据www的回答,添加.stop是否有助于该情况(因为每次它都会清除之前的动画)? - user359135
@gordatron - .stop 对我的情况没有帮助。行为相同。现在我正在使用FireFox 6.0.2。我想知道这是否是jQuery实现.animate的方式。 - nthpixel
@cvsguimaraes - 是的,说得好。你知道规范中是否有任何保证 Web Workers 在后台标签页中运行的内容吗?我有点担心浏览器供应商将来会任意地限制这些... - UpTheCreek
我会更改代码的倒数第二行为 before = now;,以获取自上一次调用结束而不是开始后经过的时间。这通常是在动画时所需的。现在,如果间隔函数的执行需要15毫秒,则 elapsedTime 将在下一次调用时(当选项卡处于活动状态时)给出35毫秒(而不是50毫秒,即间隔时间)。 - Jonas Berlin
这不能被视为接受的答案,因为MDN文档对rAF有不同的说明。链接在这里-> US/docs/Web/API/window/requestAnimationFrame - HalfWebDev

94

我遇到了与音频淡出和HTML5播放器相同的问题。当标签变成非活动状态时,它就会卡住。 后来我发现WebWorker允许无限制地使用间隔/超时。我使用它向主JavaScript发布“ticks”。

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 代码:

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;
    }
});

13
好的!现在希望他们不会“修复”这个。 - pimvdb
3
太好了!这种方法应该在你确切需要计时器工作而不仅是为了修复一些动画问题时使用! - Konstantin Smolyanin
1
我用这个制作了一个很棒的节拍器。有趣的是,所有最受欢迎的HTML5鼓机都没有使用这种方法,而是使用较差的常规setTimeout。http://skyl.github.io/grimoire/fretboard-prototype.html - 目前来看,Chrome或Safari播放效果最好。 - Skylar Saveland
如果开发人员开始滥用它,他们会对其进行“修复”。除了极少数例外,您的应用程序并不那么重要,以至于它应该在后台消耗资源来提供一些最小的用户体验改进。 - Gunchars
@Gunchars 你说得完全正确,但如果应用程序能够在这方面询问用户的许可,那不是更好吗?如果用户愿意允许这种情况发生,为什么不呢? - Farzan

60

可以使用 Web Workers(如上所述的)解决问题,因为它们运行在单独的进程中,不会被拖慢。

我编写了一个小脚本,可以在不更改您的代码的情况下使用 - 它只是覆盖了 setTimeout、clearTimeout、setInterval 和 clearInterval 函数。

只需在所有代码之前包含它即可。

更多信息请点击这里


1
我已经在一个包含使用定时器的库的项目上进行了测试(因此我无法自己实现工作线程)。它表现得非常好。感谢这个库。 - ArnaudR
@neoDev 你是否在加载其他JavaScript之前加载HackTimer.js(或HackTimer.min.js)? - Davide Patti
我也尝试过这个库,时间是在2017年,但它在Safari 12中无法工作。我确保将其放在所有其他脚本之前。请问有人能否确认它是否仍然有效? - WindChimes
@WindChimes 我已经在Chrome中尝试了这个库,它仍然可以工作。但我不确定Safari是否可行。 - Martin Mbae
3
我有点困惑 - 在React中,你只需要使用NPM安装它,而不需要导入任何东西吗? - Louis Sankey
显示剩余4条评论

18

setIntervalrequestAnimationFrame在选项卡不活动时不起作用,或者在不正确的时间间隔内工作。解决方案是使用另一个时间事件源。例如,Web SocketsWeb Workers是两个在选项卡不活动时正常工作的事件源。因此,不需要将所有代码移到Web Worker中,只需将Worker用作时间事件源:

// 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 链接


1
只需将 Worker 用作时间事件源即可。感谢您的想法。 - Promod Sampath Elvitigala
该 jsfiddle 报错 SecurityError。 - user3187724

15

对我来说,像其他人一样在后台播放音频并不重要,我的问题是当你在其他标签页中并返回到它们时,一些动画会变得很疯狂。我的解决方法是将这些动画放在防止非活动标签页if中:

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

多亏如此,我的动画只在选项卡处于活动状态时才运行。我希望这能对遇到类似情况的人有所帮助。


1
在复杂的答案中,这个简单的解决方案拯救了我。谢谢。 - Overcomer

14

只需这样做:

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

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

浏览器中未激活的选项卡会缓冲部分setIntervalsetTimeout函数。

stop(true,true)将停止所有缓冲事件,并立即执行最后一个动画。

window.setTimeout()方法现在会限制发送不超过一次超时时间,每秒钟一次,在非活动选项卡中。此外,它现在会将嵌套超时限制为HTML5规范允许的最小值:4ms(而不是它过去限制为10ms)。


1
这是一个很好的知识点 - 例如对于 AJAX 更新,最新数据总是正确的 - 但对于所提出的问题,这是否意味着动画会显著减慢/暂停,因为 "a" 没有适当地增加相应的次数? - user359135

7
受Ruslan答案的启发,这是一个方便的小类,您可以将其复制并粘贴到您的代码中,作为setInterval的替代品:
class WorkerInterval {
  worker = null;
  constructor(callback, interval) {
    const blob = new Blob([`setInterval(() => postMessage(0), ${interval});`]);
    const workerScript = URL.createObjectURL(blob);
    this.worker = new Worker(workerScript);
    this.worker.onmessage = callback;
  }

  stop() {
    this.worker.terminate();
  }
}

使用示例:

   const interval = new WorkerInterval(callback, 100);
   // sometime later
   interval.stop();

5
我认为对于这个问题最好的理解是在这个例子中:http://jsfiddle.net/TAHDb/ 我正在做一个简单的事情:
每隔1秒隐藏第一个span并将其移动到末尾,显示第二个span。
如果您停留在页面上,则它按预期工作。但是,如果您隐藏选项卡几秒钟,当您回来时,您将看到奇怪的事情。
就像所有未在您不活动的时间内发生的事件现在都会在1次中发生一样。因此,对于一些短暂的时间,您将获得像X事件一样。它们非常快,可以同时看到所有6个spans。
因此,Chrome似乎只延迟事件,因此当您回来时,所有事件都将发生,但全部同时发生...
其中一个实际应用是用于简单的幻灯片放映。想象一下数字是图像,如果用户在隐藏选项卡时停留,当他回来时,他将看到所有图像浮动,完全混乱。
要修复此问题,请使用stop(true,true),如pimvdb所说。
这将清除事件队列。

4
实际上,这是由于jQuery使用的requestAnimationFrame导致的。由于其中一些小问题(比如这个),他们将其移除了。在1.7中,您不会看到这种行为。 http://jsfiddle.net/TAHDb/1/。因为间隔是每秒钟一次,所以实际上对我发布的问题没有影响,因为对于非活动选项卡的最大值也是每秒钟一次 - 这没有任何区别。 - pimvdb

4
我为那些试图解决计时器函数问题的人提供了一个简单的解决方案,正如@kbtzr在另一个答案中提到的,我们可以使用Date对象来计算自开始以来经过的时间,而不是固定增量,这将即使您不在应用程序选项卡中也能正常工作。
以下是示例HTML。
<body>
  <p id="time"></p>
</body>

那么这段JavaScript代码:

let display = document.querySelector('#time')
let interval
let time
function startTimer() {
    let initialTime = new Date().getTime()
    interval = setInterval(() => {
        let now = new Date().getTime()
        time = (now - initialTime) + 10
        display.innerText = `${Math.floor((time / (60 * 1000)) % 60)}:${Math.floor((time / 1000) % 60)}:${Math.floor((time / 10) % 100)}`
    }, 10)
}
startTimer()

这样,即使因为非活动标签的性能原因增加了间隔值,所做的计算也会保证正确的时间。这是一个普通的代码,但我在我的React应用程序中使用了这个逻辑,并且您也可以根据需要进行修改。


2
播放音频文件可以确保在此期间完整的后台JavaScript性能。
对我来说,这是最简单和最不具侵入性的解决方案-除了播放微弱/几乎空白的声音外,没有其他潜在的副作用。
您可以在此处找到详细信息:https://dev59.com/MW025IYBdhLWcg3wZlHd#51191818 (从其他答案中,我看到有些人使用Audio标签的不同属性,我想知道是否可能使用Audio标签实现完全性能,而无需实际播放任何内容)。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接