Frontend Labs
⋯

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>.


Generated from real-life-JS-guide.md · 8 Oct 2026 16:36