Running JavaScript in "Background"

Running JavaScript in "Background"

·

4 min read

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