I’ve been building a small programming language and needed a project that was visual, demanding, and unforgiving about performance. Fractals turned out to be the perfect testbed: they’re embarrassingly parallel, heavy on floating‑point work, and any bug shows up immediately as a glitch on the screen.
In this post I’ll walk through how I wired a Fractal explorer in the browser using:
- A homegrown language as the “authoring layer”
- WebAssembly as the execution target
- A minimal JavaScript shell for input and rendering
Why Fractals Make Great Language Benchmarks
From a distance, a fractal is just an image. From the runtime’s point of view, it’s a brutal little benchmark:
- Millions of pixels, same loop shape, different data
- Lots of floating‑point operations
- Clear performance feedback: do users see results in milliseconds or seconds?
That combination makes fractals a nice stress test for:
- Code generation quality (are we emitting tight loops?)
- Numeric semantics (are we handling edge cases and precision consistently?)
- Runtime model (do we block the UI thread, do we stream, can we refine progressively?)
Unlike a synthetic benchmark, you also get something beautiful at the end, which keeps motivation high.
Architecture: Language → WASM → Canvas
The overall architecture looks like this:
- Write fractal logic in my language
- Compile to WebAssembly.
- Call the exported functions from JavaScript.
- Stream results into an
ImageDatabuffer on a<canvas>.
In more detail:
Language layer
Describes the per‑pixel computation: given coordinates and parameters, return an “escape value” that we’ll later map to color.-
WebAssembly module
Exposes functions like:-
render_region(centerX, centerY, scale, width, height, maxIterations) - Writes raw iteration counts into a linear memory buffer.
-
-
JavaScript shell
- Handles input (mouse, touch, wheel).
- Translates viewport changes into calls to
render_region. - Maps iteration counts → RGBA and blits to the canvas.
The nice thing about this layering: I can iterate on the language and the fractal formulas without touching the UI.
Authoring Fractals in a Custom Language
My language’s job here is: “describe what one pixel should do.” The core concept is a pure‑ish function that takes a coordinate and some parameters and returns a scalar:
- Input: coordinate in the complex plane, global parameters (max iterations, escape radius, etc.).
- Output: a number representing how “quickly” the point escapes or stabilizes.
Conceptually, the code you’d write in a conventional language looks like this:
// PSEUDOCODE – the real syntax is my language
fn iterate(x, y, params) -> int {
// map (x, y) to complex plane
// apply small iterative rule until escape or max iterations
// return how long it took
}
In the language, I deliberately kept:
- No classes, minimal state
- Simple control flow
- Explicit numeric types
Because the target is WebAssembly, avoiding hidden allocations and dynamic dispatch makes code generation much simpler.
WebAssembly: Trade‑offs and Lessons
Compiling to WebAssembly gives you:
- Near‑native inner loops in the browser
- A simple, linear memory model
- Tooling that’s getting better every year
For this project, a few practical lessons:
-
Linear memory as a frame buffer
I allocate a chunk of WASM memory as a raw
u32buffer. Each pixel gets one slot, which I fill with an integer “escape value.” JavaScript reads that buffer and maps it to colors. Progressive refinement
Instead of rendering full quality in one go, I can render in passes: coarse grid first, then fill in gaps. This pattern gives users immediate feedback while the WASM loops keep working.Responsiveness vs. throughput
You quickly learn to balance batch sizes. Large chunks make WASM efficient but can freeze the main thread; smaller chunks keep the UI snappy but add overhead. A simple “time budget per frame” works well enough.
If you want to go further, WebAssembly threads and SharedArrayBuffer open the door to true multi‑core rendering, but I kept this explorer single‑threaded to focus on the language itself.
The JavaScript Shell: Minimal but Important
JavaScript is the glue layer:
- Sets up the
<canvas>and a 2D context. - Manages the viewport (center, zoom).
- Handles user input (drag, wheel, touch gestures).
- Calls into WASM with the current viewport and reads back the buffer. A typical render cycle looks like:
- User pans or zooms.
- JS updates
centerX,centerY,scale. - JS calls
render_regionon the WASM module. - WASM fills its output buffer with iteration counts.
- JS maps those values to colors and updates
ImageData.
This separation keeps the UI code tiny and lets the language + WASM do the heavy lifting.
Using Fractals as a Language Design Tool
Beyond having a cool demo, fractals turned out to be surprisingly useful for language design:
Semantics become visible
Off‑by‑one errors, numeric issues, or small semantic changes show up as visible artifacts. Great for debugging semantics and codegen.Performance work is rewarding
Tightening a loop or changing a data layout translates directly into smoother zooms or higher iteration counts.New features can be validated interactively
Adding a new numeric type, a control‑flow construct, or a standard library function can be exercised immediately by writing a new fractal “kernel” and seeing if it behaves.
It’s an ongoing feedback loop: improvements in the language make the explorer better, and new ideas from the explorer push the language forward.

Top comments (0)