Chrome:后台标签页中超时/间隔被暂停?

189
我正在使用这个测试来测试setTimeout的准确性。如预期,setTimeout不是非常准确,但对于大多数应用程序而言不会存在严重的精度问题。如果我在Chrome中运行测试,并让它在后台标签页中运行(也就是切换到另一个标签页并在那里浏览),然后返回测试并检查结果(如果测试已完成),我发现结果发生了显著变化。看起来超时时间变慢了很多。在Firefox 4或IE9中测试并没有出现这种情况。
因此,在没有焦点的选项卡中,Chrome暂停或至少减缓了JavaScript的执行。在互联网上找不到太多有关此主题的信息。这意味着我们不能运行后台任务,例如使用XHR调用和setInterval定期检查服务器(如果时间允许,我认为setInterval也会出现同样的行为)。
是否有人遇到过这种情况?是否有解决这个暂停/减速的方法?你认为这是一个漏洞吗,我应该报告给开发者吗?

1
有趣!你能否判断Chrome是暂停和恢复计时器还是重新启动它,一旦你重新访问标签?或者这种行为是随机的吗?这是否与Chrome在独立进程中运行标签有关? - HyderA
@gAMBOOKa:看看 @pimvdb 的答案。很可能会将速度减慢到每秒最多一次。 - KooiInc
1
四年过去了,这个问题仍然存在。我为具有“转换”效果的div设置了一个setTimeOut,因此并不是所有的div都同时转换,而是每隔15毫秒转换一次,从而创建了一些滚动效果。当我切换到另一个标签页并在一段时间后回来时,所有的div都会同时转换,而setTimeOut则完全被忽略了。对于我的项目来说,这不是一个大问题,但它是一个奇怪和不需要的附加功能。 - Rvervuurt
对于我们的动画,在按顺序调用setTimeout时,我们的解决方案就是确保记住定时器的句柄/ID(它是从setTimeout返回的),并在设置新定时器之前,如果有句柄,首先调用clearTimeout。在我们的情况下,这意味着当您返回选项卡时,动画播放方面可能会出现一些初始奇怪的问题,但很快就会解决,常规动画就会恢复。最初我们认为这是代码问题。 - Action Dan
8个回答

129

5
谢谢,我应该查看“未激活的选项卡”。有时候不是以英语为母语的人有点吃亏。 - KooiInc
1
@Kooilnc:没问题 :) 我也不是母语为英语的人。 - pimvdb
1
值得一提的是:「2021年1月更新:从Chrome 88开始,在某些情况下,计时器可以被限制为1分钟。阅读更多信息请访问 https://usefulangle.com/web-updates/post/110/chrome-88-will-throttle-timers-1-minute」。 - QuantumBlack
我在Chrome 100上测试了以1分钟间隔运行setInterval并锁定屏幕。该函数大约每30-180秒执行一次。 - Janne Annala

38

可以使用Web Workers解决此问题,因为它们在单独的进程中运行,不会被拖慢

我编写了一个小脚本,可以在不改变代码的情况下使用- 它简单地覆盖setTimeout、clearTimeout、setInterval和clearInterval函数

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

http://github.com/turuslan/HackTimer


12
虽然这很好,但请注意:1. 工作线程无法访问DOM,2. 只有当工作线程在其自己的文件中时才会执行。对于许多情况而言,它不能完全替代setTimeout。 - Madara's Ghost
2
你说得没错,但是一些现代浏览器允许使用Blob(http://www.html5rocks.com/en/tutorials/workers/basics/#toc-inlineworkers)来使用工作者而不需要它们自己的文件。 - Ruslan Tushov
2
即便如此,Web Workers 仍然缺少许多功能(主要是 DOM),这使得它们无法安全地替代 setTimeout 等函数。 - Madara's Ghost
那么对于必须在前端运行的代码呢?例如,我们希望完成繁重的图形处理任务,同时还能做其他事情。 - Michael
1
@Michael,有一个名为OffscreenCanvas的东西。 - Sebastian Simon
显示剩余5条评论

23
播放空的声音可以强制浏览器保持性能。我在阅读这篇评论后发现了这一点:How to make JavaScript run at normal speed in Chrome even when tab is not active?。来自该评论源的内容如下:

Chromium内部人员还澄清说,对于所有“播放音频”的后台标签以及任何存在“活动WebSocket连接”的页面,都将自动禁用过度限制。

我需要一个基于WebSockets的浏览器游戏的无限性能,所以我知道使用WebSockets并不能保证无限的性能,但从测试中,播放音频文件似乎可以确保它。
以下是我为此目的创建的两个空音频循环,您可以自由、商业使用: http://adventure.land/sounds/loops/empty_loop_for_js_performance.ogg http://adventure.land/sounds/loops/empty_loop_for_js_performance.wav
(它们包含-58db噪音,-60db不起作用)
我使用Howler.js在用户需求时播放它们:https://github.com/goldfire/howler.js
function performance_trick()
{
    if(sounds.empty) return sounds.empty.play();
    sounds.empty = new Howl({
        src: ['/sounds/loops/empty_loop_for_js_performance.ogg','/sounds/loops/empty_loop_for_js_performance.wav'],
        volume:0.5,
        autoplay: true, loop: true,
    });
}

很遗憾地,JavaScript 没有内建方法来默认开启/关闭完整的性能,然而,加密货币挖掘者可以利用 Web Workers 在没有任何提示的情况下劫持所有的计算线程。


谢谢,使用耳机时58db声音确实很大,但静音该网站可以解决这个问题。 - Kaan Soral
为什么是音量0.5? - Blaze
不记得了,可能是复制粘贴时留下的,但有一个声音限制,低于该限制,浏览器就会忽略声音 - 所以可能是这个原因。 - Kaan Soral
尝试使用nosleep.js作为如何使用视频的示例。 - Travis
2
有趣的方法。这会劫持我的蓝牙耳机,不允许我的手机播放声音...实际上...这可能解释了一些事情... - aidan
这可能不是一个受欢迎的观点,但一旦用户离开您的应用程序并转到不同的选项卡,实际上用户正在将您的应用程序置于后台。寻找防止此类情况发生的方法正是恶意行为者所喜欢的劫持方式。 - JSON

5

针对单元测试,您可以在chrome/chromium中使用参数--disable-background-timer-throttling运行。


3
我发布了一个npm包,叫做 worker-interval,它使用Web-Workers实现了setInterval和clearInterval功能,可以在Chrome、Firefox和IE的非活动标签页上保持运行。

对于现代浏览器(Chrome、Firefox和IE)中的定时器,即使是在非活动标签页中,也会被限制为每秒钟最多触发一次。

您可以在以下链接中找到更多信息:

https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval

https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers#Timeouts_and_intervals


0

我将我的jQuery核心更新到1.9.1版本,这解决了在非活动选项卡中的间隔差异问题。我建议您首先尝试这个方法,然后再考虑其他代码覆盖选项。


你是从哪个版本升级来的?我在 ~1.6 版本中遇到了一些超时问题(画廊滑块)。 - dmi3y

0

浏览器旨在优化用户体验和电池寿命,它们限制后台标签的活动以节省CPU和电力。

避免后台执行抑制的两种方法是 (例如ping/pong消息的示例)

第一种方法 -

内联:

// Web Worker code defined as a string
const workerCode = `
// When the worker receives a message...
    onmessage = function(e) {
        // ...if that message is 'start'
        if (e.data === 'start') {
            // ...set an interval to send a heartbeat every minute
            setInterval(() => {
                // Fetch a heartbeat from the server
                fetch('http://127.0.0.1:9000/heartbeat')
                .then(response => {
                    // If the response isn't okay, throw an error
                    if (!response.ok) {
                        throw new Error('Network response was not ok');
                    }
                    return response.json(); 
                })
                .then(data => {
                    // Log the received heartbeat
                    console.log('Heartbeat received:', data);
                })
                .catch(error => {
                    // If there's an error, log it
                    console.error('Fetch error:', error.message);
                });
            }, 60000);
        }
    };
`;

// Create a new Blob object from the worker code
const blob = new Blob([workerCode], { type: 'application/javascript' });

// Create a new Web Worker from the blob URL
const worker = new Worker(URL.createObjectURL(blob));

// Post a 'start' message to the worker to begin the heartbeat
worker.postMessage('start');

第二 -

worker.js 文件:

// When the worker receives a message...
onmessage = function(e) {
    // ...if that message is 'start'
    if (e.data === 'start') {
        // ...set an interval to send a heartbeat every minute
        setInterval(() => {
            // Fetch a heartbeat from the server
            fetch('http://127.0.0.1:9000/heartbeat')
            .then(response => {
                // If the response isn't okay, throw an error
                if (!response.ok) {
                    throw new Error('Network response was not ok');
                }
                return response.json(); 
            })
            .then(data => {
                // Log the received heartbeat
                console.log('Heartbeat received:', data);
            })
            .catch(error => {
                // If there's an error, log it
                console.error('Fetch error:', error.message);
            });
        }, 60000);
    }
};

主要部分:
// Create a new Web Worker from the external worker file
const worker = new Worker('worker.js');

// Post a 'start' message to the worker to begin the heartbeat
worker.postMessage('start');

-1

这是我的解决方案,它获取当前毫秒数,并将其与创建函数时的毫秒数进行比较。对于间隔,它会在运行函数时更新毫秒数。您还可以通过ID获取间隔/超时。

<script>

var nowMillisTimeout = [];
var timeout = [];
var nowMillisInterval = [];
var interval = [];

function getCurrentMillis(){
    var d = new Date();
    var now = d.getHours()+""+d.getMinutes()+""+d.getSeconds()+""+d.getMilliseconds();
    return now;
}

function setAccurateTimeout(callbackfunction, millis, id=0){
    nowMillisTimeout[id] = getCurrentMillis();
    timeout[id] = setInterval(function(){ var now = getCurrentMillis(); if(now >= (+nowMillisTimeout[id] + +millis)){callbackfunction.call(); clearInterval(timeout[id]);} }, 10);
}

function setAccurateInterval(callbackfunction, millis, id=0){
    nowMillisInterval[id] = getCurrentMillis();
    interval[id] = setInterval(function(){ var now = getCurrentMillis(); if(now >= (+nowMillisInterval[id] + +millis)){callbackfunction.call(); nowMillisInterval[id] = getCurrentMillis();} }, 10);
}

//usage
setAccurateTimeout(function(){ console.log('test timeout'); }, 1000, 1);

setAccurateInterval(function(){ console.log('test interval'); }, 1000, 1);

</script>

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