Left ArrowBack

notes / JavaScript / performance / web workers

web workers

A row of droids on display for sale

Web Workers

Planted

Status: seed

Running computationally expensive functions on a web page can make the page slow or stop responding (blocked). This is because the main thread running the UI (User Interface) also executes all the JavaScript. Being on 1 thread means the engine can only do 1 thing at a time. If the engine is executing an expensive function, it can't also respond to the user attempting to scroll a page.

This can be solved by moving the execution of expensive functions to a different thread. The Web Workers API is a Web API that allows JavaScript to run in a background thread separate from the main execution thread. The worker constructor creates a web worker & returns an object that can send and receive messages.

/index.js

const myWorker = new Worker('myWorker.js');

myWorker.postMessage('...');
myWorker.onmessage = function (event) {};

/myWorker.js

onmessage = function (event) {};
postMessage('...');

Problem

Imagine we created a 10 second timer. Every second, the count is outputted to the console.

localhost:3000

Console

const startButton = document.querySelector('button#timer');

function secondsFromStart(startMS) {
    return Math.floor((Date.now() - startMS) / 1000);
}

function onTimerEnd(intervalId) {
    clearInterval(intervalId);
    startButton.disabled = false;
}

function onInterval(startMS, intervalId) {
    const seconds = secondsFromStart(startMS);
    console.log(seconds);

    if (seconds > 9) {
        onTimerEnd(intervalId);
    }
}

function startTimer() {
    const startMS = Date.now();

    const id = setInterval(() => {
        onInterval(startMS, id);
    }, 1000);
}

startButton.onclick = function () {
    startButton.disabled = true;
    console.clear();
    startTimer();
};

Expensive Function

Now imagine we were required to execute an expensive function while the timer is running. To mock an expensive function, a while loop that runs for 3 seconds will be used.

/index.js

function expensiveFunc() {
    const laterMS = Date.now() + 3000;
    while (Date.now() < laterMS) {}
}

If you start the timer, then start the expensive function, you will notice the output is missing some numbers. When the counter has started, every second, the engine outputs a number to the console. When the expensive function is invoked, it starts executing the while loop. When it comes time to output the next number, the engine can't do it. It needs to 1st complete the while loop before it can continue with the counting.

localhost:3000

Console

const startButton = document.querySelector('button#timer');
const expensiveButton = document.querySelector('button#expensive');

function secondsFromStart(startMS) {
    return Math.floor((Date.now() - startMS) / 1000);
}

function onTimerEnd(intervalId) {
    clearInterval(intervalId);
    startButton.disabled = false;
}

function onInterval(startMS, intervalId) {
    const seconds = secondsFromStart(startMS);
    console.log(seconds);

    if (seconds > 9) {
        onTimerEnd(intervalId);
    }
}

function startTimer() {
    const startMS = Date.now();

    const id = setInterval(() => {
        onInterval(startMS, id);
    }, 1000);
}

startButton.onclick = function () {
    startButton.disabled = true;
    console.clear();
    startTimer();
};

function expensiveFunc() {
    console.log('expensiveFunc invoked');
    const laterMS = Date.now() + 3000;
    while (Date.now() < laterMS) {}
    console.log('expensiveFunc complete');
}

expensiveButton.onclick = function () {
    expensiveButton.disabled = true;
    expensiveFunc();
    expensiveButton.disabled = false;
};

Solution

A solution to this problem is to move the expensive function to a different thread using a worker:

  1. 1. Create a worker containing the function,
  2. 2. Send a message to the worker when the function need to be invoked,
  3. 3. Have the worker execute the function &
  4. 4. When complete, have the worker send back the results of the function.

Now the count is no longer blocked when the expensive function is executed.

React

A Web Worker can also be used within a React Component via the useRef hook.

Where to Next?

A sci-fi robot taxi driver with no lower body