The Mandelbrot set is both very accessible and also a deep rabbit hole. Given less than an hour’s introduction to complex numbers, you can appreciate how the Mandelbrot set is created. Given many more hours, you can watch all the renders of it people have posted to YouTube. Given days or months or years, you can learn quite a lot about holomorphic dynamics and high precision and high performance computing.

Originally this page had only the simplest possible implementations of Mandelbrot set explorers using vanilla JS and WebGL. Those were in January 2025. Over time, I think I’ll revisit this page and go one stratum deeper down the Mandelbrot rabbit hole, and hopefully share what I learn.

The first update was in November 2025. I added a Julia set explorer, and allowed for comparing and contrasting functional/imperative implementations of the JS explorers. Next steps would be multithreading the vanilla explorers, adding higher precision numerics in the webgl shader, and trying perturbation theory.

What is the Mandelbrot Set?

The Mandelbrot set is produced by creating a sequence of complex numbers. The sequence is parametrized by an arbitrary complex number c. The sequence always starts at z0 = 0. The recursion relation for getting all subsequent numbers is z_{n + 1} = z_n² + c. This sequence will tell us if c is in the Mandelbrot set.

The magnitude of z_n as n tends to infinity will either be bounded or unbounded. In principle, it’s impossible to compute this in finite time. In practice, we can pick an upper bound on the number of iterations, and if we find that we haven’t diverged by then, we can estimate, at our level of granularity, c is in the set.

If the sequence ever reaches a number with modulus greater than 4, you can be sure that the sequence will diverge. You can color points differently based on how many iterations it takes for them to diverge, and that’s where all the fancy fractal color patterns come from.

Implementations

The Vanilla Javascript version

This Mandelbrot set marks a milestone in my development as a computer scientist. With it, I became a javascript programmer.

Below are two canvases that render the Mandelbrot set using 2d canvas contexts. I come from C, so I was very excited to try out the academic, functional features of JavaScript. To make it interesting, I thought I’d compare performance with a no-frills imperative implementation that does everything straightforwardly inline. To zoom in on a point, hover your mouse over it. The canvases are only drawn when hovered over.

The functional approach was more fun to write, and is probably a more mathematically satisfying way to think about what’s happening. I also used objects to represent complex numbers, so if you’re an academic programmer, you’ll find it more satisfying than two JS numbers simply manipulated inline.

function new_complex_number(real, imag) {
    return { real: real, imag: imag };
}

function generate_mandelbrot_set_function(c) {
    return (z) => {
        const z1 = multiply_complex(z, z);
        return add_complex(z1, c);
    }
}

function functional_iterate(real, imag) {
    const c = new_complex_number(real, imag);
    const mandelfunc = generate_mandelbrot_set_function(c);
    let z = new_complex_number(0, 0);
    let iterations = 0;

    while (iterations < max_iterations_vanilla && norm2(z) < 4) {
        z = mandelfunc(z)
        iterations++;
    }

    return iterations;
}

Being able to create a new function for each c, and then writing z = mandelfunc(z) reads very nicely.

Alternatively, we have the workman’s implementation:

function imperative_iterate(real, imag) {
    let iterations = 0;
    let re = 0;
    let im = 0;
    let re2 = 0;
    let im2 = 0;

    while (iterations < max_iterations_vanilla && (re2 + im2 < 4)) {
        const temp_real = re2 - im2 + real;
        im = 2 * re * im + imag;
        re = temp_real;
        re2 = re * re;
        im2 = im * im;
        iterations++;
    }

    return iterations;
}

Hover over either canvas below to do some zooming, and see how the FPS for each of the implementations compares. 50 iterations per pixel are performed.

Functional: 0.00 FPS

Imperative: 0.00 FPS

Unfortunately for academic beauty, the inline version seems to perform roughly 50–100% faster. I’d have to imagine this comes from JS allocating all these tiny objects, and incurring another indirection for each call to the mandelfunc. This is on my 2020 M1 Macbook Pro.

Another interesting performance note for new JavaScript devs: using canvas.clientWidth is significantly slower than using canvas.width, by a factor of about half. I’m not 100% sure why this is, but supposedly it has something to do with clientWidth involving a calculation by the browser, whereas width is a simple read of a cached value.

Using WebGL

With this, I became a shader artist.

Similar to the above, but parallelism granted by the GPU. Iterations per pixel are 10k. The zoom level is clamped to 1e-5. I’m a little disappointed by the shallowness that you get for free — or at least the shallowness that my code breaks down at.

One day, I’d like to revisit this with some higher precision numerics. I’d like to try emulated double precision in the shader, as well as implementing a fixed point data type. Given that the sequence is guaranteed to blow up if it’s magnitude ever exceeds 4, we only need two or three bits before the decimal, then we could use 29 or 30 after, or even more with some more effort.

0.00 FPS

Delta real: 2.5

Julia Sets

In the Mandelbrot set, we used the recursion relation z_{n + 1} = z_n² + c, and varied c across the complex plane. For a given c, we set z0 = 0 and iterated until we blew up or hit our limit on iterations. This let us color the complex plane according to whether or not c was in the Mandelbrot set.

A similar exercise is to fix c, and then vary z0 over the complex plane. Then we color the plane according to whether or not the sequence blows up.

I think this is slightly imprecise, but the set of points that blow up are called Julia Sets for that value of c. You can click on the right canvas to move c around, and then explore its Julia set on the left.

Real: 0.00, Imaginary: 0.00

References

Here are some resources for learning about this stuff. These have helped me understand so far, and these have more details I’d like to come back to in the future.