# Animation Frame Budget (60fps)

**SHOULD** · **ID:** `animations-frame-budget` · **Category:** animations
**Source:** Custom
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/animations-frame-budget

> SHOULD: Animation work SHOULD complete within 16ms frame budget (60fps). Use requestAnimationFrame for JS animations. Batch layout reads then writes — never interleave them — so a frame does not thrash between reflows and repaints.

Keep per-frame animation work under ~10ms — the textbook 16ms is not all yours

At 60fps a frame is 16.67ms, and that number is quoted everywhere as "the budget". It is not your budget. The browser needs part of every frame for its own housekeeping — style, layout, paint, compositing, plus whatever else the main thread is already queued to do — so the work you can actually spend is closer to 10ms, and less on a mid-range phone. Treat ~10ms as the target and 16ms as the cliff you are already falling off. Practically: do the work in requestAnimationFrame, batch every DOM read before every DOM write (interleaving them forces a synchronous layout per iteration), write only compositor-friendly properties (transform, opacity), and move anything genuinely expensive off the main thread entirely.

## Bad — do not do this

`animations-frame-budget-bad`

```tsx
import { useEffect, useRef, useState } from 'react';
import { useFrameRate } from '@/hooks/useFrameRate';
import { FpsMeter } from '@/components/demo-kit/FpsMeter';
import { blockMainThread } from '@/lib/demo';

export function FrameBudgetBad() {
  const [running, setRunning] = useState(false);
  const fps = useFrameRate(running);
  const boxRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!running) return;
    let raf = 0;
    let x = 0;
    let dir = 1;
    const loop = () => {
      x += dir * 3;
      if (x > 160 || x < 0) dir *= -1;
      // BAD: ~20ms of synchronous work every frame. The frame itself is 16.7ms, and the
      // browser needs part of that for style/layout/paint — so ~10ms is the real ceiling.
      blockMainThread(20);
      if (boxRef.current) boxRef.current.style.transform = `translateX(${x}px)`;
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, [running]);

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-3">
        <button
          onClick={() => setRunning((v) => !v)}
          className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm transition-colors duration-150 ease-out hover:bg-primary/90"
        >
          {running ? 'Stop' : 'Start'}
        </button>
        <FpsMeter fps={running ? fps : 60} />
      </div>
      <div className="relative h-12 rounded-lg bg-muted/50 overflow-hidden">
        <div ref={boxRef} className="absolute top-2 left-0 size-8 rounded-md bg-primary" />
      </div>
      <p className="text-xs text-destructive">
        ~20ms of JS work every frame — the counter drops well below 60fps and the box stutters. Note the cliff is not at
        16.7ms: the browser needs part of every frame for its own rendering, so anything past ~10ms of your work already costs frames
      </p>
    </div>
  );
}
```

## Good — do this

`animations-frame-budget-good`

```tsx
import { useEffect, useRef, useState } from 'react';
import { useFrameRate } from '@/hooks/useFrameRate';
import { FpsMeter } from '@/components/demo-kit/FpsMeter';

export function FrameBudgetGood() {
  const [running, setRunning] = useState(false);
  const fps = useFrameRate(running);
  const boxRef = useRef<HTMLDivElement>(null);

  useEffect(() => {
    if (!running) return;
    let raf = 0;
    let x = 0;
    let dir = 1;
    const loop = () => {
      x += dir * 3;
      if (x > 160 || x < 0) dir *= -1;
      // GOOD: one cheap compositor-friendly write per frame, well under budget.
      if (boxRef.current) boxRef.current.style.transform = `translateX(${x}px)`;
      raf = requestAnimationFrame(loop);
    };
    raf = requestAnimationFrame(loop);
    return () => cancelAnimationFrame(raf);
  }, [running]);

  return (
    <div className="space-y-4">
      <div className="flex items-center gap-3">
        <button
          onClick={() => setRunning((v) => !v)}
          className="px-4 py-2 rounded-lg bg-primary text-primary-foreground text-sm transition-colors duration-150 ease-out hover:bg-primary/90"
        >
          {running ? 'Stop' : 'Start'}
        </button>
        <FpsMeter fps={running ? fps : 60} />
      </div>
      <div className="relative h-12 rounded-lg bg-muted/50 overflow-hidden">
        <div ref={boxRef} className="absolute top-2 left-0 size-8 rounded-md bg-primary" />
      </div>
      <p className="text-xs text-success">
        Only a cheap <code>transform</code> write per frame — the counter holds at 60fps. A frame is 16.7ms, but the browser
        needs part of it for style, layout, paint and compositing: aim for ~10ms of your own work, not 16
      </p>
    </div>
  );
}
```

## References

- [web.dev — Rendering performance](https://web.dev/articles/rendering-performance)
- [MDN — Performance fundamentals](https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/Fundamentals)
