Chrome: timeouts/interval suspended in background tabs?
Asked Answered
G

8

192

I was testing the accuracy of setTimeout using this test. Now I noticed that (as expected) setTimeout is not very accurate but for most appliances not dramatically inaccurate. Now if I run the test in Chrome and let it run in a background tab (so, switching to another tab and browse on there), returning to the test and inspecting the results (if the test finished) they are dramatically changed. It looks like the timeouts have been running a lot slower. Tested in FF4 or IE9 this didn't occur.

So it looks like Chrome suspends or at least slows down javascript execution in a tab that has no focus. Couldn't find much on the internet on the subject. It would mean that we can't run background tasks, like for example checking periodically on a server using XHR calls and setInterval (I suspect to see the same behavior for setInterval, will write a test if time is with me).

Has anyone encountered this? Would there be a workaround for this suspension/slowing down? Would you call it a bug and should I file it as such?

Galeiform answered 17/5, 2011 at 14:34 Comment(4)
Interesting! Can you tell if Chrome is pausing and resuming timer or restarting it, once you re-access the tab? Or is the behavior random? Could it have anything to do with the fact that Chrome runs tabs in independent processes?Gschu
@gAMBOOKa: take a look @ pimvdb's answer. It's likely a slow down to a maximum of once per second.Galeiform
4 years later and this problem still exists. I have a setTimeOut for divs with a transition, so not all divs transition at the same time, but actually 15ms after eachother, creating some rolling effect. When I go to another tab and come back after a while, all divs transition at the same time and the setTimeOut is completely ignored. It's not a big problem for my project, but it is a weird and unwanted addition.Toga
For our animation which called setTimeout in a sequence, the solution for us was just to make sure that we remember the handle/ID of the timer (it's returned from setTimeout) and before we set a new timer we first call clearTimeout if we've got the handle. In our case this means that when you return to the tab, there may be some initial wierdness in terms of what animation is playing but it sorts itself out pretty quickly and the regular animation resumes. We had thought this was an issue with out code initially.Roughandready
C
133

I recently asked about this and it is behaviour by design. When a tab is inactive, only at a maximum of once per second the function is called. Here is the code change.

Perhaps this will help: How can I make setInterval also work when a tab is inactive in Chrome?

TL;DR: use Web Workers.

Culpa answered 17/5, 2011 at 14:44 Comment(4)
thanks, I should've looked with 'inactive tab'. Not being a native english speaker is a handicap sometimes.Galeiform
@Kooilnc: No problem :) I'm not a native English speaker either.Culpa
Worth mentioning this: "Update, Jan 2021 : From Chrome 88+, timers can be throttled to 1 minute in certain conditions. Read more usefulangle.com/web-updates/post/110/…"Donnelly
I tested running setInterval on Chrome 100 with 1 minute interval and locked the screen. The function had been executed in roughly 30-180 second intervals.Sterol
M
38

There is a solution to use Web Workers, because they run in separate process and are not slowed down

I've written a tiny script that can be used without changes to your code - it simply overrides functions setTimeout, clearTimeout, setInterval, clearInterval

Just include it before all your code

http://github.com/turuslan/HackTimer

Moltke answered 28/6, 2015 at 22:23 Comment(11)
That's nice and all but be aware that: 1. Workers have no access to the DOM, 2. Workers are only ever executed if they're on a file on their own. It's not a drop-in replacement for setTimeout for a lot of cases.Ravage
You are right, but some modern browsers allow to use workers without their own files by using Blobs (html5rocks.com/en/tutorials/workers/basics/#toc-inlineworkers)Moltke
Even with that, Web Workers are missing a lot of functionality (namely, DOM) that allows them to be a safe replacement for setTimeout and co.Ravage
What about for code that has to run in the front end, for instance, heavy graphics processing tasks that we would like to finish while we do other stuff?Fencesitter
Well, you can create Workers, service workers and use the canvas API using a data-url. new Worker('data:text/javascript,(' + function myWorkerCode () { /*...*/ } + '()'). It's also a nice way to check if you have import expression support: try { eval('import("data:text/javascript,void 0")') } catch (e) { /* no support! */ }Defensive
@Fencesitter There’s OffscreenCanvas.Wigwam
I know OP was asking about Chrome, but this doesn't work for iOS SafariKobarid
@RuslanTushov: It helps a lot, it works as expected, just one clarification cant we rename the JS file name or inside the HackTimer.js if we rename the logPrefix = 'HackTimer.js: ' name or ('HackTimerWorker.js'); etc also its impacting ? how this naming are referred and why this important ? cant alter this ?Ea
@RuslanTushov it works great for the background tab issue but somtimes, combined with jquery, hacktimer executes last causing the page to simply run forever in my case. Is there a way to clear all hacktimer timeouts after a period of time? TxMethinks
Why would you want/need this?Axiomatic
The linked library is infrequently used and hasn't been updated in 7 years. I recommend instead using worker-timers which does the same thing and is receiving active maintenance. Also, save yourself some debugging time and add worker-src blob: to your content-security-policy if you get a CSP error!Roberto
L
24

Playing an empty sound forces the browser to retain the performance. I discovered it after reading this comment: How to make JavaScript run at normal speed in Chrome even when tab is not active?

With the source of that comment found here:

The Chromium insider also clarified that aggressive throttling will be automatically disabled for all background tabs “playing audio” as well as for any page where an “active websocket connection is present.”

I need unlimited performance on-demand for a browser game that uses WebSockets, so I know from experience that using WebSockets doesn't ensure unlimited performance, but from tests, playing an audio file seems to ensure it

Here's two empty audio loops I created for this purpose, you can use them freely, commercially: http://adventure.land/sounds/loops/empty_loop_for_js_performance.ogg http://adventure.land/sounds/loops/empty_loop_for_js_performance.wav

(They include -58db noise, -60db doesn't work)

I play them, on user-demand, with 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,
    });
}

It's sad that there is no built-in method to turn full JavaScript performance on/off by default, yet, crypto miners can hijack all your computing threads using Web Workers without any prompt.

Lipp answered 5/7, 2018 at 12:52 Comment(6)
Thank you, 58db is very hearable with headphones tho, but muting the site solves that problemLipp
Why volume 0.5?Petulance
Don't remember could be a leftover from a copy-paste, but there was a sound limit and lover than that limit, the browser was just ignoring the sound - so could've been thatLipp
Try nosleep.js for an example of how to use a video for thisKobarid
Interesting approach. This would hijack my bluetooth headphones, and not allow my phone to play sounds... infact... that might explain a few things...Fadeless
It might not be a popular opinion but once a user moves away from your app to a different tab your application is factually being backgrounded by the user. Finding ways to prevent this is the very type of hijack that malicious actors love.Seminal
D
6

For unit tests you can run your chrome/chromium with argument: --disable-background-timer-throttling

Demb answered 5/11, 2022 at 6:20 Comment(1)
Spent some weeks looking for this flag. Also works great for electron apps that can access command line switches like: app.commandLine.appendSwitch('disable-background-timer-throttling') Ilan
P
4

I have released worker-interval npm package which setInterval and clearInterval implementation with using Web-Workers to keep up and running on inactive tabs for Chrome, Firefox and IE.

Most of the modern browsers (Chrome, Firefox and IE), intervals (window timers) are clamped to fire no more often than once per second in inactive tabs.

You can find more information on

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

Parrakeet answered 13/12, 2017 at 15:42 Comment(0)
T
0

I updated my jQuery core to 1.9.1, and it solved the Interval discrepancy in inactive tabs. I would try that first, then look into other code override options.

Typical answered 11/7, 2013 at 17:29 Comment(1)
which version did you upgrade from? I experienced some timeout problems (gallery sliders) with version ~1.6Dynamoelectric
P
0

Browsers are designed to optimize user experience and battery life, and they restrict the activity of background tabs to conserve CPU and power.

2 ways to dodge background execution supression is (example on ping/pong messages):

1st -

inline:

// 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');

2nd -

worker.js file:

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

main section:

// 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');
Papotto answered 22/8, 2023 at 15:43 Comment(0)
W
-2

here is my solution which gets the current millisecond, and compares it to the millisecond that the function was created. for interval, it will update the millisecond when it runs the function. you can also grab the interval/timeout by an 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>
Whimsy answered 5/6, 2019 at 14:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.