Files
hermes-sync/skills/creative/p5js/references/troubleshooting.md

13 KiB

Troubleshooting

Performance

Step Zero — Disable FES

The Friendly Error System (FES) adds massive overhead — up to 10x slowdown. Disable it in every production sketch:

// BEFORE any p5 code
p5.disableFriendlyErrors = true;

// Or use p5.min.js instead of p5.js — FES is stripped from minified build

Step One — pixelDensity(1)

Retina/HiDPI displays default to 2x or 3x density, multiplying pixel count by 4-9x:

function setup() {
  pixelDensity(1);        // force 1:1 — always do this first
  createCanvas(1920, 1080);
}

Use Math.* in Hot Loops

p5's sin(), cos(), random(), min(), max(), abs() are wrapper functions with overhead. In hot loops (thousands of iterations per frame), use native Math.*:

// SLOW — p5 wrappers
for (let p of particles) {
  let a = sin(p.angle);
  let d = dist(p.x, p.y, mx, my);
}

// FAST — native Math
for (let p of particles) {
  let a = Math.sin(p.angle);
  let dx = p.x - mx, dy = p.y - my;
  let dSq = dx * dx + dy * dy;  // skip sqrt entirely
}

Use magSq() instead of mag() for distance comparisons — avoids expensive sqrt().

Diagnosis

Open Chrome DevTools > Performance tab > Record while sketch runs.

Common bottlenecks:

  1. FES enabled — 10x overhead on every p5 function call
  2. pixelDensity > 1 — 4x pixel count, 4x slower
  3. Too many draw calls — thousands of ellipse(), rect() per frame
  4. Large canvas + pixel operationsloadPixels()/updatePixels() on 4K canvas
  5. Unoptimized particle systems — checking all-vs-all distances (O(n^2))
  6. Memory leaks — creating objects every frame without cleanup
  7. Shader compilation — calling createShader() in draw() instead of setup()
  8. console.log() in draw() — DOM write per frame, destroys performance
  9. DOM manipulation in draw() — layout thrashing (400-500x slower than canvas ops)

Solutions

Reduce draw calls:

// BAD: 10000 individual circles
for (let p of particles) {
  ellipse(p.x, p.y, p.size);
}

// GOOD: single shape with vertices
beginShape(POINTS);
for (let p of particles) {
  vertex(p.x, p.y);
}
endShape();

// BEST: direct pixel manipulation
loadPixels();
for (let p of particles) {
  let idx = 4 * (floor(p.y) * width + floor(p.x));
  pixels[idx] = p.r;
  pixels[idx+1] = p.g;
  pixels[idx+2] = p.b;
  pixels[idx+3] = 255;
}
updatePixels();

Spatial hashing for neighbor queries:

class SpatialHash {
  constructor(cellSize) {
    this.cellSize = cellSize;
    this.cells = new Map();
  }

  clear() { this.cells.clear(); }

  _key(x, y) {
    return `${floor(x / this.cellSize)},${floor(y / this.cellSize)}`;
  }

  insert(obj) {
    let key = this._key(obj.pos.x, obj.pos.y);
    if (!this.cells.has(key)) this.cells.set(key, []);
    this.cells.get(key).push(obj);
  }

  query(x, y, radius) {
    let results = [];
    let minCX = floor((x - radius) / this.cellSize);
    let maxCX = floor((x + radius) / this.cellSize);
    let minCY = floor((y - radius) / this.cellSize);
    let maxCY = floor((y + radius) / this.cellSize);

    for (let cx = minCX; cx <= maxCX; cx++) {
      for (let cy = minCY; cy <= maxCY; cy++) {
        let key = `${cx},${cy}`;
        let cell = this.cells.get(key);
        if (cell) {
          for (let obj of cell) {
            if (dist(x, y, obj.pos.x, obj.pos.y) <= radius) {
              results.push(obj);
            }
          }
        }
      }
    }
    return results;
  }
}

Object pooling:

class ParticlePool {
  constructor(maxSize) {
    this.pool = [];
    this.active = [];
    for (let i = 0; i < maxSize; i++) {
      this.pool.push(new Particle(0, 0));
    }
  }

  spawn(x, y) {
    let p = this.pool.pop();
    if (p) {
      p.reset(x, y);
      this.active.push(p);
    }
  }

  update() {
    for (let i = this.active.length - 1; i >= 0; i--) {
      this.active[i].update();
      if (this.active[i].isDead()) {
        this.pool.push(this.active.splice(i, 1)[0]);
      }
    }
  }
}

Throttle heavy operations:

// Only update flow field every N frames
if (frameCount % 5 === 0) {
  flowField.update(frameCount * 0.001);
}

Frame Rate Targets

Context Target Acceptable
Interactive sketch 60fps 30fps
Ambient animation 30fps 20fps
Export/recording 30fps render Any (offline)
Mobile 30fps 20fps

Per-Pixel Rendering Budgets

Pixel-level operations (loadPixels() loops) are the most expensive common pattern. Budget depends on canvas size and computation per pixel.

Canvas Pixels Simple noise (1 call) fBM (4 octave) Domain warp (3-layer fBM)
540x540 291K ~5ms ~20ms ~80ms
1080x1080 1.17M ~20ms ~80ms ~300ms+
1920x1080 2.07M ~35ms ~140ms ~500ms+
3840x2160 8.3M ~140ms ~560ms WILL CRASH

Rules of thumb:

  • 1 noise() call per pixel at 1080x1080 = ~20ms/frame (OK at 30fps)
  • 4-octave fBM per pixel at 1080x1080 = ~80ms/frame (borderline)
  • Multi-layer domain warp at 1080x1080 = 300ms+ (too slow for real-time, fine for noLoop() export)
  • Headless Chrome is 2-5x slower than desktop Chrome for pixel ops

Solution: render at lower resolution, fill blocks:

let step = 3;  // render 1/9 of pixels, fill 3x3 blocks
loadPixels();
for (let y = 0; y < H; y += step) {
  for (let x = 0; x < W; x += step) {
    let v = expensiveNoise(x, y);
    for (let dy = 0; dy < step && y+dy < H; dy++)
      for (let dx = 0; dx < step && x+dx < W; dx++) {
        let i = 4 * ((y+dy) * W + (x+dx));
        pixels[i] = v; pixels[i+1] = v; pixels[i+2] = v; pixels[i+3] = 255;
      }
  }
}
updatePixels();

Step=2 gives 4x speedup. Step=3 gives 9x. Visible at 1080p but acceptable for video (motion hides it).

Common Mistakes

1. Forgetting to reset blend mode

blendMode(ADD);
image(glowLayer, 0, 0);
// WRONG: everything after this is ADD blended
blendMode(BLEND);  // ALWAYS reset

2. Creating objects in draw()

// BAD: creates new font object every frame
function draw() {
  let f = loadFont('font.otf');  // NEVER load in draw()
}

// GOOD: load in preload, use in draw
let f;
function preload() { f = loadFont('font.otf'); }

3. Not using push()/pop() with transforms

// BAD: transforms accumulate
translate(100, 0);
rotate(0.1);
ellipse(0, 0, 50);
// Everything after this is also translated and rotated

// GOOD: isolated transforms
push();
translate(100, 0);
rotate(0.1);
ellipse(0, 0, 50);
pop();

4. Integer coordinates for crisp lines

// BLURRY: sub-pixel rendering
line(10.5, 20.3, 100.7, 80.2);

// CRISP: integer + 0.5 for 1px lines
line(10.5, 20.5, 100.5, 80.5);  // on pixel boundary

5. Pixel density confusion

// WRONG: assuming pixel array matches canvas dimensions
loadPixels();
let idx = 4 * (y * width + x);  // wrong if pixelDensity > 1

// RIGHT: account for pixel density
let d = pixelDensity();
loadPixels();
let idx = 4 * ((y * d) * (width * d) + (x * d));

// SIMPLEST: set pixelDensity(1) at the start

6. Color mode confusion

// In HSB mode, fill(255) is NOT white
colorMode(HSB, 360, 100, 100);
fill(255);  // This is hue=255, sat=100, bri=100 = vivid purple

// White in HSB:
fill(0, 0, 100);  // any hue, 0 saturation, 100 brightness

// Black in HSB:
fill(0, 0, 0);

7. WebGL origin is center

// In WEBGL mode, (0,0) is CENTER, not top-left
function draw() {
  // This draws at the center, not the corner
  rect(0, 0, 100, 100);

  // For top-left behavior:
  translate(-width/2, -height/2);
  rect(0, 0, 100, 100);  // now at top-left
}

8. createGraphics cleanup

// BAD: memory leak — buffer never freed
function draw() {
  let temp = createGraphics(width, height);  // new buffer every frame!
  // ...
}

// GOOD: create once, reuse
let temp;
function setup() {
  temp = createGraphics(width, height);
}
function draw() {
  temp.clear();
  // ... reuse temp
}

// If you must create/destroy:
temp.remove();  // explicitly free

9. noise() returns 0-1, not -1 to 1

let n = noise(x);  // 0.0 to 1.0 (biased toward 0.5)

// For -1 to 1 range:
let n = noise(x) * 2 - 1;

// For a specific range:
let n = map(noise(x), 0, 1, -100, 100);

10. saveCanvas() in draw() saves every frame

// BAD: saves a PNG every single frame
function draw() {
  // ... render ...
  saveCanvas('output', 'png');  // DON'T DO THIS
}

// GOOD: save once via keyboard
function keyPressed() {
  if (key === 's') saveCanvas('output', 'png');
}

// GOOD: save once after rendering static piece
function draw() {
  // ... render ...
  saveCanvas('output', 'png');
  noLoop();  // stop after saving
}

11. console.log() in draw()

// BAD: writes to DOM console every frame — massive overhead
function draw() {
  console.log(particles.length);  // 60 DOM writes/second
}

// GOOD: log periodically or conditionally
function draw() {
  if (frameCount % 60 === 0) console.log('FPS:', frameRate().toFixed(1));
}

12. DOM manipulation in draw()

// BAD: layout thrashing — 400-500x slower than canvas ops
function draw() {
  document.getElementById('counter').innerText = frameCount;
  let el = document.querySelector('.info');  // DOM query per frame
}

// GOOD: cache DOM refs, update infrequently
let counterEl;
function setup() { counterEl = document.getElementById('counter'); }
function draw() {
  if (frameCount % 30 === 0) counterEl.innerText = frameCount;
}

13. Not disabling FES in production

// BAD: every p5 function call has error-checking overhead (up to 10x slower)
function setup() { createCanvas(800, 800); }

// GOOD: disable before any p5 code
p5.disableFriendlyErrors = true;
function setup() { createCanvas(800, 800); }

// ALSO GOOD: use p5.min.js (FES stripped from minified build)

Browser Compatibility

Safari Issues

  • WebGL shader precision: always declare precision mediump float;
  • AudioContext requires user gesture (userStartAudio())
  • Some blendMode() options behave differently

Firefox Issues

  • textToPoints() may return slightly different point counts
  • WebGL extensions may differ from Chrome
  • Color profile handling can shift colors

Mobile Issues

  • Touch events need return false to prevent scroll
  • devicePixelRatio can be 2x or 3x — use pixelDensity(1) for performance
  • Smaller canvas recommended (720p or less)
  • Audio requires explicit user gesture to start

CORS Issues

// Loading images/fonts from external URLs requires CORS headers
// Local files need a server:
// python3 -m http.server 8080

// Or use a CORS proxy for external resources (not recommended for production)

Memory Leaks

Symptoms

  • Framerate degrading over time
  • Browser tab memory growing unbounded
  • Page becomes unresponsive after minutes

Common Causes

// 1. Growing arrays
let history = [];
function draw() {
  history.push(someData);  // grows forever
}
// FIX: cap the array
if (history.length > 1000) history.shift();

// 2. Creating p5 objects in draw()
function draw() {
  let v = createVector(0, 0);  // allocation every frame
}
// FIX: reuse pre-allocated objects

// 3. Unreleased graphics buffers
let layers = [];
function reset() {
  for (let l of layers) l.remove();  // free old buffers
  layers = [];
}

// 4. Event listener accumulation
function setup() {
  // BAD: adds new listener every time setup runs
  window.addEventListener('resize', handler);
}
// FIX: use p5's built-in windowResized()

Debugging Tips

Console Logging

// Log once (not every frame)
if (frameCount === 1) {
  console.log('Canvas:', width, 'x', height);
  console.log('Pixel density:', pixelDensity());
  console.log('Renderer:', drawingContext.constructor.name);
}

// Log periodically
if (frameCount % 60 === 0) {
  console.log('FPS:', frameRate().toFixed(1));
  console.log('Particles:', particles.length);
}

Visual Debugging

// Show frame rate
function draw() {
  // ... your sketch ...
  if (CONFIG.debug) {
    fill(255, 0, 0);
    noStroke();
    textSize(14);
    textAlign(LEFT, TOP);
    text('FPS: ' + frameRate().toFixed(1), 10, 10);
    text('Particles: ' + particles.length, 10, 28);
    text('Frame: ' + frameCount, 10, 46);
  }
}

// Toggle debug with 'd' key
function keyPressed() {
  if (key === 'd') CONFIG.debug = !CONFIG.debug;
}

Isolating Issues

// Comment out layers to find the slow one
function draw() {
  renderBackground();      // comment out to test
  // renderParticles();    // this might be slow
  // renderPostEffects();  // or this
}