1. Overview

The JavaScript language uses a single-threaded model, which means that all tasks can only be done on one thread, one thing at a time. If the previous task is not done, the later task has to wait. With the increase in computing power of computers, especially with the advent of multi-core CPUs, single-threading poses a great inconvenience and cannot fully utilize the computing power of computers.

The role of the Web Worker is to create a multi-threaded environment for JavaScript, allowing the main thread to create Worker threads and assign some tasks to the latter to run. While the main thread is running, the worker thread is running in the background without interfering with each other. When the worker thread finishes the computation, it returns the result to the main thread. The advantage of this is that some computationally intensive or high-latency tasks are taken care of by the worker thread, and the main thread (which is usually responsible for UI interactions) is smooth and does not get blocked or slowed down.

Once a worker thread is created successfully, it will always run without being interrupted by activities on the main thread (e.g., user clicks on buttons, form submissions). This makes it easier to respond to communication from the main thread at all times. However, it also means that the worker is resource-intensive and should not be overused, and should be closed once it has been used.

The Web Worker has the following points of caution for use.

1.1. Homologous restrictions

The script file assigned to the worker thread to run must be the same source as the main thread’s script file.

1.2. DOM Limitations

The global object where the worker thread is located, unlike the main thread, cannot read the DOM object of the web page where the main thread is located, nor can it use the document, window, and parent objects. However, the Worker thread can navigator object and location object.

1.3. Communication Contact

Worker threads and main threads are not in the same context, they cannot communicate directly and must do so via messages.

1.4. Script Restrictions

The worker thread cannot execute the alert() method and confirm() method, but can issue AJAX requests using the XMLHttpRequest object

1.5. File Restrictions

The worker thread cannot read local files, i.e. it cannot open the local file system (file://), and the scripts it loads must come from the network.

2. Basic usage

2.1. Main threads

The thread uses the new command to call the Worker() constructor to create a new Worker thread.

1
var worker = new Worker('work.js');

The argument to the Worker() constructor is a script file that is the task to be executed by the Worker thread. Since the Worker cannot read local files, this script must come from the network. If the download does not succeed (e.g., a 404 error), the worker fails silently.

Then, the main thread calls the worker.postMessage() method to send a message to the Worker.

1
2
worker.postMessage('Hello World');
worker.postMessage({method: 'echo', args: ['Work']});

The argument to the worker.postMessage() method is the data that the main thread passes to the Worker. It can be of various data types, including binary data.

Then, the main thread specifies the listener function via worker.onmessage to receive the message sent back by the child thread.

1
2
3
4
5
6
7
8
9
worker.onmessage = function (event) {
  console.log('Received message ' + event.data);
  doSomething();
}

function doSomething() {
  // Execution of tasks
  worker.postMessage('Work done!');
}

In the above code, the data property of the event object gets the data sent by the Worker.

Once the Worker finishes its task, the main thread can shut it down.

1
worker.terminate();

2.2. Worker threads

The worker thread needs to have a listener function inside it that listens for message events.

1
2
3
self.addEventListener('message', function (e) {
  self.postMessage('You said: ' + e.data);
}, false);

In the above code, self represents the subthread itself, i.e. the global object of the subthread. Therefore, it is equivalent to the following two ways of writing.

1
2
3
4
5
6
7
8
9
// Writing Method I
this.addEventListener('message', function (e) {
  this.postMessage('You said: ' + e.data);
}, false);

// Writing method 2
addEventListener('message', function (e) {
  postMessage('You said: ' + e.data);
}, false)

In addition to using self.addEventListener() to specify the listener function, you can also use self.onmessage to specify it. The argument to the listener function is an event object whose data property contains the data sent by the main thread. The self.postMessage() method is used to send a message to the main thread.

Depending on the data sent by the main thread, the Worker thread can call different methods, here is an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
self.addEventListener('message', function (e) {
  var data = e.data;
  switch (data.cmd) {
    case 'start':
      self.postMessage('WORKER STARTED: ' + data.msg);
      break;
    case 'stop':
      self.postMessage('WORKER STOPPED: ' + data.msg);
      self.close(); // Terminates the worker.
      break;
    default:
      self.postMessage('Unknown command: ' + data.msg);
  };
}, false);

In the above code, self.close() is used to close itself inside Worker.

2.3. Worker load script

There is a special method importScripts() inside the Worker if you want to load other scripts.

1
importScripts('script1.js');

This method can load multiple scripts at the same time.

1
importScripts('script1.js', 'script2.js');

2.4. Error Handling

The main thread can listen to Worker to see if an error occurs. If an error occurs, Worker will trigger the error event of the main thread.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
worker.onerror(function (event) {
  console.log([
    'ERROR: Line ', e.lineno, ' in ', e.filename, ': ', e.message
  ].join(''));
});

// or
worker.addEventListener('error', function (event) {
  // ...
});

Worker can also listen for error events internally.

2.5. Close Worker

When you have finished using it, you must close Worker in order to save system resources.

1
2
3
4
5
// Main threads
worker.terminate();

// Worker threads
self.close();

3. Data Communication

As mentioned earlier, the content of communication between the main thread and the Worker can be either text or objects. It should be noted that this communication is a copy relationship, i.e., it passes values instead of addresses, and any changes made by Worker to the communication content will not affect the main thread. In fact, the internal mechanism of the browser is to serialize the communication content, and then send the serialized string to Worker, who then restores it.

Binary data, such as File, Blob, ArrayBuffer, and other types, can also be exchanged between the main thread and the worker, and can also be sent between threads. Here is an example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// Main threads
var uInt8Array = new Uint8Array(new ArrayBuffer(10));
for (var i = 0; i < uInt8Array.length; ++i) {
  uInt8Array[i] = i * 2; // [0, 2, 4, 6, 8,...]
}
worker.postMessage(uInt8Array);

// Worker threads
self.onmessage = function (e) {
  var uInt8Array = e.data;
  postMessage('Inside worker.js: uInt8Array.toString() = ' + uInt8Array.toString());
  postMessage('Inside worker.js: uInt8Array.byteLength = ' + uInt8Array.byteLength);
};

However, the copy method of sending binary data can cause performance problems. For example, if the main thread sends a 500MB file to the worker, by default the browser will generate a copy of the original file. To solve this problem, JavaScript allows the main thread to transfer the binary data directly to the child thread, but once transferred, the main thread can no longer use the binary data, to prevent the problem of multiple threads modifying the data at the same time. This method of transferring data is called Transferable Objects. This allows the main thread to quickly hand over the data to the worker, which is very convenient for image processing, sound processing, 3D operations, etc., and does not create a performance burden.

If you want to transfer control of the data directly, you have to use the following write method.

1
2
3
4
5
6
// Transferable Objects
worker.postMessage(arrayBuffer, [arrayBuffer]);

// Demo
var ab = new ArrayBuffer(1);
worker.postMessage(ab, [ab]);

4. Web Worker for the same page

Typically, the worker loads a single JavaScript script file, but it can also load code that is on the same page as the main thread.

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
  <body>
    <script id="worker" type="app/worker">
      addEventListener('message', function () {
        postMessage('some message');
      }, false);
    </script>
  </body>
</html>

The above is a script embedded in a web page. Note that you must specify that the type attribute of the <script> tag is a value that the browser does not recognize, in the above case app/worker.

Then, this embedded script is read and processed with Worker.

1
2
3
4
5
6
7
8

var blob = new Blob([document.querySelector('#worker').textContent]);
var url = window.URL.createObjectURL(blob);
var worker = new Worker(url);

worker.onmessage = function (e) {
  // e.data === 'some message'
};

In the above code, the script code embedded in the web page is first converted into a binary object, then a URL is generated for the binary object, and then the Worker loads the URL, so that both the main thread and the Worker’s code are on the same web page.

5. Example: Worker thread completes polling

Sometimes, browsers need to poll the server state to be the first to know about state changes. This can be done in the Worker.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function createWorker(f) {
  var blob = new Blob(['(' + f.toString() +')()']);
  var url = window.URL.createObjectURL(blob);
  var worker = new Worker(url);
  return worker;
}

var pollingWorker = createWorker(function (e) {
  var cache;

  function compare(new, old) { ... };

  setInterval(function () {
    fetch('/my-api-endpoint').then(function (res) {
      var data = res.json();

      if (!compare(data, cache)) {
        cache = data;
        self.postMessage(data);
      }
    })
  }, 1000)
});

pollingWorker.onmessage = function () {
  // render data
}

pollingWorker.postMessage('init');

In the above code, the worker polls the data once every second and compares it with the cache. If it doesn’t match, there is a new change on the server side, so the main thread is notified.

6. Example: Worker New Worker

New worker threads can be created inside worker threads (currently only supported by Firefox browser). The following example takes a computationally intensive task and assigns it to 10 workers

The main thread code is as follows.

1
2
3
4
var worker = new Worker('worker.js');
worker.onmessage = function (event) {
  document.getElementById('result').textContent = event.data;
};

The code of the worker thread is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// worker.js

// settings
var num_workers = 10;
var items_per_worker = 1000000;

// start the workers
var result = 0;
var pending_workers = num_workers;
for (var i = 0; i < num_workers; i += 1) {
  var worker = new Worker('core.js');
  worker.postMessage(i * items_per_worker);
  worker.postMessage((i + 1) * items_per_worker);
  worker.onmessage = storeResult;
}

// handle the results
function storeResult(event) {
  result += event.data;
  pending_workers -= 1;
  if (pending_workers <= 0)
    postMessage(result); // finished!
}

In the above code, 10 new Worker threads are created inside the Worker thread and messages are sent to each of the 10 Workers in turn, informing them of the start and end points of the computation. The code of the computation task script is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// core.js
var start;
onmessage = getStart;
function getStart(event) {
  start = event.data;
  onmessage = getEnd;
}

var end;
function getEnd(event) {
  end = event.data;
  onmessage = null;
  work();
}

function work() {
  var result = 0;
  for (var i = start; i < end; i += 1) {
    // perform some complex calculation here
    result += 1;
  }
  postMessage(result);
  close();

7. API

7.1. Main threads

The Worker() constructor is natively provided by the browser for the main thread to generate the worker thread.

1
var myWorker = new Worker(jsUrl, options);

The Worker() constructor, can accept two parameters. The first parameter is the URL of the script (which must comply with the same-origin policy), which is required and can only load JS scripts, otherwise it will report an error. The second parameter is a configuration object, which is optional. One of its roles is to specify the name of the worker, which is used to distinguish between multiple worker threads.

1
2
3
4
5
// Main threads
var myWorker = new Worker('worker.js', { name : 'myWorker' });

// Worker threads
self.name // myWorker

The Worker() constructor returns a Worker thread object for the main thread to manipulate the Worker.The properties and methods of the Worker thread object are as follows.

  • Worker.onerror: Specifies the listener function for the error event.
  • Worker.onmessage: specifies the listener function for the message event, the data sent is in the Event.data property.
  • Worker.onmessageerror: specifies the listener function for the messageerror event. This event will be triggered when the sent data cannot be serialized to a string.
  • Worker.postMessage(): sends a message to the worker thread.
  • Worker.terminate(): terminate the worker thread immediately.

7.2. Worker threads

The Web Worker has its own global object, not the main thread’s window, but a global object customized for the Worker. So not all the objects and methods defined on window are available.

The Worker thread has some global properties and methods of its own.

  • self.name: the name of the worker. This property is read-only and is specified by the constructor.
  • self.onmessage: the listener for the message event.
  • self.onmessageerror: specifies the listener function for the messageerror event. This event will be triggered when the sent data cannot be serialized to a string.
  • self.close(): Close the worker thread.
  • self.postMessage(): send a message to the worker thread that spawned it.
  • self.importScripts(): Load JS scripts.

Reference https://www.ruanyifeng.com/blog/2018/07/web-worker.html