p5.js Instance Mode: Multiple Sketches Without Global Pollution
p5.js Instance Mode lets you run multiple independent sketches on one page without global namespace pollution. Each sketch gets its own canvas, animation loop, and event handlers — essential for React/Vue integration and dashboard-style visualizations.
18 Feb 2026, 22:15 UTC

The Global Mode Problem
If you've built anything beyond a single canvas with p5.js, you've likely hit the global mode wall. The default setup()/draw() functions attach to window, so a second sketch overwrites the first. Event handlers collide. Tree-shaking fails because bundlers can't tell which p5 methods you actually use. And embedding in React or Vue means fighting the component lifecycle — cleanup is manual, props are inaccessible inside the sketch, and server-side rendering throws ReferenceError: document is not defined.
Instance Mode, stable since p5.js 1.0 (2020), wraps the entire API in a constructor: new p5(sketch, container). Each call returns an independent p5 instance with its own canvas, animation loop, and event namespace. No global p5 object is created.
How Instance Mode Works
The sketch function receives a p5 instance — conventionally named p. Every drawing call becomes a method on that instance:
const sketch = (p) => {
p.setup = () => {
p.createCanvas(400, 300);
p.background(240);
};
p.draw = () => {
p.ellipse(p.mouseX, p.mouseY, 20, 20);
};
};
new p5(sketch, 'canvas-container');Key differences from global mode:
- Explicit scope:
p.random(),p.map(),p.mousePressed()— no implicit globals. - Container targeting: The second argument accepts a DOM element, selector string, or
null(appends tobody). - Independent loops:
p.noLoop()pauses only that instance. - Cleanup:
p.remove()stops the animation frame, removes the canvas, and detaches event listeners.
Worked Example: Dashboard with Two Visualizations
Imagine a monitoring dashboard showing a particle system and a scrolling line chart, both fed by the same WebSocket stream. Each visualization lives in its own <div> and runs as a separate p5 instance.
HTML
<div class="dashboard">
<div id="particles"></div>
<div id="chart"></div>
</div>JavaScript (ES modules)
import p5 from 'p5';
// Shared WebSocket connection
const ws = new WebSocket('wss://example.com/telemetry');
ws.onmessage = (e) => {
const data = JSON.parse(e.data);
particleInstance.receive(data);
chartInstance.receive(data);
};
// Particle system sketch
const particleSketch = (p) => {
let particles = [];
p.setup = () => {
p.createCanvas(400, 300);
p.background(10);
};
p.draw = () => {
p.background(10, 50); // fade trail
particles.forEach((pt, i) => {
pt.update();
pt.show(p);
if (pt.dead()) particles.splice(i, 1);
});
};
p.receive = (data) => {
if (data.type === 'particle') {
particles.push(new Particle(p, data.x, data.y));
}
};
};
// Scrolling line chart sketch
const chartSketch = (p) => {
const history = [];
const maxPoints = 200;
p.setup = () => {
p.createCanvas(400, 200);
p.background(255);
};
p.draw = () => {
p.background(255);
p.stroke(0, 120, 255);
p.noFill();
p.beginShape();
history.forEach((val, i) => {
const x = p.map(i, 0, maxPoints, 0, p.width);
const y = p.map(val, 0, 100, p.height, 0);
p.vertex(x, y);
});
p.endShape();
};
p.receive = (data) => {
if (data.type === 'metric') {
history.push(data.value);
if (history.length > maxPoints) history.shift();
}
};
};
// Instantiate — each gets its own container and loop
const particleInstance = new p5(particleSketch, 'particles');
const chartInstance = new p5(chartSketch, 'chart');Both sketches subscribe to the same WebSocket but render independently. Pausing the chart (chartInstance.noLoop()) doesn't freeze the particles. If the dashboard unmounts, call particleInstance.remove() and chartInstance.remove() to reclaim memory.
Trade-offs and Limitations
| Aspect | Global Mode | Instance Mode |
|---|---|---|
| Boilerplate | Minimal | p. prefix on every call |
| Tree-shaking | Poor | Works with ES modules |
| Multiple sketches | Manual namespace hacks | Native, isolated |
| Framework integration | Fragile | Natural fit (closure over props) |
| Third-party add-ons | Auto-attach to global | Must pass instance explicitly |
| SSR (Next.js, Nuxt) | Same guard needed | Guard + p.remove() in cleanup |
Specific gotchas:
- Add-ons:
p5.soundorp5.playrequirenew p5.SoundFile(p, ...)ornew p5.Play(p, ...)— they don't auto-bind. - Keyboard events:
p.keyPressed()only fires when the instance's canvas has focus. For global shortcuts, usewindow.addEventListener('keydown', ...). - Memory leaks: Forgetting
p.remove()on component unmount leavesrequestAnimationFrameloops and canvas event listeners alive. In React, pairuseEffectwith a cleanup function. - SSR guard: Wrap instantiation in
if (typeof window !== 'undefined')or use dynamic import withssr: false.
Migration Checklist
- Wrap your sketch in a function that accepts
p. - Prefix every p5 call with
p.(createCanvas→p.createCanvas,random→p.random). - Replace global
setup()/draw()withp.setup = () => {}andp.draw = () => {}. - Instantiate with
new p5(sketch, containerRef). - Call
instance.remove()on teardown (React:return () => instance.remove()inuseEffect). - Update add-on imports to pass the instance explicitly.
- Verify tree-shaking: run
npm run buildand check the bundle for unused p5 methods.
Start small — convert one sketch, confirm independent loops and cleanup, then scale. The official reference at p5js.org/reference/#/p5/p5 documents the constructor signature and every instance method.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.