Animate on the compositor

Animate transform and opacity on rAF; stretch very fast objects.

From apple-design, §11, after Designing Fluid Interfaces ↗.

left vs transform under load

Block the main thread and watch which box stutters.

Longest frame –
Animating leftBad
left: 0 → 100%
Animating transformGood
transform: translateX()
.box { will-change: transform; }

box.animate(
  [{ transform: "translateX(0)" }, { transform: `translateX(${travel}px)` }],
  { duration: 1600, iterations: Infinity, direction: "alternate",
    easing: "cubic-bezier(0.77, 0, 0.175, 1)" }
);

setInterval vs rAF

Each tick is one display frame. Compare the spacing.

Display –
Timer-drivenBad
setInterval(step, 16)–
Display-syncedGood
requestAnimationFrame(step)–
let last = performance.now();
function step(now) {
  const dt = (now - last) / 1000;
  last = now;
  x = advance(x, dt);
  el.style.transform = `translateX(${x}px)`;
  requestAnimationFrame(step);
}
requestAnimationFrame(step);

Stretch fast motion

Raise the speed until the left dot splits into ghosts.

Jump per frame –
Sharp dotBad
rotate(θ) translateX(r)
Stretched by velocityGood
… scale(1/√s, s)
const step = speed * dt;
const s = 1 + Math.min(2, Math.max(0, step / size - 0.3) * amount);
dot.style.transform =
  `rotate(${angle}rad) translateX(${r}px) scale(${1 / Math.sqrt(s)}, ${s})`;

Why it matters, pros and cons

Why it matters

transform and opacity skip layout, and rAF gives each frame one new position.

Pros

  • Survives main-thread jank.
  • rAF adapts to 60, 90 and 120 Hz.
  • Stretched flings read as one object.

Cons

  • rAF-driven transforms still stall on a blocked thread.
  • will-change costs GPU memory per layer.
  • Some effects genuinely need layout, like reflowing text.