当标签页在 Chrome 中处于非活动状态时,如何使 setInterval 也能正常工作?

How can I make setInterval also work when a tab is inactive in Chrome?

提问人:pimvdb 提问时间:5/8/2011 最后编辑:Giacomo1968pimvdb 更新时间:7/30/2023 访问量:212224

问:

我每秒运行一段代码 30 次。这很好用,但是当我选择另一个选项卡(以便包含我的代码的选项卡变为非活动状态)时,由于某种原因将其设置为空闲状态。setIntervalsetInterval

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

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

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

如果运行此代码,然后切换到另一个选项卡,请等待几秒钟并返回,动画将继续在切换到另一个选项卡时的位置。

因此,如果选项卡处于非活动状态,动画不会每秒运行 30 次。这可以通过计算每秒调用函数的次数来确认 - 如果选项卡处于非活动状态,则不会是 30 次,而只是 1 或 2 次。setInterval

我想这是为了提高系统性能而设计完成的,但是有什么方法可以禁用此行为吗?

在我的场景中,这实际上是一个缺点。

javascript google-chrome setinterval

评论

4赞 alex 5/8/2011
可能不会,除非你把它和对象一起砍掉,以真正看到时间已经过去了。Date
0赞 James 5/8/2011
您能详细解释一下您的方案吗?也许这不是一个缺点。
2赞 Shadow Wizard Is Sad And Angry 5/8/2011
您的意思是代码更改,并且您提出的问题也在此处进行了讨论 - Oliver Mattos 发布了一些解决方法,也许它也适用于您的情况?
7赞 bobince 5/8/2011
几乎总是最好根据自动画开始以来经过的实时时间量来键控动画,如从 中读取的那样。因此,当间隔不能快速触发时(由于这个或其他原因),动画只会变得更生涩,而不是变慢。Date
1赞 Erez Cohen 10/23/2017
问题的标题将我带到了这里,但是我的用例有点不同 - 无论选项卡是否处于非活动状态,我都需要刷新身份验证令牌。如果这与你有关,请在此处查看我的答案

答:

183赞 25 revs, 4 users 97%kbtzr #1

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

如果转换值是使用帧之间经过的实时时间而不是每个间隔的固定增量计算的,则您不仅可以解决此问题,还可以使用 requestAnimationFrame 实现窒息动画,因为如果处理器不是很忙,它可以达到 60fps。

下面是一个普通的 JavaScript 示例,使用: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>


对于后台任务(与 UI 无关)

@UpTheCreek评论:

对于演示问题很好,但仍然 有些事情你需要继续运行。

如果您有需要以给定时间间隔精确执行的后台任务,则可以使用 HTML5 Web Worker。看看下面的 Möhre 的回答,了解更多详情......

CSS 与 JS“动画”

这个问题和许多其他问题可以通过使用 CSS 过渡/动画而不是基于 JavaScript 的动画来避免,这会增加相当大的开销。我推荐这个jQuery插件,让你从CSS转换中受益,就像方法一样。animate()

评论

1赞 nthpixel 8/27/2011
我在 FireFox 5 上尝试过这个,即使选项卡未对焦,'setInterva'l 仍然运行。我在“setInterval”中的代码使用“animate()”滑动滑动。看起来动画被 FireFox 排队了。当我返回选项卡时,FireFox 会立即一个接一个地弹出队列,从而产生快速移动的幻灯片,直到队列为空。
0赞 9/26/2011
@nthpixel添加 .stop(根据 www 的答案)会在 tat 情况下有所帮助(因为每次它都会清除以前的动画)
0赞 nthpixel 9/30/2011
@gordatron - .stop 对我的情况没有帮助。相同的行为。我现在使用的是 FireFox 6.0.2。我想知道这是否是jQuery实现.animate的方式。
0赞 UpTheCreek 3/4/2013
@cvsguimaraes - 是的,好点子。您是否知道规范中是否有任何内容可以保证 Web Worker 即使在后台选项卡中也能运行?我有点担心浏览器供应商将来也会武断地决定夹紧这些......
0赞 Jonas Berlin 7/24/2015
我会将代码的倒数第二行更改为读取,以获取自开始以来经过的时间,而不是上一次调用的结束。这通常是您在制作动画时想要的。就像现在一样,如果 interval 函数的执行需要 15 毫秒,则下次调用它时(当选项卡处于活动状态时)将给出 35 毫秒(而不是 50 毫秒,即间隔)。before = now;elapsedTime
14赞 www 9/12/2011 #2

只需这样做:

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

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

非活动的浏览器选项卡缓冲某些 or 函数。setIntervalsetTimeout

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

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

评论

1赞 9/26/2011
很高兴知道 - 例如,对于最新数据始终正确的 ajax 更新 - 但对于发布的问题,这是否意味着动画显着减慢/暂停,因为“a”不会增加适当的次数?
5赞 1st4ck 11/8/2011 #3

我认为对这个问题的最佳理解是在这个例子中:http://jsfiddle.net/TAHDb/

我在这里做一件简单的事情:

间隔 1 秒,每次隐藏第一个跨度并将其移动到最后一个跨度,并显示第二个跨度。

如果您停留在页面上,它将按预期工作。 但是,如果你把标签隐藏几秒钟,当你回来时,你会看到一个奇怪的东西。

就像在您现在不活跃期间没有发生的所有事件一样,将在 1 次内全部发生。所以在几秒钟内,你会得到 X 事件。它们速度如此之快,以至于可以一次看到所有 6 个跨度。

所以它接缝 chrome 只会延迟事件,所以当你回来时,所有事件都会发生,但都是一次性发生的......

这是一个实用的应用程序,如果发生这种情况,那就是一个简单的幻灯片。想象一下数字是图像,如果用户在回来时保持隐藏选项卡,他将看到所有图像都漂浮在空中,完全迷住了。

要解决此问题,请使用 stop(true,true),就像 pimvdb 告诉的那样。 这将清除事件队列。

评论

4赞 pimvdb 11/8/2011
事实上,这是因为使用了jQuery。由于它有一些怪癖(比如这个),他们删除了它。在 1.7 中,您看不到这种行为。jsfiddle.net/TAHDb/1。因为间隔是每秒一次,这实际上不会影响我发布的问题,因为非活动选项卡的最大值也是每秒一次 - 所以这没有区别。requestAnimationFrame
94赞 Möhre 9/21/2012 #4

我在音频衰落和 HTML5 播放器方面遇到了同样的问题。当选项卡变为非活动状态时,它被卡住了。 因此,我发现 WebWorker 可以不受限制地使用间隔/超时。我用它来将“刻度”发布到主 javascript。

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 9/27/2012
整洁!现在让我们希望他们不会“解决”这个问题。
3赞 Konstantin Smolyanin 4/18/2013
伟大!当您确实需要计时器工作时,应该使用这种方法,而不仅仅是为了修复一些动画问题!
1赞 Skylar Saveland 12/28/2013
我用这个做了一个很棒的节拍器。有趣的是,所有最流行的 html5 鼓机都不使用这种方法,而是使用劣质的常规 setTimeout。skyl.github.io/grimoire/fretboard-prototype.html - Chrome 或 Safari 现在玩得最好。
0赞 Gunchars 10/2/2019
如果开发人员开始滥用它,他们将“修复”它。除了极少数例外情况外,您的应用并不重要,它应该在后台消耗资源以提供一些最低限度的用户体验改进。
0赞 Farzan 2/16/2022
@Gunchars 你说得很对,但如果应用程序可以在这方面征求用户的许可,那不是更好吗?如果用户愿意允许这种情况发生,那为什么不呢?
60赞 Ruslan Tushov 6/29/2015 #5

有一个解决方案可以使用Web Worker(如前所述),因为它们在单独的进程中运行并且不会减慢速度

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

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

更多信息在这里

评论

1赞 ArnaudR 10/7/2015
我已经在一个项目上测试了它,包括使用计时器的库(所以我无法自己实现 workers)。它就像一个魅力。感谢这个图书馆
0赞 neoDev 8/21/2017
@ruslan-Tushov 刚刚在 chrome 60 上尝试过,但似乎根本没有效果......我正在从几个 setTimeout 中调用几个函数来向 DOM 添加/删除由 css 动画动画的元素,并且我看到它们在选项卡切换后会延迟冻结(就像没有插件一样)
0赞 Davide Patti 9/7/2017
@neoDev 您是否在任何其他 JavaScript 之前加载 HackTimer.js(或 HackTimer.min.js)?
0赞 Davide Patti 9/9/2017
@neoDev这听起来很奇怪,因为我最近尝试过并且有效
3赞 Louis Sankey 3/12/2021
我有点困惑 - 使用 React,您可以使用 NPM 安装它而不需要导入任何东西吗?
-1赞 Tengiz 1/28/2016 #6

这是我的粗略解决方案

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

评论

0赞 foobored 5/8/2018
调用它正在处理的自己的事件,从而具有一个不阻塞 UI 的无限循环。当它无限循环时,它会检查每个事件处理是否有超时或间隔函数要运行。postMessageHandler
-1赞 user7356139 12/30/2016 #7

我能够使用音频标签并在至少 250 毫秒内调用我的回调函数并处理其 ontimeupdate 事件。它在一秒钟内调用了 3-4 次。它比一秒滞后 setTimeout 要好

2赞 Mariy 8/17/2017 #8

深受鲁斯兰·图绍夫(Ruslan Tushov)图书馆的影响,我创建了自己的小型图书馆。只需将脚本添加到 中,它就会修补并使用 .<head>setIntervalsetTimeoutWebWorker

评论

1赞 neoDev 8/21/2017
刚刚在 Chrome 60 上尝试过,但似乎根本没有效果......我正在从几个 setTimeout 中调用几个函数来向 DOM 添加/删除由 css 动画动画的元素,并且我看到它们在选项卡切换后会延迟冻结(就像没有插件一样)
0赞 Mariy 8/21/2017
我将其与 Chrome 60 一起使用。你能做一个演示并将其发布在 github 问题上吗?
2赞 Kaan Soral 7/5/2018 #9

播放音频文件可暂时确保完整的后台 Javascript 性能

对我来说,这是最简单、侵入性最小的解决方案——除了播放微弱/几乎空的声音外,没有其他潜在的副作用

您可以在此处找到详细信息:https://stackoverflow.com/a/51191818/914546

(从其他答案中,我看到有些人使用Audio标签的不同属性,我确实想知道是否可以使用Audio标签来获得完整的性能,而无需实际播放某些内容)

15赞 Tomaaszq 12/27/2018 #10

对我来说,像这里的其他人一样在后台播放音频并不重要,我的问题是我有一些动画,当你在其他选项卡中并返回它们时,它们表现得很疯狂。我的解决方案是将这些动画放入其中,如果这可以防止非活动选项卡

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

多亏了这一点,我的动画仅在选项卡处于活动状态时才运行。 我希望这能帮助某人处理我的案子。

评论

1赞 Overcomer 3/23/2022
在复杂的答案中,这个简单的解决方案派上了用场。谢谢。
18赞 iman 1/29/2019 #11

当选项卡处于非活动状态或工作但在正确的时间段不工作时,两者都不起作用。解决方案是将另一个源用于时间事件。例如,Web 套接字Web 辅助角色是两个事件源,当 Tab 处于非活动状态时,它们可以正常工作。因此,无需将所有代码移动到 Web Worker,只需使用 Worker 作为时间事件源即可:setIntervalrequestAnimationFrame

// 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赞 Promod Sampath Elvitigala 3/21/2020
“只需使用 worker 作为时间事件源” 谢谢你的想法
0赞 user3187724 12/30/2020
jsfiddle 给出一个 SecurityError。
0赞 TechDogLover OR kiaNasirzadeh 2/5/2019 #12

注意:如果您喜欢在后台工作的间隔,例如,播放音频或其他内容,则此解决方案不适用。但是,如果您对动画在返回页面或选项卡时无法正常工作感到困惑,这是一个很好的解决方案。

有很多方法可以实现这个目标,也许“WebWorkers”是最标准的,可以肯定的是,它不是最简单和方便的,特别是如果你没有足够的时间,所以你可以尝试这种方式:

基本概念:

  1. 为您的间隔(或动画)建立一个名称并设置您的间隔(动画),以便在用户第一次打开您的页面时运行:var interval_id = setInterval(your_func, 3000);

  2. 每次浏览器(选项卡)被停用并重新运行您的间隔(动画)时,您都可以再次通过$(window).focus(function() {});$(window).blur(function() {});clearInterval(interval_id)interval_id = setInterval();

示例代码:

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;
});
1赞 鄭元傑 2/19/2020 #13

这是一个很古老的问题,但我遇到了同样的问题。

如果您在 chrome 上运行您的网络,您可以通读这篇文章 Chrome 57 中的背景选项卡

基本上,如果间隔计时器没有用完计时器预算,则可以运行。

预算的消耗基于计时器内任务的 CPU 时间使用情况。

根据我的场景,我将视频绘制到画布并传输到 WebRTC。

即使选项卡处于非活动状态,webrtc 视频连接也会不断更新。

但是,您必须使用 instead 而不是 但是,不建议将 itt 用于 UI 渲染。setIntervalrequestAnimationFrame

最好听事件并相应地改变呈现机械主义。visibilityChange

此外,您可以尝试 Kaan Soral 的建议,它应该可以根据文档工作。

评论

0赞 Kapil Soni 1/23/2022
但是如果选项卡处于非活动状态,setInterval 不起作用
0赞 Pawan Deore 2/18/2022
@KapilSoni嘿,你有没有找到任何解决方案,请发表评论。
0赞 Kapil Soni 2/18/2022
@PawanDeore我使用了 rxjs interval(1000) 而不是那个
4赞 Lacerda1109 1/27/2022 #14

我在这里为任何试图在计时器函数中解决这个问题的人带来了一个简单的解决方案,正如@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赞 Jarekczek 4/10/2022 #15

此问题有一个解决方法,尽管实际上必须在某些窗口中使选项卡处于活动状态

  • 将非活动标签页设为单独的浏览器窗口。
  • 不要使任何其他窗口最大化(除非最大化的窗口在您的后面)。

这应该给浏览器一种始终处于活动状态的印象。

这有点麻烦,但也是一个快速的胜利。前提是可以控制窗户的布置。

评论

1赞 Giacomo1968 4/20/2022
虽然这可能是一个不错的最终用户问题,但这如何是一个以编程为中心的解决方案?
1赞 Mr. Polywhirl 5/24/2022 #16

我通过添加一个正常运行的 UI 修改了 Lacerda 的响应

我添加了开始/恢复/暂停/停止操作。

const
  timer = document.querySelector('.timer'),
  timerDisplay = timer.querySelector('.timer-display'),
  toggleAction = timer.querySelector('[data-action="toggle"]'),
  stopAction = timer.querySelector('[data-action="stop"]'),
  tickRate = 10;
let intervalId, initialTime, pauseTime = 0;

const now = () => new Date().getTime();

const formatTime = (hours, minutes, seconds) =>
  [hours, minutes, seconds]
    .map(v => `${isNaN(v) ? 0 : v}`.padStart(2, '0'))
    .join(':');

const update = () => {
  let
    time = (now() - initialTime) + 10,
    hours = Math.floor((time / (60000)) % 60),
    minutes = Math.floor((time / 1000) % 60),
    seconds = Math.floor((time / 10) % 100);
  timerDisplay.textContent = formatTime(hours, minutes, seconds);
};

const
  startTimer = () => {
    initialTime = now();
    intervalId = setInterval(update, tickRate);
  },
  resumeTimer = () => {
    initialTime = now() - (pauseTime - initialTime);
    intervalId = setInterval(update, tickRate);
  },
  pauseTimer = () => {
    clearInterval(intervalId);
    intervalId = null;
    pauseTime = now();
  },
  stopTimer = () => {
    clearInterval(intervalId);
    intervalId = null;
    initialTime = undefined;
    pauseTime = 0;
  },
  restartTimer = () => {
    stopTimer();
    startTimer();
  };

const setButtonState = (button, state, text) => {
  button.dataset.state = state;
  button.textContent = text;
};

const
  handleToggle = (e) => {
    switch (e.target.dataset.state) {
      case 'pause':
        setButtonState(e.target, 'resume', 'Resume');
        pauseTimer();
        break;
      case 'resume':
        setButtonState(e.target, 'pause', 'Pause');
        resumeTimer();
        break;
      default:
        setButtonState(e.target, 'pause', 'Pause');
        restartTimer();
    }
  },
  handleStop = (e) => {
    stopTimer();
    update();
    setButtonState(toggleAction, 'initial', 'Start');
  };

toggleAction.addEventListener('click', handleToggle);
stopAction.addEventListener('click', handleStop);
update();
html, body {
  width: 100%;
  height: 100%;
  margin: 0;
  padding: 0;
}

body {
  display: flex;
  justify-content: center;
  align-items: center;
  background: #000;
}

.timer {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 0.5em;
}

.timer .timer-display {
  font-family: monospace;
  font-size: 3em;
  background: #111;
  color: #8F8;
  border: thin solid #222;
  padding: 0.25em;
}

.timer .timer-actions {
  display: flex;
  justify-content: center;
  gap: 0.5em;
}

.timer .timer-actions button[data-action] {
  font-family: monospace;
  width: 6em;
  border: thin solid #444;
  background: #222;
  color: #EEE;
  padding: 0.5em;
  cursor: pointer;
  text-transform: uppercase;
}

.timer .timer-actions button[data-action]:hover {
  background: #444;
  border: thin solid #666;
  color: #FFF;
}
<div class="timer">
  <div class="timer-display"></div>
  <div class="timer-actions">
    <button data-action="toggle" data-state="initial">Start</button>
    <button data-action="stop">Stop</button>
  </div>
</div>

7赞 Christian Fritz 3/24/2023 #17

受 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();