42. Mini project: a multi-threaded Mandelbrot renderer (worker pool + SAB)
// mandelbrot-worker.js — computes one horizontal STRIP of the image into shared memory
self.onmessage = ({ data: { sab, width, height, startRow, endRow, maxIter } }) => {
const pixels = new Uint8ClampedArray(sab);
for (let y = startRow; y < endRow; y++) {
for (let x = 0; x < width; x++) {
const cx = (x / width) * 3.5 - 2.5;
const cy = (y / height) * 2 - 1;
let zx = 0, zy = 0, iter = 0;
while (zx * zx + zy * zy < 4 && iter < maxIter) {
const xtemp = zx * zx - zy * zy + cx;
zy = 2 * zx * zy + cy;
zx = xtemp;
iter++;
}
const idx = (y * width + x) * 4;
const shade = (iter / maxIter) * 255;
pixels[idx] = pixels[idx + 1] = pixels[idx + 2] = shade;
pixels[idx + 3] = 255;
}
}
self.postMessage({ startRow, endRow }); // just a completion signal — pixel data is ALREADY
}; // in the shared buffer, no copy needed back
// main.js — splits the image into N horizontal strips, one per worker, writing
// directly into one SharedArrayBuffer (requires COOP/COEP headers, see §39)
async function renderMandelbrot(canvas, maxIter = 300) {
const { width, height } = canvas;
const sab = new SharedArrayBuffer(width * height * 4);
const workerCount = navigator.hardwareConcurrency || 4;
const rowsPerWorker = Math.ceil(height / workerCount);
const jobs = Array.from({ length: workerCount }, (_, i) => {
const worker = new Worker("mandelbrot-worker.js");
const startRow = i * rowsPerWorker;
const endRow = Math.min(startRow + rowsPerWorker, height);
return new Promise((resolve) => {
worker.onmessage = () => { worker.terminate(); resolve(); };
worker.postMessage({ sab, width, height, startRow, endRow, maxIter });
});
});
await Promise.all(jobs); // all strips computed IN PARALLEL across real CPU cores
const imageData = new ImageData(new Uint8ClampedArray(sab), width, height);
canvas.getContext("2d").putImageData(imageData, 0, 0);
}
On an 8-core machine this renders roughly 8x faster than a single-threaded
loop, and — critically — the main thread never blocks, so the page stays
scrollable and responsive throughout the entire computation, unlike running
the same loop directly in a <script>.