Why Bloom over p5.js

Same beginner-friendly syntax, very different ceiling

Bloom is inspired by Processing and p5.js, and for small sketches the experience is the same: write circle(x, y, r), see a circle. The difference shows up when a sketch gets big — thousands or hundreds of thousands of shapes per frame. This page lays out the measured difference and, just as importantly, the architectural reasons behind it — fairly, without overstating.

The benchmark

Headless Chromium, M1-class machine, an animated scatter of shapes from the same Bloom source, measured as sustained frames per second:

Shapes
Bloom VM + WebGL
Bloom Canvas2D
p5.js
≤ 10k
60
60
60
50k
30
18
~10
100k
15
10
<10
200k
8
4

p5.js drops below 60fps at roughly 10–15k shapes and below 30fps at roughly 20–30k. Even Bloom's plain Canvas2D path — before any WebGL — was already about 2.2× p5 at the 30–50k range. With the WebGL renderer engaged, the gap widens further as counts climb.

Try it yourself The live benchmark page runs Bloom and p5.js side by side in your browser so you can reproduce these numbers on your own hardware. For the full charted history of how Bloom reached these numbers — every optimization with its measured before/after — see Performance — How Bloom Got Fast.

Why Bloom wins

The gap is not micro-optimization — it comes from three concrete architectural differences. Each is a factual property of how the two systems draw, not a claim about code quality.

1. p5.js has no auto-batching

In p5.js, every shape is its own native draw call. Bloom's WebGL2 renderer accumulates every shape of a primitive type into one typed array and issues a single gl.drawArraysInstanced call per primitive type per frame. One draw call for 100k circles instead of 100k. This is the dominant factor at high counts.

2. Per-shape Color overhead

p5 builds and tracks color objects per shape. Bloom packs RGBA into a single 32-bit integer that the GPU unpacks for free in the shader — no per-shape allocation, no garbage to collect under a tight draw loop.

3. Redundant context writes

The naive Canvas2D approach reassigns ctx.fillStyle for every shape, and each assignment re-parses the CSS color string. Bloom's Canvas2D path diffs that state and skips unchanged writes — on a palette-reuse sketch, 156k writes instead of 360k (a 2.3× reduction). This is why even Bloom's non-WebGL path beats p5.

For the full mechanism behind all three — the instance buffer layout, the fused draw opcodes, and the WASM SIMD particle kernel that pushes 100k particles to 0.6ms/frame — see The Rendering Pipeline.

What's honest about this comparison

A fair benchmark names its own limits. These numbers are real, but they describe a specific regime:

Caveat
What it means
Vsync cap below 10k
At small counts both render comfortably and are pinned to the 60fps display refresh. The difference there is zero — Bloom's advantage only appears once a frame can't finish in 16.7ms.
Headless numbers
Measured in headless Chromium on M1-class hardware. Your absolute fps will vary with GPU, browser, and display; the relative ranking is the durable result.
Particle-workload scope
The benchmark is an animated scatter of simple primitives — the workload the WebGL and SIMD paths are built for. p5.js is a broad, mature 2D toolkit; this is not a claim that Bloom matches its breadth, only that Bloom scales this particular workload further.
The takeaway For learning, small sketches, and most creative coding, both are excellent and feel identical. If your sketch draws tens of thousands of shapes per frame, Bloom's batched WebGL renderer and SIMD particle kernel give it a markedly higher ceiling — without changing the code you write.