Search code examples
javascriptweb-worker

Does the UI thread have priority over web workers?


If I do a seconds long computation in a web worker, can I expect the UI to not stutter because of it? Including mobile? If not, what can I do about it? The workload is easy to split up into smaller chunks, but about half the site doesn't work until that computation is done.


Solution

  • ...can I expect the UI to not stutter because of it?

    Largely, yes, you can, within the limits of the capability of the browser and device. This is, after all, the raison d'être of web workers: To move long-running processes off the UI thread so the UI can remain responsive. There are no guarantees, but... Empirically: I've done tests where the worker busy-loops for 30+ seconds while the main UI updates, and it works just fine on desktop, Android, and iOS.

    Such tests are not difficult:

    Live on plnkr

    Live snippet (may not work on all devices because of how I'm creating the worker):

    const chars = "|/-\\".split("");
    let charindex = -1;
    const spinner = document.getElementById("spinner");
    setInterval(() => {
        charindex = (charindex + 1) % chars.length;
        spinner.innerHTML = chars[charindex];
    }, 50);
    function log(msg) {
        const p = document.createElement("pre");
        p.appendChild(document.createTextNode(msg));
        document.body.appendChild(p);
    }
    function main() {
        const url = URL.createObjectURL(
          new Blob([
            document.getElementById("worker").textContent
          ], {type: "text/javascript"})
        );
        const w = new Worker(url);
        w.onmessage = function(event) {
            if (event.data === "ready") {
                w.postMessage("start");
            } else {
                log(event.data);
            }
        };
    }
    main();
    <div id="spinner"></div>
    <script id="worker" type="worker">
    this.addEventListener("message", e => {
        if (e.data === "start") {
            let last = Date.now();
            const stop = last + 20000;
            let now;
            while ((now = Date.now()) < stop) {
                if (now - last > 1000) {
                    postMessage("tick");
                    last = now;
                }
            }
        }
    });
    postMessage("ready");
    </script>

    worker.js:

    this.addEventListener("message", e => {
        if (e.data === "start") {
            let last = Date.now();
            const stop = last + 30000;
            let now;
            while ((now = Date.now()) < stop) {
                if (now - last > 1000) {
                    postMessage("tick");
                    last = now;
                }
            }
        }
    });
    postMessage("ready");
    

    host.html:

    <!doctype html>
    <html>
    <head>
    <meta charset="UTF-8">
    <title>Worker Host</title>
    <style>
    body {
        font-family: sans-serif;
    }
    </style>
    </head>
    <body>
    <div id="spinner"></div>
    <script>
    const chars = "|/-\\".split("");
    let charindex = -1;
    const spinner = document.getElementById("spinner");
    setInterval(() => {
        charindex = (charindex + 1) % chars.length;
        spinner.innerHTML = chars[charindex];
    }, 50);
    function log(msg) {
        const p = document.createElement("pre");
        p.appendChild(document.createTextNode(msg));
        document.body.appendChild(p);
    }
    function main() {
        const w = new Worker("worker.js");
        w.onmessage = function(event) {
            if (event.data === "ready") {
                w.postMessage("start");
            } else {
                log(event.data);
            }
        };
    }
    main();
    </script>
    </body>
    </html>