A small, standalone, framework-agnostic frame scheduler with phases, priorities, and per-job FPS throttling. One RAF loop, any renderer, no framework required.
- One RAF loop for your whole app, across multiple roots
- Phases (
start → input → physics → update → render → finish) you can extend at runtime - Priorities and cross-job
before/afterordering (topological sort) - FPS throttling per job, with drop or catch-up semantics
- Demand (
invalidate) and manual (step) frame modes - Zero dependencies. Vanilla core pulls in no React.
This is its own package, not a build artifact of another library. It began as the core
frame scheduler inside react-three-fiber
(the engine behind its useFrame) and is being ported into an independent, framework-agnostic
library with its own API, docs, tests, and release cycle. The r3f lineage is where it came
from — not something it depends on; the vanilla core ships zero dependencies and no React.
Pre-1.0: the port is ongoing and the API is still settling, but the package is standalone and usable today. Expect refinements before 1.0.
npm install @pmndrs/schedulerReact is an optional peer dependency, only needed for the /react entry.
import { getScheduler } from '@pmndrs/scheduler'
const scheduler = getScheduler()
scheduler.register(
(state, delta) => {
// called every frame: state = { time, delta, elapsed, frame }
},
{ phase: 'update' },
)No host renderer or setup flag is required — registering a job lazily creates the
scheduler's root and starts the loop. If a host (e.g. a <Canvas>) registers later, it
adopts jobs already registered.
import { useFrame } from '@pmndrs/scheduler/react'
function Spinner() {
useFrame((state, delta) => {
// runs every frame
})
return null
}useFrame returns a controls object ({ id, scheduler, step, stepAll, pause, resume, isPaused })
where isPaused is reactive.
| Import | Contents |
|---|---|
@pmndrs/scheduler |
Scheduler, getScheduler, all types |
@pmndrs/scheduler/react |
useFrame + re-exported scheduler + types |
Every register / useFrame call creates a job. Jobs run in named phases
(start → input → physics → update → render → finish) that you can extend at runtime, with
priorities and before/after dependencies inside each phase. It's a DAG scheduler:
you declare ordering, it figures out the sequence — no priority-number guessing.
scheduler.addPhase('ai', { after: 'physics', before: 'update' })
scheduler.register(processInput, { phase: 'input' })
scheduler.register(() => world.step(1 / 60), { phase: 'physics', fps: 60, drop: false })
scheduler.register(updateAI, { phase: 'ai', fps: 20 })
scheduler.register(render, { phase: 'render' })Three guides, roughly in reading order. New here? Start with Concepts — it explains the mental model (jobs, phases, the frame budget) that the API references build on.
- Concepts — start here. Jobs, phases, the frame budget, FPS throttling, frameloop modes, and the design rationale (why named phases beat priority numbers), ending in a full game-loop example.
- useFrame Hook — the React API: registration timing, options, controls, examples, and best practices.
- Scheduler API — the full vanilla surface: roots, phases, job registration & control, frameloop, demand mode, manual stepping, and testing.
Runnable demos live in examples/ — a vanilla app and a
React app. From either directory: pnpm install && pnpm dev.
MIT