Using p5.js createLoop() for Precise Animation Timing
Learn how p5.js createLoop() provides a fixed‑interval update step separate from the render loop, enabling deterministic timing for animations, audio, or simulations.
09 Feb 2026, 18:59 UTC

Problem: Need for deterministic timing
When a sketch must stay in sync with audio, external sensors, or a fixed‑step simulation, relying solely on the default draw() loop can be problematic. The browser’s requestAnimationFrame callback fires at the display refresh rate (typically 60 Hz) and can vary if the tab is backgrounded or the GPU is busy, causing jitter in time‑based animations.
Thesis: createLoop() gives you a custom, independent update interval
Starting with p5.js 1.4.0, the createLoop() function lets you register a callback that runs at a fixed interval you specify, completely separate from the rendering loop. This provides deterministic timing for logic while still allowing you to use p5.js’s drawing functions in draw() for visual output.
How createLoop() works
- Call
createLoop(intervalMs, callback)whereintervalMsis the desired period (e.g.,33for ~30 Hz). - The callback receives the elapsed time since the loop started, useful for physics or phase calculations.
- The loop runs on its own timer (based on
setTimeout/setIntervalpolyfill) and does not blockdraw(). - You can stop it later with
loop.stop().
Worked example: 30 Hz moving circle, 60 Hz background
The following sketch demonstrates a circle that advances at a steady 30 Hz regardless of frame‑rate fluctuations, while the background color follows the mouse at the usual 60 Hz draw() rate.
// Ensure you are using p5.js >=1.4.0
let circleX = 0;
let loop;
function setup() {
createCanvas(400, 300);
// Create a loop that ticks every 33 ms (~30 Hz)
loop = createLoop(33, (elapsed) => {
// Update position based on elapsed time (in ms)
circleX = (elapsed * 0.1) % width; // 0.1 px/ms = 100 px/s
});
loop.start(); // start the custom loop
}
function draw() {
// Standard draw runs at requestAnimationFrame rate (~60 Hz)
background(map(mouseX, 0, width, 50, 200));
fill(255, 100, 100);
noStroke();
ellipse(circleX, height / 2, 40, 40);
}
function windowResized() {
resizeCanvas(windowWidth, windowHeight);
}
To try this:
- Open the p5.js web editor.
- Paste the code into the sketch window.
- Click the play button; you should see a pink circle moving smoothly across the canvas.
- Open DevTools → Performance tab, record a few seconds, and look for the “createLoop” callback (visible in the flame‑graph if you enable “Show custom events”). Its ticks should appear roughly every 33 ms.
Trade‑off and limitation
Because createLoop() is not tied to requestAnimationFrame, it will keep ticking even if the browser is throttling animation frames. Heavy work inside the custom loop can therefore consume CPU continuously, potentially raising temperature or draining battery on mobile devices. Moreover, if you read or write shared variables (like circleX) from both loops without synchronization, you risk race conditions. In this example the circle’s position is only written in the custom loop and read in draw(), which is safe because JavaScript is single‑threaded and the reads happen after the write in the same event loop tick, but more complex state should be guarded with atomic checks or a simple double‑buffer pattern.
Actionable closing
Use createLoop() for any logic that needs a fixed step—physics, audio sequencing, or polling external data—while keeping rendering in the standard draw(). Choose an interval that matches your desired frequency (e.g., 16 ms for 60 Hz, 33 ms for ~30 Hz). After integrating, profile with the browser’s Performance panel to verify that the loop’s callbacks stay within the target interval and that frame drops stay low. If you notice sustained high CPU usage, consider throttling the loop further or moving the heaviest calculations to a Web Worker.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.