Presentation

Threadizer is a JavaScript library making web workers easy to use.

Install

The package is published on npm, sources and documentation are available on github.

npm install @threadizer/core

Quick Start

In this example, the function is ran within the worker thread then the main thread send it a message named "custom-event" containing some data. You can send any Object (it must works with JSON.stringify(...)) or any instance of ArrayBuffer, MessagePort, ImageBitmap or OffscreenCanvas.

						import Threadizer from "@threadizer/core";

						const thread = await new Threadizer(()=>{

							self.on("custom-event", ( event )=>{

								console.log(self, event.detail);

							});

						});

						const buffer = new ArrayBuffer(1000);

						thread.transfer("custom-event", buffer);
					

Importing external libraries

You may need libraries within your worker, for that the easiest way is to compile and export your worker script into a dedicated file. To use the worker file simply place the path as first parameter:

  • main.js
  • worker.js
							import Threadizer from "@threadizer/core";

							const thread = await new Threadizer("path/to/worker.js");
						
							import * as THREE from "three";

							console.log(THREE);
						

You also can use importScripts(...) inside the worker to load externals or vendors scripts:

							import Threadizer from "@threadizer/core";

							const thread = await new Threadizer(()=>{

								importScripts("path/to/three.min.js");

								console.log(THREE);

							});
						

Deep workers

A worker can itself create sub-workers offering infinite possibility of optimisations.

If a thread is destroyed, all its child threads are destroyed too. Also, remember that if a parent thread is frozen by a long process, communications among its child threads would be delayed.

  • main.js
  • worker.js
							import Threadizer from "@threadizer/core";

							const thread = await new Threadizer("path/to/worker.js");
						
							import Threadizer from "@threadizer/core";

							const subthreadA = await new Threadizer(()=>{

								console.log("subthread A ready");

							});

							const subthreadB = await new Threadizer(()=>{

								console.log("subthread B ready");

							});
						

Performance

Running scripts within workers is the best way to avoid freezing the main-thread. Here is an example of a high CPU usage stress-test script, it will launch a loop of 1e4 iterations to sort an array.

Running the main-thread test will cause the browser to freeze the current tab while processing (around 6 seconds with on an Intel i9, less than 20 seconds on a recent iPhone). You can check how much time the main-thread got freezed in the devtools console.

  • in worker
  • on main-thread
								import Threadizer from "@threadizer/core";

								const thread = await new Threadizer(()=>{

									console.log("begin performance-worker");
									console.time("performance-worker");

									const array = new Array();

									for( let index = 0; index < 1e4; index++ ){

										array[index] = Math.random();

										array.sort();

									}

									console.timeEnd("performance-worker");

								});
							
								console.time("performance-worker");

								const array = new Array();

								for( let index = 0; index < 1e4; index++ ){

									array[index] = Math.random();

									array.sort();

								}

								console.timeEnd("performance-worker");
							

Stream

You may need to use multiple dedicated threads to execute diffent jobs. Stream is here to make this job easier, it automaticaly listen to the complete callback and send it to the next pipe.

Then you can use the whole set of pipes as a big Promise and get the output data.

						import Threadizer from "@threadizer/core";

						const randomData = window.btoa(window.location.href);

						const stream = Threadizer.createStream(randomData);

						const resolveThread = await new Threadizer(()=>{

							self.on("pipe", ({ detail, complete })=>{

								console.log("resolve pipe");

								complete(self.atob(detail));

							});

						});

						const fetchThread = await new Threadizer(()=>{

							self.on("pipe", async ({ detail, complete })=>{

								console.log("fetch pipe");

								const page = await fetch(detail).then(response => response.text());

								complete({
									ok: response.ok,
									url: response.url,
									type: response.type,
									status: response.status,
									redirected: response.redirected,
									content: page
								});

							});

						});

						const page = await stream.pipe(encodeThread).pipe(sendThread);

						console.log(page);
					

Pool

In some cases, you need to execute a same thread for different inputs. Thats where pools help. It clone the thread you give as much as you requested. Then, using the transfer method will send it to an available thread or wait until one become available again.

The second parameter define how many threads are created in the pool.

By default, the number of created threads is equal to navigator.hardwareConcurrency (or 8 on unsupported browsers). Also you shall avoid using more threads then the CPU got logic threads to limit concurrency.

						import Threadizer from "@threadizer/core";

						const thread = await new Threadizer(()=>{

							self.on("encode", async ({ detail, complete })=>{

								await new Promise(resolve => setTimeout(resolve, 100));

								complete(self.btoa(detail));

							});

						});

						const pool = await Threadizer.createPool(thread, 6);

						let done = 0;

						for( let index = 0, length = 100; index < length; index++ ){

							pool.transfer("encode", `${ index } Hello World!`).then(( output )=>{

								done++;

								console.log(done, index, output);

								if( done === length ){

									console.log("all done");

								}

							});

						}
					

Playground

The code below is editable, it will be executed inside a worker:

The variable Threadizer is already loaded and accessible.

							console.log(Threadizer);