Skip Redrawing Static Backgrounds in p5.js with Offscreen Buffers
Render static p5.js backgrounds once in an offscreen buffer with createGraphics(), then composite them each frame while animating only the foreground.
14 Oct 2025, 11:10 UTC

The per-frame cost you are probably paying
Every call to draw() in p5.js runs up to 60 times per second. If that function redraws a detailed background grid, map, or pattern on every frame, the browser spends time repainting pixels that never change. That leaves less budget for the moving elements and can cause dropped frames as the sketch grows.
Draw static layers once into an offscreen buffer
createGraphics(w, h) returns a p5.Graphics object: an offscreen renderer with the same drawing API as the main canvas. Think of it as a second canvas that stays hidden until you copy it onto the visible one.
You draw expensive static geometry into that buffer once, usually in setup(). Each frame, you call image(buffer, 0, 0) to composite the buffer onto the main canvas, then draw only the changing foreground elements on top. The buffer does not track the main canvas, so you must explicitly redraw into it when the background itself changes.
Worked example: static grid with moving circles
Save the following as index.html, open it in a browser, and open DevTools (usually F12 or right-click > Inspect). No special permissions are required. Pin a specific p5.js version in production instead of using @latest.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>p5.js offscreen buffer</title>
<script src="https://cdn.jsdelivr.net/npm/p5@latest/lib/p5.js"></script>
</head>
<body></body>
<script>
let buffer;
let x = 50;
function setup() {
createCanvas(600, 400);
// Allocate once. Never create large buffers inside draw().
buffer = createGraphics(600, 400);
// Expensive static geometry, drawn one time.
buffer.background(245);
buffer.stroke(200);
for (let i = 0; i <= 600; i += 40) {
buffer.line(i, 0, i, 400);
buffer.line(0, i, 600, i);
}
console.log("Static buffer rendered"); // Verify this prints once
}
function draw() {
// Composite the pre-rendered background.
image(buffer, 0, 0);
// Only the foreground changes each frame.
fill(255, 80, 80);
noStroke();
circle(x, 200, 40);
x = (x + 2) % 620;
}
</script>
</html>
In DevTools Console, confirm Static buffer rendered appears once when the page loads, not repeatedly. The grid should remain fixed while the red circle moves across it. If you resize the window, the buffer stays at 600 by 400 pixels unless you recreate or rescale it, so expect a mismatch between the buffer and a resized main canvas.
Trade-offs and limits
- Graphics memory: Each buffer consumes video memory. Size it only as large as needed and release buffers you no longer use.
- Stale content: The main canvas shows whatever was last drawn into the buffer. If the background should change, redraw into the buffer explicitly;
draw()alone will not refresh it. - High-DPI and zoom: Buffer alignment can depend on
pixelDensity()and browser zoom. Lines or text may shift between the buffer and main canvas at different display scales. Verify at the target zoom before relying on pixel-perfect overlap.
Quick verification checklist
- Open DevTools Console and reload the page. The setup-only log message should appear exactly once.
- Watch the frame rate or visual smoothness while adding more foreground elements; static background cost should not grow with them.
- Resize the browser and check whether the buffer still covers the visible canvas. If not, handle
windowResizedto recreate or scale the buffer. - Open the sketch at another zoom level and compare buffer edges against main-canvas edges for alignment.
For sketches with expensive unchanging art, an offscreen buffer moves that work out of the per-frame loop. For animations where everything changes every frame, the buffer adds memory cost without a corresponding win, so measure before you adopt it.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.