Navigate
Search topics across all sections
Animation Loop
A 3D scene is not a static picture. It redraws itself many times per second, and each redraw is your chance to move things, spin things, or react to user input. This is the animation loop — the heartbeat of every 3D experience.
You test your spinning cube on your laptop and it looks perfect — a smooth, gentle rotation. Your friend opens it on their gaming monitor and the cube is spinning twice as fast. Another friend on an old tablet sees it moving in slow motion. Same code, three different speeds.
Animation speed varies across devices. Cube rotates 2.4x faster on 144Hz vs 60Hz.
Real-world
Remember those flipbooks you made as a kid? You draw a slightly different picture on each page, then flip through them fast and it looks like smooth animation.
Your computer does the same thing — it draws about 60 "pages" per second. Each page is called a frame.
On each frame, you get a chance to change things: rotate an object a tiny bit, move a character slightly forward, update a color. Flip through all these tiny changes fast enough, and you get smooth motion.
But here's the catch: some flipbooks have 60 pages per second, others have 144. If you move an object "1 step per page", faster flipbooks mean faster movement. That's why we need delta time.
Frame Starts
Browser says: time to draw!
Update
Move objects, apply physics
Render
Draw the new picture
Repeat
~60 times per second
Hands-On: Making Things Move
In R3F, the useFrame hook is your animation loop. It runs once per frame and gives you everything you need to create smooth animations.
Step 1: The simplest animation
function SpinningBox() {
const ref = useRef<Mesh>(null!);
useFrame(() => {
ref.current.rotation.y += 0.01;
});
return (
<mesh ref={ref}>
<boxGeometry />
<meshStandardMaterial color="tomato" />
</mesh>
);
}useFrame runs your callback every single frame. Here, we add 0.01 radians to the Y rotation each frame, making the cube spin. But there's a problem — this spins faster on a 144Hz monitor than on a 60Hz one.
Step 2: Fix it with delta time
useFrame((state, delta) => {
// 1 radian per second, on ANY device
ref.current.rotation.y += 1.0 * delta;
});The second argument, delta, is the time in seconds since the last frame. On a 60Hz monitor, delta is ~0.016s. On 144Hz, it's ~0.007s. Multiply your speed by delta, and the rotation is always 1 radian per second regardless of frame rate. Problem solved!
Step 3: Use the clock for oscillation
useFrame((state) => {
const t = state.clock.elapsedTime;
ref.current.position.y = Math.sin(t) * 0.5;
// Bobs up and down smoothly forever
});state.clock.elapsedTime gives you the total seconds since the scene started. Feed it into Math.sin() and you get a smooth wave that oscillates between -1 and 1. Multiply by 0.5 to bob half a unit up and down. Try changing 0.5 to 2 for a bigger bounce!
What you just learned
The animation loop (useFrame) runs once per frame — about 60 times per second — and is where you update positions, rotations, and other properties.
Always multiply speeds by 'delta' (time since last frame) to make animations look the same on 60Hz and 144Hz monitors.
state.clock.elapsedTime gives the total time since the scene started — perfect for smooth oscillations with Math.sin().
Never create new objects (new Vector3, new Color) inside useFrame — pre-allocate them outside to avoid memory issues.
Question
If the animation loop runs 60 times per second, that means you have about 16 milliseconds to do all your work per frame. What happens if your update logic takes 20 milliseconds? What would the user see?
Think about it...
You have a cube rotating with 'ref.current.rotation.y += 0.01' in useFrame. On a 60Hz monitor it completes one full rotation in ~10.5 seconds. How long does the same rotation take on a 144Hz monitor?
Hint: Think about how many times useFrame fires per second on each monitor...
Try These Challenges
Put what you learned into practice. Try each challenge in the demo above using the Leva controls, then check the solution.
Try This!
Beginner
Set speed to 5 — which cube goes crazy?
Try This!
Intermediate
Try Math.sin(clock.elapsedTime) for oscillation
Try This!
Beginner
Use delta * 0.1 for slow-motion
These are the patterns that trip up developers most often. Switch between Wrong and Fixed to compare the code side by side.
useFrame(() => {
// WRONG: new Vector3 every frame = memory leak!
const dir = new Vector3(1, 0, 0);
ref.current.position.add(
dir.multiplyScalar(0.01)
);
});useFrame(() => {
// WRONG: on a 144Hz monitor, this runs
// 2.4x faster than on a 60Hz monitor!
ref.current.rotation.y += 0.01;
});useFrame(() => {
// WRONG: sorting 10,000 items every frame
const sorted = particles.sort((a, b) =>
a.distanceTo(camera) - b.distanceTo(camera)
);
updatePositions(sorted);
});Best Practices
Always Use Delta Time
Multiply all movement and rotation speeds by delta. This ensures your animation runs at the same real-world speed on 60Hz, 144Hz, and variable-refresh displays.
Pre-Allocate Reusable Objects
Create Vector3, Color, and other objects outside useFrame using useRef or useMemo. Reuse them with .set() and .copy() to avoid garbage collection pauses.
Keep useFrame Lightweight
You have about 16ms per frame at 60fps. Heavy computations like sorting large arrays or complex raycasting should be spread across frames or offloaded to workers.
Render On Demand When Possible
For static scenes that only change on user interaction (like model viewers), use R3F's frameloop="demand" prop. This can reduce GPU usage from 100% to nearly 0% when idle, saving battery life.