Using p5.Graphics for Efficient Off‑Screen Rendering in p5.js
Learn how to use p5.Graphics in p5.js to create off‑screen buffers for static backgrounds, filters, and layered effects while managing memory trade‑offs.
11 Jan 2026, 12:32 UTC

Problem: Redrawing static content every frame wastes cycles
When a sketch contains elements that never change — such as a background, a UI panel, or a pre‑computed texture — drawing them inside draw() forces the browser to repeat the same work 60 times per second. This adds unnecessary load, especially on low‑power devices, and can limit the complexity of animated parts.
Thesis: Off‑screen buffers (p5.Graphics) let you draw once and reuse the result
By creating an off‑screen graphics buffer with createGraphics(), you can render static content a single time, store the pixel data, and composite it onto the main canvas each frame with image(). The buffer behaves like a miniature canvas: it supports most 2D drawing commands, blending modes, and filters.
How p5.Graphics works
- Creation:
const buf = createGraphics(w, h);returns a graphics object whose internal buffer isw × h × 4bytes (RGBA). - Drawing: All p5 drawing functions (
ellipse,rect,image,filter, etc.) can be called onbufjust like on the main canvas. - Compositing: In
draw(), place the buffer withimage(buf, x, y). You can also tint or blend it usingtint()or theblend()method.
Worked example: static background with moving shapes
The following sketch creates a 400 × 400 off‑screen buffer, draws a gradient background and a fixed ellipse once, then animates a set of rotating squares over it.
let bg;
let angle = 0;
function setup() {
createCanvas(400, 400);
// create off‑screen buffer
bg = createGraphics(width, height);
bg.noStroke();
// gradient background (drawn once)
for (let y = 0; y < height; y++) {
const inter = map(y, 0, height, 0, 1);
const c = lerpColor(color(30, 30, 80), color(10, 10, 40), inter);
bg.stroke(c);
bg.line(0, y, width, y);
}
// static ellipse
bg.fill(200, 100, 100);
bg.ellipse(width / 2, height / 2, 150, 150);
}
function draw() {
// copy the buffer onto the main canvas
image(bg, 0, 0);
// animated layer
push();
translate(width / 2, height / 2);
rotate(angle);
for (let i = 0; i < 4; i++) {
rotate(HALF_PI / 2);
fill(100, 200, 255, 180);
rect(-20, -60, 40, 120);
}
pop();
angle += 0.02;
}
Expected behavior: the gradient and ellipse remain fixed while the squares rotate. Because the buffer is drawn only once in setup(), the per‑frame workload consists of copying the buffer and rendering the animated layer.
Trade‑off: memory consumption vs. CPU savings
Each buffer occupies width × height × 4 bytes. For a 400 × 400 buffer that is about 640 KB. Adding multiple buffers (e.g., for parallax layers) scales memory linearly. If memory becomes a concern on target devices, you can:
- Reduce buffer dimensions to the smallest size that still covers the needed area.
- Dispose of a buffer when no longer needed by setting the variable to
null (allowing garbage collection). - Recreate buffers only when the canvas size changes; note that
resizeCanvas()does not affect existing buffers.
To verify memory impact, open Chrome DevTools → Performance → Memory, take a heap snapshot before and after creating buffers, and observe the increase roughly matching 4 × w × h × bufferCount bytes.
Limitations and practical checks
- Size changes: You cannot resize a
p5.Graphicsobject after creation; changing dimensions requirescreateGraphics(newW, newH)and discarding the old buffer. - WebGL mode: By default, buffers are 2D canvases. If your main sketch uses WEBGL, create the buffer with
createGraphics(w, h, WEBGL)to match contexts; otherwise mixing 2D drawing calls on a WEBGL buffer may produce unexpected results. - Verification: After drawing to a buffer, call
image(buf, 0, 0)and visually confirm that the content appears. For filters, draw a noisy texture into the buffer each frame, applybuf.filter(BLUR, 2), then composite; the blur should be uniform without regenerating noise.
Actionable closing
Start by identifying any static visual part of your sketch — background, UI, or pre‑computed texture. Move its drawing commands into a setup()‑time p5.Graphics buffer, composite it each frame with image(), and measure the frame‑time improvement. Monitor memory usage to ensure the trade‑off fits your target devices, and recreate buffers only when the canvas size truly changes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.