Managing Frame Rate in PixiJS with the Built‑in Ticker
Learn how to use PixiJS's built‑in ticker to manage frame rate efficiently, with a concrete throttling example, trade‑offs, and verification steps.
28 May 2026, 06:13 UTC

The problem: uncontrolled animation loops
When you start adding moving sprites, particle systems, or physics updates to a PixiJS scene, each requestAnimationFrame callback you write adds overhead. Multiple independent loops fight for the same frame budget, causing jank on low‑end devices and making it hard to reason about the actual frame rate your game runs at.
Thesis: lean on PixiJS’s ticker instead of rolling your own loop
PixiJS ships a single, high‑resolution ticker that drives the renderer’s update cycle. By scheduling work through ticker.add() you get delta‑time values automatically, avoid duplicate requestAnimationFrame registrations, and gain a built‑in knob for throttling the frame rate.
How the ticker works
- Delta‑time driven – each callback receives the elapsed milliseconds since the last tick, so motion stays consistent regardless of frame‑rate fluctuations.
- Unified loop – the ticker runs inside the renderer’s main update pass, so there is only one
requestAnimationFrameper frame. - Control API –
ticker.add(fn),ticker.remove(fn),ticker.start(),ticker.stop(), and properties likemaxFPS,minFPS, andspeedlet you shape timing without extra code. - Manual drive – calling
ticker.update(deltaMs)yourself enables pause, slow‑motion, or custom step sizes (useful for replays or deterministic simulations).
Worked example: throttling a scene to 30 FPS
The following minimal page shows a rotating sprite and logs the measured FPS. It runs in any static‑file server (e.g., npx serve or python -m http.server). No build step required.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>PixiJS Ticker Demo</title>
<script src="https://cdn.jsdelivr.net/npm/pixi.js@8.x/dist/pixi.min.js"></script>
</head>
<body>
<canvas id="app"></canvas>
<script>
const app = new PIXI.Application({
view: document.getElementById('app'),
width: 800,
height: 600,
backgroundColor: 0x1099bb,
// Disable auto‑start so we can configure the ticker first
autoStart: false
});
const sprite = new PIXI.Graphics();
sprite.beginFill(0xff0000);
sprite.drawRect(-50, -50, 100, 100);
sprite.endFill();
sprite.position.set(400, 300);
app.stage.addChild(sprite);
// Throttle to 30 FPS
app.ticker.maxFPS = 30;
app.ticker.minFPS = 30; // optional: clamp lower bound
let last = performance.now();
let frames = 0;
app.ticker.add((delta) => {
// delta is a multiplier (1 at 60 FPS). Convert to ms:
const deltaMs = delta * (1000 / 60);
sprite.rotation += 0.01 * delta;
frames++;
const now = performance.now();
if (now - last >= 1000) {
console.log(`Measured FPS: ${frames}`);
frames = 0;
last = now;
}
});
app.ticker.start();
</script>
</body>
</html>
Where to run: save as index.html and serve the folder with a static server. Permissions: none beyond reading the file. Expected check: open the browser console; you should see "Measured FPS: 30" (or close to it) once per second. Risk: if the browser throttles background tabs, the logged FPS may drop; test in a foreground tab.
Trade‑offs and limitations
- Version drift – PixiJS 7 vs. 8 changed the ticker’s internal class (e.g.,
Tickervs.TickerPlugin). Code that accesses private properties like_lastTimewill break on upgrade. - Performance ceiling – each callback added to the ticker runs every frame. Hundreds of heavy callbacks can saturate the main thread, especially on mobile GPUs. Profile with the browser’s Performance panel before scaling.
- Debug visibility – the ticker’s internal frame‑time buffer is not exposed. Timing bugs often require instrumenting your own callbacks (as in the example) rather than inspecting engine state.
Verifying the setup
- Open the demo and confirm the console logs ~30 FPS.
- Change
app.ticker.maxFPS = 60;and reload; the log should approach 60 FPS. - Inspect the current PixiJS source (e.g.,
node_modules/pixi.js/lib/ticker/Ticker.jsor the GitHub repo) to verify method signatures match the version you ship.
Actionable closing
Replace any hand‑rolled requestAnimationFrame loops with app.ticker.add() and use maxFPS/minFPS to set a performance budget. Run the minimal demo on your target devices, watch the logged FPS, and only add more callbacks after the baseline stays stable. If you need deterministic stepping (e.g., for replay or networking), drive the ticker manually with ticker.update(desiredDeltaMs) instead of relying on the automatic loop.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.