If you draw canvas a lot, you may find your canvas being freezed when tab/window switched into background. At first, I thought it was a canvas problem. But after some digging, it is actually a Browser's behavior. Let's see what it is and how to tackle this problem.
Say you have a timer to run a function for every 100 miliseconds like below:
let count = 0;
setInterval(() => {
count += 1;
console.log(count);
}, 100);
While running this timer, you switch current tab/window into background, wait a little time and switch back to see the console logs. You may find something not normal. When the tab/window in the background, yes the timer still runs some times, but definitly not in every 100 miliseconds. This is the problem. When a tab/window in the background, browser will "freeze" the JavaScript. Like in this example, we want the timer to run every 100ms, but this was changed to a lot bigger delay, about every 1 second.
This behavior applys to setInterval/setTimeout. If you think about to use requestAnimationFrame to run a infinity recursive function, you will encounter this problem as well. So this is why at the beginning of this article, I say the canvas is freezed when tab/window is in the background. Because I use requestAnimationFrame to run the draw function to draw images in the canvas, so when requestAnimationFrame is freezed, the canvas was not being drawed, thus displays like be freezed.
Now we know what the problem actually is, let's see how to solve it. We know the reason is timer functions be freezed, so the solution is to find a way which will not be freezed when tab/window in background.
Actually, there are many ways to achieve this. For example, we can create a web worker and run a timer in the worker. The timer will send a message to the main thread, and main thread will call the target function every time received the message.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<button id="sendMsg">sendMsg</button>
<button id="closeWorker">closeWorker</button>
<script>
// web worker code
// write it in a string, so we don't need to save it in a new js file
const workerStr = `
console.log("this is worker", self.name);
self.onmessage = e => {
console.log('Received message ' + e.data);
// set an interval, send message to main thread every 100ms
setInterval(() => {
self.postMessage("hello index");
}, 100)
};`;
// start web worker
const blob = new Blob([workerStr], { type: "text/javascript" });
const url = URL.createObjectURL(blob);
const worker = new Worker(url);
// when receiving message from worker, do the target work
worker.onmessage = (e) => {
console.log("Received message " + e.data);
// do the worker
};
// send message to worker to start the interval
// here we may send the delay time
sendMsg.onclick = () => {
worker.postMessage("hello worker");
};
// terminate worker
closeWorker.onclick = () => {
worker.terminate();
};
</script>
</body>
</html>
Another solution I tried is to use the Web Audio API. The AudioScheduledSourceNode has a stop function which receives an argument when to specify the stop playing time. When the node stops playing, it will trigger the ended event. These two features could be used to simulate functions like setInterval/setTimeout.
function bg(callback, delay) {
const AC = window.AudioContext || window.webkitAudioContext;
const ac = new AC();
const gainNode = ac.createGain();
gainNode.gain.value = 0;
gainNode.connect(ac.destination);
// audio context need human operations to resume
document.onmousedown = () => ac.resume();
// flag is used to cancel the below recursive call
let flag = false;
const stop = () => {
flag = true;
};
const f = () => {
if (flag) return;
const oscillatorNode = ac.createOscillator();
// ended event call function itself
oscillatorNode.onended = f;
oscillatorNode.connect(gainNode);
oscillatorNode.start(0);
// stop at specified time, and trigger the ended event
oscillatorNode.stop(ac.currentTime + delay / 1000);
callback();
};
return f(), stop;
}
// using example
const stop = bg(() => {
count += 1;
console.log(count);
}, 100);
// call stop to stop the bg interval
// stop();