EDIT: per Yogi's comment (see "setTimeout" and "throttling" in https://developer.mozilla.org/en-US/docs/Web/API/setTimeout ), I've tried adding an AudioContext to prevent the slowdown.
document.addEventListener('click', ev => {
let audCtxt = new AudioContext({});
});
(AudioContext needs user interaction, hence the event listener.)
But, no luck.
Other ideas I'm noting here to follow up are
while
loop, checking Date.now()
for multiples of 10ms -- but I think that would crash the pagerequestAnimationFrame
?Original post:
I have a setTimeout
firing every 0.01 seconds that's acting as a master clock for my web app.
The app plays synchronized sounds that respond to user interaction, hence the need for a master clock. Simplified:
let counter = 0;
setTimeout(() => {
counter++;
console.log(counter);
}, 10);
When on a mobile device, the setTimeout slows down (about 2-4x) when the screen is locked/off. (Tested on Android, not iOS).
This can be verified by logging, like the above, or by generating a sound when the counter is multiple of 100.
setTimeout
is not reliable as other things, such as promises, have higher execution priority. One possible workaround is to create a custom timer using promises. Here is an example:
var customDelay = new Promise(function (resolve) {
var delay = 10; // milliseconds
var before = Date.now();
while (Date.now() < before + delay) { };
resolve();
});
customDelay.then(function () {
//Timer triggered
});
Update 1:
Given that you want a 10ms update frequency, running the above code on the main thread ends up locking up the UI due to the while loop. With that in mind, offloading that while loop into a web worker would resolve this. Here is some code:
<html>
<head>
<title></title>
</head>
<body>
<script id="FastTimer" type="javascript/worker">
onmessage = function (event) {
var delay = 10; // milliseconds
var before = Date.now();
while (Date.now() < before + delay) { };
postMessage({data: []});
};
</script>
<script>
var worker;
window.onload = function() {
var blob = new Blob([document.querySelector("#FastTimer").textContent]);
blobURL = window.URL.createObjectURL(blob);
worker = new Worker(blobURL);
worker.addEventListener("message", receivedWorkerMessage);
worker.onerror = workerError;
//Start the worker.
worker.postMessage({});
}
var counter = 0;
function receivedWorkerMessage(event) {
worker.postMessage({});
timerTiggered();
}
function timerTiggered() {
counter++;
console.log(counter);
}
function workerError(error) {
alert(error.message);
}
function stopWorker() {
worker.terminate();
worker = null;
}
</script>
</body>
</html>
The main issue with the above is that I suspect there would be some sort of time cost going back and forth between the worker (maybe a couple ms, hard to say).
As mentioned, normally requestAnimationFrame
is used for animations in web apps. However, this would likely not fire when the screen is locked. But if you want to try, here is a sample:
<html>
<head>
<title></title>
</head>
<body>
<script>
var counter = 0;
var minTimeSpan = 10;
var lastTime = performance.now();
function animate() {
let t = performance.now();
if (t - lastTime >= minTimeSpan) {
timerTiggered();
}
requestAnimationFrame(animate);
}
function timerTiggered() {
counter++;
console.log(counter);
}
animate();
</script>
</body>
</html>
Update 2:
Based on feedback, over time update 1 can cause high memory usage and tabs crashing. When the original answer in this thread was provided setTimeout
when used in a web worker also didn't work accurately in hidden tabs in some browsers, however does work well in Chrome and Edge. Be sure to test this with your target browsers.
<html>
<head>
<title></title>
</head>
<body>
<script id="FastTimer" type="javascript/worker">
onmessage = function (event) {
var delay = 10; // milliseconds
setTimeout(() => {
postMessage({data: []});
}, delay);
};
</script>
<script>
var worker;
window.onload = function() {
var blob = new Blob([document.querySelector("#FastTimer").textContent]);
blobURL = window.URL.createObjectURL(blob);
worker = new Worker(blobURL);
worker.addEventListener("message", receivedWorkerMessage);
worker.onerror = workerError;
//Start the worker.
worker.postMessage({});
}
var counter = 0;
function receivedWorkerMessage(event) {
worker.postMessage({});
timerTiggered();
}
function timerTiggered() {
counter++;
console.log(counter);
}
function workerError(error) {
alert(error.message);
}
function stopWorker() {
worker.terminate();
worker = null;
}
</script>
</body>
</html>