This example is rendered with eight WebWorkers, and uses ArrayBuffers with transferred ownership to pass data from the worker back to the main script.

mandelbrot_set.ts

interface WorkerJob {
	width: number;
	height: number;
	y: number;
}

interface WorkerResponse {
	y: number;
	row: ArrayBuffer;
}

const TASK_COUNT = 8

function createCanvas() {
	const canvas = document.createElement('canvas');

	canvas.id = "mandelbrot_canvas";
	
	canvas.setAttribute("style", "width:100%;")
	
	canvas.style.border = "1px solid #ccc";

	const root = document.getElementById("mandelbrot");
	root.appendChild(canvas);
	canvas.style.height = Math.floor(canvas.offsetWidth) + 'px';

	canvas.width = canvas.offsetWidth;
	canvas.height = canvas.offsetWidth;

	return canvas;	
}

function showMessage(msg: string) {
	let hello = document.createElement('div');
	let textNode = document.createTextNode(msg)
	hello.appendChild(textNode)

	let entry = document.getElementById("mandelbrot")
	entry.appendChild(hello)
}

function spawnWorkers(mandelbrot_canvas: HTMLCanvasElement) {

	let startTime: number
	let endTime: number

	function start() {
		startTime = new Date().getTime();
	};

	function end(width: number, height: number) {
		endTime = new Date().getTime();
		let timeDiff = endTime - startTime;

		// get milliseconds 
		let milliseconds = Math.round(timeDiff);
		showMessage(width + " x " + height + " " + milliseconds + " ms")
	}

	start()
	
	const ctx = mandelbrot_canvas.getContext("2d");

	const width = mandelbrot_canvas.width * 4
	const height = mandelbrot_canvas.height * 4
	let remaining = height
	mandelbrot_canvas.width = width
	mandelbrot_canvas.height = height
	mandelbrot_canvas.style.width  = (width / 4) + 'px';
	mandelbrot_canvas.style.height = (height / 4) + 'px';
	
	let tasks: Array<Worker> = []
	
	for (let count = 0; count < TASK_COUNT; count++) {
		const worker = new Worker("/assets/js/mandelbrot_worker_renderer.bundle.js");
		worker.onmessage = function(msg: MessageEvent) {

			const image = ctx.getImageData(0, 0, 
				mandelbrot_canvas.width * 2, 1);
	
			const data = image.data
	
			const messageData: WorkerResponse = msg.data
			const y = messageData["y"]
			const rowBuffer = messageData["row"]
			const row = new Uint32Array(rowBuffer)
			
	
			for (let x = 0; x < width; x ++) {
				const offset = (x * 4);
				const xx = (x / width) * 4.0 - 2.0 - 0.5;

				let gray = 0;
						
				if (row[x] % 2 == 0) {
					gray = 255;
				}

				data[offset + 0] = gray;    //red
				data[offset + 1] = gray;    //green
				data[offset + 2] = gray;    //blue
				data[offset + 3] = 255;     //alpha
			}
		
			ctx.putImageData(image, 0, y);
			remaining -= 1
			if (remaining == 0) {
				end(width, height)
			}
		}
		tasks.push(worker)
	}
	
	let task_counter = 0
	
	for (let y = 0; y < height; y++) {
		
		const msg: WorkerJob = {
			width: width, 
			height: height, 
			y: y
		}
		let worker = tasks[task_counter]
		worker.postMessage(msg)	
		task_counter += 1
		if (task_counter >= tasks.length) {
			task_counter = 0
		}
	}
	
}

const mandelbrot_canvas = createCanvas()

spawnWorkers(mandelbrot_canvas)

mandelbrot_worker_renderer.ts

declare function postMessage(message: any, transfer?: Transferable[]):void

let Mandelbrot = {
	
	ITERATIONS: 1024,
	BAILOUT: 16,

	iterate: function (xx: number, yy: number) {
	
		const iterations = this.ITERATIONS
		const bailout = this.BAILOUT
		
		const cr = xx
		const ci = yy
		let zi = 0.0
		let zr = 0.0
	
		let i = 0
	
		while (i++ < iterations) {
			const temp = zr * zi
			const zr2 = zr * zr
			const zi2 = zi * zi
			zr = zr2 - zi2 + cr
			zi = 2.0 * temp + ci
			if (zi2 + zr2 > bailout) {
				return i
			}
		}
		return i
	},
}


function render(messageData: WorkerJob) {

	const width = messageData["width"]
	const height = messageData["height"]
	const y = messageData["y"]

	const arrayBuffer = new ArrayBuffer(width * 32)
	const rowIterations = new Uint32Array(arrayBuffer)
	
	const yy = ((y / height) * 4.0 - 2.0)

	for (let x = 0; x < width; x++) {
	
		const xx = (x / width) * 4.0 - 2.0 - 0.5
		rowIterations[x] = Mandelbrot.iterate(xx, yy)
	}
	
	const result: WorkerResponse = {
		y: y,
		row: arrayBuffer
	}

	postMessage(result, [result.row])
}

self.onmessage = function (msg: MessageEvent) {
	const messageData:WorkerJob = msg.data
	render(messageData)
}