Using PixiJS Ticker for Stable Game Loops: When to Rely on Delta Time and When to Step Manually
Learn how PixiJS Ticker drives requestAnimationFrame, shares a global loop, and why mixing variable deltaTime with fixed timesteps can keep your game logic deterministic.
13 Aug 2026, 20:45 UTC

The Problem: Variable Delta Time Makes Physics Jitter
When you drive game logic directly from the deltaTime that PixiJS Ticker provides, the length of each frame can vary. At 60 fps the ideal delta is about 16.66 ms, but browser scheduling, garbage collection, or a heavy render step can push a frame to 30 ms or drop it to 8 ms. Feeding that uneven interval into a physics integrator makes position updates drift, causing visible jitter or tunneling.
How PixiJS Ticker Works
PixiJS exposes a singleton PIXI.Ticker.instance that wraps requestAnimationFrame. Every time the browser paints, the ticker invokes all registered listeners, passing a deltaTime argument measured in milliseconds since the last call. You can add logic with ticker.add(fn, context), remove it with ticker.remove(fn), and control the loop via start(), stop() and pause(). Because the ticker runs on the main thread, any expensive work inside a listener delays the next paint and reduces the observed frame rate.
Worked Example: Rendering with Ticker, Simulation with Fixed Timestep
In this pattern the ticker drives only the render step, while a separate loop advances the game world at a constant step size (e.g., 1/60 s). The example below shows how to set it up.
// Create the PixiJS application (or use an existing stage)
const app = new PIXI.Application({ width: 800, height: 600 });
document.body.appendChild(app.view);
// Fixed‑step parameters
const FIXED_TIME_STEP = 1 / 60; // seconds
let accumulator = 0;
// Game state placeholder
let playerX = 0;
// Render listener – runs every paint
app.ticker.add((delta) => {
// delta is in milliseconds; convert to seconds for interpolation
const interpolated = accumulator / FIXED_TIME_STEP;
// Use interpolated state for smooth rendering
playerSprite.x = playerX * interpolated;
});
// Simulation step – called manually as fast as needed
function updateGame(dt) {
// dt is the fixed step in seconds
playerX += 100 * dt; // example velocity 100 px/s
}
// Main loop – driven by the ticker but stepping simulation manually
let lastTime = performance.now();
function frame(now) {
const frameTime = (now - lastTime) / 1000; // seconds
lastTime = now;
accumulator += frameTime;
// Process as many fixed steps as we can catch up
while (accumulator >= FIXED_TIME_STEP) {
updateGame(FIXED_TIME_STEP);
accumulator -= FIXED_TIME_STEP;
}
// Request next frame via the ticker (ensures callback runs before paint)
app.ticker.update(); // advances ticker by one frame, triggering render listener
requestAnimationFrame(frame);
}
// Start the loop
requestAnimationFrame(frame);
What this does:
- The ticker’s
update()method is called once perrequestAnimationFrametick, which fires the render listener and gives you a chance to draw the interpolated state. - The simulation loop runs independently, consuming a fixed time step from the accumulator. Because the step size is constant, physics behaves the same regardless of how long a frame actually took.
- If the tab is backgrounded,
requestAnimationFramecallbacks are throttled, so the render listener runs less often, but the simulation still advances in fixed chunks when the loop resumes, preventing spiral‑of‑death.
Trade‑off and Limitation
Adding a manual step loop introduces extra code and requires you to manage the accumulator correctly. Forgetting to subtract the fixed step can cause the accumulator to grow without bound, leading to a spiral‑of‑death where the simulation tries to catch up by running many steps in a single frame. The practical way to check that you are not accumulating excessively is to log the accumulator value each frame; it should stay roughly below two times the fixed step.
Another limitation is that the ticker still runs on the main thread. If your render listener does heavy work (e.g., large texture uploads or complex shader calculations), the frame rate will drop and the simulation will receive fewer updates per second, which may feel like slow‑motion. Profiling with the browser’s performance panel or a lightweight FPS counter helps you spot costly render work before it affects gameplay.
Actionable Closing
- Keep the ticker listener focused on rendering only—read the interpolated game state and draw sprites.
- Place all deterministic logic (physics, AI, fixed‑step animation) in a function that you call with a constant
dt. - Use an accumulator to absorb variable frame times and run as many fixed steps as needed each frame.
- Verify behavior by opening the console and watching the accumulator log; it should remain small and stable.
- When you need deterministic replays or headless testing, you can bypass the ticker entirely and call
ticker.update(fixedDelta)manually.
By separating the rendering loop from the simulation step, you gain stable physics while still benefiting from PixiJS’s convenient ticker‑driven render loop.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.