# Motion Transform Shorthands Are Not GPU-Accelerated

**MUST** · **ID:** `performance-motion-shorthand-not-gpu` · **Category:** performance
**Source:** [Emil Kowalski](https://emilkowalski.com/)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/performance-motion-shorthand-not-gpu

> MUST: In Motion/Framer Motion, animate the full `transform` string, not the `x`/`y`/`scale` shorthands: the shorthands are interpolated per `requestAnimationFrame` on the MAIN THREAD and drop frames under load, while `transform` as a whole string (along with `opacity`, `filter`, `clipPath`) is handed to the Web Animations API and ticked by the compositor. This is the false floor under `animations-compositor-friendly` — `animate={{ x: 100 }}` looks compliant and is not.

Animate the full transform string in Motion, because x, y, and scale are ticked on the main thread

This is the false floor under every "animate transform and opacity" rule you have already read. Writing animate={{ x: 100 }} looks like you obeyed them — it does compile down to a transform — but the value is interpolated by the library on each requestAnimationFrame tick and written to style, so it lives and dies on the main thread and drops frames the moment anything else is busy. Motion only hands an animation to the Web Animations API when the animated property is on its accelerated list — opacity, filter, clipPath, and transform as a whole string — and the x / y / scale shorthands are not on it. Animate transform: "translateX(100px)" instead and the compositor ticks it, so it keeps moving even while the main thread is blocked. Distinct from setting transform on the element rather than a parent CSS variable: that rule is about a style-recalc storm across descendants, this one is about which thread owns the tween.

## Rule snippet

```tsx
// main-thread rAF:  animate={{ x: 100 }}
// compositor/WAAPI: animate={{ transform: "translateX(100px)" }}
```

## Bad — do not do this

`performance-motion-shorthand-not-gpu-bad`

```tsx
import { useRef, useState } from 'react';
import { motion } from 'motion/react';

const DISTANCE = 200;
const BLOCK_MS = 600;

const blockMainThread = (ms: number) => {
  const end = performance.now() + ms;
  while (performance.now() < end) {
    // spin: a long task, exactly like a big JSON parse or a layout-heavy render
  }
};

export function MotionShorthandNotGpuBad() {
  const boxRef = useRef<HTMLDivElement>(null);
  const [moved, setMoved] = useState<number | null>(null);

  const stress = () => {
    const box = boxRef.current;
    if (!box) return;
    const before = box.getBoundingClientRect().left;
    blockMainThread(BLOCK_MS);
    const after = box.getBoundingClientRect().left;
    let delta = after - before;
    if (delta < -1) delta += DISTANCE; // the loop wrapped mid-block
    setMoved(Math.round(delta));
  };

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="space-y-3 rounded-lg border border-border bg-card p-4">
        <div className="text-xs text-muted-foreground">
          <code>animate={'{{'} x: {DISTANCE} {'}}'}</code>
        </div>
        <div className="h-10 overflow-hidden rounded-md bg-muted">
          <motion.div
            ref={boxRef}
            initial={{ x: 0 }}
            animate={{ x: DISTANCE }}
            transition={{ duration: 1.6, ease: 'linear', repeat: Infinity }}
            className="size-10 rounded-md bg-primary"
          />
        </div>

        <button
          type="button"
          onClick={stress}
          className="w-full rounded-md border border-border bg-muted px-3 py-2 text-sm text-foreground"
        >
          Block the main thread for {BLOCK_MS}ms
        </button>

        <div className="text-xs tabular-nums text-error">
          moved during the block: {moved === null ? '—' : `${moved} px`}
        </div>
      </div>
      <p className="text-xs text-error">
        The <code>x</code> shorthand is a Motion value written on every{' '}
        <code>requestAnimationFrame</code> tick — main-thread work wearing a{' '}
        <code>transform</code> costume. Block the thread and the box measurably stops: the distance
        it covered during the block is around zero, and it resumes from where it froze.
      </p>
    </div>
  );
}
```

## Good — do this

`performance-motion-shorthand-not-gpu-good`

```tsx
import { useRef, useState } from 'react';
import { motion } from 'motion/react';

const DISTANCE = 200;
const BLOCK_MS = 600;

const blockMainThread = (ms: number) => {
  const end = performance.now() + ms;
  while (performance.now() < end) {
    // spin: identical long task to the bad example, so the comparison is fair
  }
};

export function MotionShorthandNotGpuGood() {
  const boxRef = useRef<HTMLDivElement>(null);
  const [moved, setMoved] = useState<number | null>(null);

  const stress = () => {
    const box = boxRef.current;
    if (!box) return;
    const before = box.getBoundingClientRect().left;
    blockMainThread(BLOCK_MS);
    const after = box.getBoundingClientRect().left;
    let delta = after - before;
    if (delta < -1) delta += DISTANCE; // the loop wrapped mid-block
    setMoved(Math.round(delta));
  };

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="space-y-3 rounded-lg border border-border bg-card p-4">
        <div className="text-xs text-muted-foreground">
          <code>
            animate={'{{'} transform: &apos;translateX({DISTANCE}px)&apos; {'}}'}
          </code>
        </div>
        <div className="h-10 overflow-hidden rounded-md bg-muted">
          <motion.div
            ref={boxRef}
            initial={{ transform: 'translateX(0px)' }}
            animate={{ transform: `translateX(${DISTANCE}px)` }}
            transition={{ duration: 1.6, ease: 'linear', repeat: Infinity }}
            className="size-10 rounded-md bg-primary"
          />
        </div>

        <button
          type="button"
          onClick={stress}
          className="w-full rounded-md border border-border bg-muted px-3 py-2 text-sm text-foreground"
        >
          Block the main thread for {BLOCK_MS}ms
        </button>

        <div className="text-xs tabular-nums text-success">
          moved during the block: {moved === null ? '—' : `${moved} px`}
        </div>
      </div>
      <p className="text-xs text-success">
        A full <code>transform</code> string is on Motion&apos;s accelerated list, so it is handed to
        the Web Animations API and ticked by the compositor. Block the main thread for the same{' '}
        {BLOCK_MS}ms and the box keeps travelling — the measured distance is a real number, not zero.
        Same library, same duration, one of them survives a busy thread.
      </p>
    </div>
  );
}
```

## References

- [Emil Kowalski: review-animations STANDARDS.md](https://github.com/emilkowalski/skills/blob/main/skills/review-animations/STANDARDS.md)
- [MDN: CSS and JavaScript animation performance](https://developer.mozilla.org/en-US/docs/Web/Performance/Guides/CSS_JavaScript_animation_performance)
- [web.dev: Animations guide](https://web.dev/articles/animations-guide)
