When Your Processing Sketch Crawls: Baking Geometry into PShape
Immediate-mode drawing re-sends every shape each frame, which is why dense Processing sketches crawl. Baking static geometry into a PShape group uploads it once and redraws it in a single call.
17 Aug 2026, 11:20 UTC

Your sketch runs fine with 500 points. At 5,000 it stutters. At 20,000 — a scatter plot, a map, a particle field — the frame rate collapses into single digits and the fans spin up. The usual culprit isn't your math. It's that Processing's default drawing model re-issues every shape, every frame, from scratch — and the fix is a one-time decision: move static geometry into retained PShape objects.
Immediate mode is the bottleneck
By default, Processing renders in immediate mode: each call to point(), ellipse(), or line() inside draw() sends geometry to the renderer again, 60 times a second. That's fine for a few hundred primitives. But the cost scales linearly with vertex count, and once you push tens of thousands of vertices per frame, the CPU-to-GPU chatter dominates everything else in the sketch.
The key insight: if the geometry doesn't change between frames, re-sending it is pure waste. A scatter plot of fixed data points looks identical on frame 1 and frame 10,000 — yet immediate mode pays the full upload cost every single time.
PShape: upload once, redraw cheaply
PShape is Processing's retained-mode geometry container. You build it once — typically in setup() via createShape() — and the geometry is stored so that drawing it later with shape(myShape) is a single cheap operation instead of thousands of individual draw calls.
The pattern that matters most is grouping: combine many primitives into one PShape of kind GROUP, and hundreds of separate draw calls collapse into one. This is the standard fix for frame-rate collapse in particle fields, maps, and data visualizations whose geometry is fixed.
Worked example: 20,000-point scatter plot
Here's the immediate-mode version that struggles — run it in the Processing IDE, no special permissions needed, and watch the console:
// Immediate mode: slow at scale\nfloat[] xs, ys;\n\nvoid setup() {\n size(1000, 700, P2D);\n xs = new float[20000];\n ys = new float[20000];\n for (int i = 0; i < xs.length; i++) {\n xs[i] = random(width);\n ys[i] = random(height);\n }\n}\n\nvoid draw() {\n background(255);\n stroke(0);\n for (int i = 0; i < xs.length; i++) {\n point(xs[i], ys[i]); // 20,000 draw calls, every frame\n }\n if (frameCount % 60 == 0) println(frameRate);\n}And the retained-mode version. The points are baked once in setup(); draw() issues a single call:
// Retained mode: bake once, redraw cheaply\nfloat[] xs, ys;\nPShape cloud;\n\nvoid setup() {\n size(1000, 700, P2D);\n xs = new float[20000];\n ys = new float[20000];\n cloud = createShape(GROUP);\n PShape pts = createShape();\n pts.beginShape(POINTS);\n pts.stroke(0);\n for (int i = 0; i < xs.length; i++) {\n xs[i] = random(width);\n ys[i] = random(height);\n pts.vertex(xs[i], ys[i]);\n }\n pts.endShape();\n cloud.addChild(pts);\n}\n\nvoid draw() {\n background(255);\n shape(cloud); // one draw call\n if (frameCount % 60 == 0) println(frameRate);\n}On typical hardware the first version drops to single-digit frame rates while the second renders smoothly — but treat any specific number as illustrative, not a guarantee. Verify on your own machine: run both sketches, compare the printed frameRate values, and confirm the visual output matches. Also check the createShape() reference in your installed IDE — PShape API details have shifted between Processing 3 and 4, so test on your target release.
The trade-offs you accept
Retained geometry isn't free, and the decision has real limits:
- Memory. Baked vertices live in memory (and, with OpenGL renderers, on the GPU). Millions of retained vertices can exhaust heap or GPU memory — profile before and after if you're pushing scale.
- Mutation erases the win.
PShapesupports per-vertex editing (setVertex(),setFill()), but if you mutate the shape every frame, you largely forfeit the performance advantage over immediate mode. Highly dynamic geometry is often better served by direct vertex buffers, point sprites, or shaders in P3D. - Renderer matters. The gains above assume
P2DorP3D(the OpenGL-backed renderers). With the defaultJAVA2Drenderer the speedup is smaller and some PShape features behave differently — always state which renderer your sketch uses, and test under both if it matters.
A decision rule you can apply today
Ask one question per sketch: does this geometry change between frames? If it's static or changes rarely, bake it into a PShape group in setup(). If it changes every frame, keep it dynamic and consider lower-level approaches. Pick one slow sketch you already have, convert its hottest drawing loop to a retained shape, and measure the frame rate before and after — that fifteen-minute experiment will tell you more than any benchmark table.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.