@zakkster/lite-ticker
v1.0.3
Published
Zero-dependency RAF scheduler with game-pausable timers, fixed-timestep physics, profiler hooks, and tab-switch guard.
Maintainers
Readme
@zakkster/lite-ticker
A zero-dependency RAF scheduler with game-pausable timers and optional fixed-timestep physics.
One requestAnimationFrame loop to rule them all — per-frame tasks, timeouts, and intervals that pause when the game pauses and don't explode after a tab switch.
Features
- Variable or fixed-timestep loops — choose per game
- Game-pausable setTimeout / setInterval — tied to RAF, not wall clock
- Tab-switch guard — caps dt to prevent physics explosions after background
- Profiler hooks —
onFrameStart/onFrameEndfor performance instrumentation - Disposer pattern — every add/setTimeout/setInterval returns a cleanup function
- Snapshot-safe timer iteration — callbacks can safely add/remove timers mid-frame
- Zero dependencies, < 1 KB
Installation
npm install @zakkster/lite-tickerQuick Start
import { Ticker } from '@zakkster/lite-ticker';
const ticker = new Ticker();
// Add a per-frame task (receives dt in ms)
const removeUpdate = ticker.add((dt) => {
player.x += player.vx * (dt / 1000);
});
// Game-pausable timeout (freezes when ticker is paused)
const cancelCountdown = ticker.setTimeout(() => {
console.log('3 seconds of GAME TIME have passed');
}, 3000);
ticker.start();
// Later: pause (all tasks and timers freeze)
ticker.pause();
// Resume (no delta spike — timestamp resets)
ticker.start();
// Clean up
removeUpdate();
cancelCountdown();
ticker.destroy();Options
const ticker = new Ticker({
fixedStep: 16.66, // Fixed timestep in ms (0 = variable dt, default)
maxDt: 100, // Tab-switch dt cap in ms (default: 100)
});API
Lifecycle
| Method | Description |
|--------|-------------|
| .start() | Start the loop. Resets timestamp. Idempotent. |
| .pause() | Pause. Tasks and timers freeze. |
| .destroy() | Stop and clear everything. Idempotent. |
Per-Frame Tasks
const remove = ticker.add((dt) => {
// dt is in ms — either variable or fixedStep
physics.update(dt);
renderer.draw();
});
remove(); // stop receiving framesTimers
// One-shot (like setTimeout but game-pausable)
const cancel = ticker.setTimeout(() => {
showModal('Game Over');
}, 5000);
// Repeating (like setInterval but game-pausable)
const stop = ticker.setInterval(() => {
spawnEnemy();
}, 2000);
cancel(); // cancel before it fires
stop(); // stop the intervalFixed Timestep
For deterministic physics, use fixedStep:
const ticker = new Ticker({ fixedStep: 16.66 }); // 60 updates/sec
ticker.add((dt) => {
// dt is always 16.66, regardless of frame rate.
// If a frame takes 33ms, this runs twice.
// If a frame takes 8ms, this doesn't run (accumulator < step).
body.velocity.y += GRAVITY * dt;
body.position.y += body.velocity.y * dt;
});The accumulator pattern prevents the "spiral of death" — if the game can't keep up, the dt cap kicks in and frames are skipped rather than endlessly catching up.
Tab-Switch Guard
When a user switches tabs and comes back 5 seconds later, the raw dt would be 5000ms. Without a guard, physics objects teleport across the screen and timers fire all at once.
lite-ticker caps dt at maxDt (default: 100ms). After the cap, dt falls back to fixedStep (or 16.66ms if variable), producing one normal-looking frame instead of a physics explosion.
Profiler Hooks
Two optional hooks let you instrument frame timing without modifying your tasks:
onFrameStart— fires at the absolute start of every active frame, before dt is calculated and before any tasks or timers run.onFrameEnd— fires at the absolute end of every active frame, after all tasks and timer callbacks have run. This means it captures the full cost of the frame, including any work triggered bysetTimeout/setInterval.
Both default to null (zero overhead when unused). Hooks do not fire while the ticker is paused or destroyed.
const ticker = new Ticker();
// Use the browser Performance API to record frame work
ticker.onFrameStart = () => performance.mark('frame-start');
ticker.onFrameEnd = () => {
performance.mark('frame-end');
performance.measure('frame', 'frame-start', 'frame-end');
};
ticker.start();
// Or roll your own:
let frames = 0;
ticker.onFrameEnd = () => { frames++; };
// Disable later by reassigning null:
ticker.onFrameStart = null;
ticker.onFrameEnd = null;Note: Hooks are called synchronously inline with the frame. Keep them cheap — a thrown error will break the RAF chain, same as throwing inside a task.
Timer Safety
Timer callbacks can safely add or remove other timers during execution — the timer set is snapshotted before iteration. Intervals carry over their remainder for drift-free scheduling (elapsed %= delay).
TypeScript
import { Ticker, type TickerOptions, type Disposer, type FrameHook } from '@zakkster/lite-ticker';
const ticker = new Ticker({ fixedStep: 16.66 });
const remove: Disposer = ticker.add((dt: number) => {
// fully typed
});
const onStart: FrameHook = () => performance.mark('frame-start');
ticker.onFrameStart = onStart;License
MIT
