Performance Optimisation
Profile your sketch, identify GPU vs CPU bottlenecks, reduce draw calls, use workers, and hit 60fps with thousands of elements.
Measure before optimising
The browser’s DevTools are your first tool. In Chrome:
- Open DevTools (
F12) - Go to Performance tab
- Click Record, run your sketch for a few seconds, stop
- Examine the flame chart — long bars in JavaScript are CPU bottlenecks; GPU issues show up as long frame render times
Also check the Rendering tab: enable “Frame Rendering Stats” to see FPS and GPU memory.
// Quick in-sketch FPS meter
function draw() {
// ... your sketch ...
// Overlay FPS
fill(255, 0); // white text
fill(255);
noStroke();
textSize(12);
textAlign(LEFT, TOP);
text(`${floor(frameRate())} fps`, 5, 5);
}
Common bottlenecks
1. Too many draw calls
Every circle(), rect(), line() is a draw call. Thousands of separate calls add CPU overhead.
Solution: batch with beginShape()
// ❌ 500 separate draw calls
for (let p of particles) {
circle(p.x, p.y, p.r * 2);
}
// ✅ One draw call with beginShape(POINTS)
strokeWeight(8);
stroke(200, 100, 255);
beginShape(POINTS);
for (let p of particles) {
vertex(p.x, p.y);
}
endShape();
2. loadPixels() in every frame
Calling loadPixels() + updatePixels() syncs the CPU and GPU — expensive.
Solution: move pixel processing to a shader.
3. Unnecessary object creation
Creating objects inside draw() forces the garbage collector to work every frame.
// ❌ Creates a new p5.Vector every frame
function draw() {
let gravity = createVector(0, 0.1); // new allocation every frame
}
// ✅ Allocate once
let gravity;
function setup() {
gravity = createVector(0, 0.1);
}
4. String concatenation in draw()
// ❌ Creates intermediate strings every frame
text('FPS: ' + floor(frameRate()) + ' | Count: ' + particles.length, 10, 20);
// ✅ Template literal — marginally faster, but pre-format rarely-changing text
let statsText = '';
if (frameCount % 30 === 0) {
statsText = `FPS: ${floor(frameRate())} | Count: ${particles.length}`;
}
text(statsText, 10, 20);
Web Workers for heavy computation
Move CPU-intensive work (pathfinding, simulation, data processing) off the main thread:
// main.js
const worker = new Worker('worker.js');
worker.onmessage = function(e) {
// Receive computed results
particles = e.data.particles;
};
// Every N frames, send data to worker
if (frameCount % 5 === 0) {
worker.postMessage({ particles, width, height });
}
// worker.js
self.onmessage = function(e) {
let { particles, width, height } = e.data;
// Compute physics update (no p5 APIs available here — pure JS)
let updated = particles.map(p => ({
...p,
x: p.x + p.vx,
y: p.y + p.vy,
vx: (p.x < 0 || p.x > width) ? -p.vx : p.vx,
vy: (p.y < 0 || p.y > height) ? -p.vy : p.vy
}));
self.postMessage({ particles: updated });
};
Workers run in parallel — physics runs at full speed without blocking the render loop.
Spatial partitioning
Avoid O(N²) checks. A simple grid reduces nearest-neighbour queries to O(N):
class Grid {
constructor(cellSize) {
this.s = cellSize;
this.g = {};
}
clear() { this.g = {}; }
insert(x, y, data) {
let k = `${Math.floor(x / this.s)},${Math.floor(y / this.s)}`;
(this.g[k] = this.g[k] || []).push(data);
}
query(x, y, radius) {
let r = [], cells = Math.ceil(radius / this.s);
let gx = Math.floor(x / this.s), gy = Math.floor(y / this.s);
for (let dx = -cells; dx <= cells; dx++)
for (let dy = -cells; dy <= cells; dy++)
(this.g[`${gx + dx},${gy + dy}`] || []).forEach(d => r.push(d));
return r;
}
}
WEBGL-specific optimisations
In WEBGL mode:
- Texture atlases — pack multiple images into one texture, switch between them using UV offsets
- Avoid
fill()/stroke()changes between shapes — state changes are expensive - Pre-compile shaders in
setup(); creating shaders indraw()is very slow - Reuse geometry — use
model()with a cached 3D model instead of redrawing withbeginShape() pixelDensity(1)— on retina displays, default is 2 (4× the pixels to fill)
function setup() {
pixelDensity(1); // significant GPU load reduction on retina
createCanvas(800, 600, WEBGL);
}
frameRate() target and delta time
Target lower frame rates for heavy sketches:
function setup() {
createCanvas(800, 600);
frameRate(30); // target 30fps
}
Use delta time for physics that should be frame-rate independent:
let lastTime = 0;
function draw() {
let now = millis();
let dt = (now - lastTime) / 1000; // seconds since last frame
lastTime = now;
// Scale movement by dt — same speed at any frame rate
for (let p of particles) {
p.x += p.vx * dt * 60; // * 60 so "velocity" is still in units/frame at 60fps
p.y += p.vy * dt * 60;
}
}
Key takeaways
- Profile first — use browser DevTools before guessing where the bottleneck is
- Batch draw calls with
beginShape(POINTS/LINES/TRIANGLES)instead of individual shape calls - Avoid allocating objects inside
draw()— pre-allocate and reuse - Web Workers move heavy computation off the main thread without blocking rendering
- Spatial partitioning (grid, quadtree) reduces O(N²) checks to O(N)
pixelDensity(1)is a free 4× GPU load reduction on retina displays- Delta time makes physics frame-rate independent