Using p5.js createGraphics to Offload Static Drawing and Boost Frame Rate
Learn how to use p5.js createGraphics to move static drawing off the main draw loop, boost frame rates, and manage GPU memory trade‑offs.
18 Sept 2025, 02:04 UTC

Problem: Redrawing static content every frame wastes cycles
When a p5.js sketch draws a detailed background, UI elements, or any artwork that rarely changes inside the draw() loop, the GPU repeats the same work 60 times per second (or more). On modest hardware this can push the frame rate below a smooth experience, especially when you also need to animate many sprites or process user input.
Thesis: Move unchanging graphics into an offscreen buffer with createGraphics() and composite it each frame
The createGraphics() function returns a separate p5.Renderer that behaves like its own canvas. You can draw to it once (or infrequently), then render the resulting image onto the main canvas with image(). Because the buffer is drawn only when its contents change, the per‑frame workload drops to just the dynamic parts of your sketch.
Worked example: Static noise background + moving circles
Below is a minimal sketch you can paste into the p5.js web editor or run locally with a local server. It first shows the naive approach (background drawn each frame) and then the optimized version using an offscreen buffer.
let bgBuffer; // offscreen graphics buffer
let circles = []; // dynamic objects
function setup() {
createCanvas(800, 600);
pixelDensity(1); // keep consistent with display
// ---- create the offscreen buffer once ----
bgBuffer = createGraphics(width, height);
bgBuffer.noiseDetail(4, 0.5);
bgBuffer.loadPixels();
for (let i = 0; i < bgBuffer.pixels.length; i += 4) {
const val = random(255);
bgBuffer.pixels[i] = val; // R
bgBuffer.pixels[i + 1] = val; // G
bgBuffer.pixels[i + 2] = val; // B
bgBuffer.pixels[i + 3] = 255; // A
}
bgBuffer.updatePixels();
// ---- initialise dynamic sprites ----
for (let i = 0; i < 200; i++) {
circles.push({
x: random(width),
y: random(height),
vx: random(-2, 2),
vy: random(-2, 2),
r: random(5, 15)
});
}
}
function draw() {
// 1️⃣ draw the static background from the buffer (cheap)
image(bgBuffer, 0, 0);
// 2️⃣ update and draw moving circles
for (let c of circles) {
c.x += c.vx;
c.y += c.vy;
if (c.x < 0 || c.x > width) c.vx *= -1;
if (c.y < 0 || c.y > height) c.vy *= -1;
fill(255, 100, 0, 180);
noStroke();
ellipse(c.x, c.y, c.r * 2);
}
// optional: show current FPS in the corner
fill(0);
textSize(14);
text(`FPS: ${frameRate().toFixed(1)}`, 10, 20);
}
Where to run: Any browser with JavaScript enabled; no special permissions are required. If you run the sketch locally, serve the folder with a simple static server (e.g., python -m http.server) to avoid file‑origin restrictions.
What to check: Open the browser’s developer tools, go to the Performance tab, record a few seconds, and observe the average FPS. You should see a higher and more stable FPS when the background is sourced from bgBuffer compared to a version where the noise loop lives directly in draw().
Trade‑off and limitation: GPU memory vs. CPU savings
Each offscreen buffer occupies GPU memory roughly equal to width × height × pixelDensity × 4 bytes (RGBA8). On low‑end devices, creating several large buffers can quickly exhaust available memory, leading to throttling or crashes. The technique is therefore best suited for:
- Backgrounds, UI frames, or tile‑maps that are static or change infrequently.
- Buffers whose dimensions match the canvas size (or are a small multiple).
Practical way to verify memory impact: In Chrome DevTools, open the Memory panel, take a heap snapshot before and after creating the buffer, and look for an increase in WebGLRenderingContext‑related objects. If the increase is disproportionate to your buffer size, consider reducing the buffer’s dimensions or reusing a single buffer for multiple layers.
Actionable closing: Adopt the pattern when you see repeated static work
- Identify drawing code in
draw()that produces identical output frame‑after‑frame (e.g., backgrounds, decorative borders, pre‑rendered textures). - Move that code into a
setup()block that draws to acreateGraphics()buffer. - Replace the original drawing calls with a single
image(buffer, 0, 0)at the start ofdraw(). - If the static content must update (e.g., a settings panel), redraw the buffer only when the change occurs and call
buffer.clear()before redrawing to avoid stale pixels. - Monitor FPS and GPU memory to ensure the trade‑off stays favorable on your target hardware.
By leveraging p5.js’s offscreen rendering buffer, you keep the visual fidelity of a rich static scene while freeing CPU/GPU cycles for the truly dynamic parts of your sketch—resulting in smoother animations and a more responsive experience.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.