DEV Community

Atlas Whoff
Atlas Whoff

Posted on

Framer Motion Animations That Don't Kill Performance: Patterns and Pitfalls

Framer Motion Animations That Don't Kill Performance: Patterns and Pitfalls

Framer Motion makes animations easy. It also makes performance problems easy.
Here's how to get smooth 60fps without the jank.

The Golden Rule: Animate Transform and Opacity Only

// Bad — triggers layout recalculation (expensive)
<motion.div animate={{ width: '200px', height: '100px', left: '50px' }} />

// Good — GPU-composited, no layout (cheap)
<motion.div animate={{ x: 50, scale: 1.1, opacity: 0.8 }} />
Enter fullscreen mode Exit fullscreen mode

Properties that trigger layout: width, height, top, left, padding, margin.
Properties that don't: x, y, scale, rotate, opacity, skew.

Entry Animations

const fadeUp = {
  hidden: { opacity: 0, y: 24 },
  visible: {
    opacity: 1,
    y: 0,
    transition: { duration: 0.4, ease: 'easeOut' },
  },
}

function Card({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      variants={fadeUp}
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true }}  // only animate once, not on scroll back
    >
      {children}
    </motion.div>
  )
}
Enter fullscreen mode Exit fullscreen mode

viewport={{ once: true }} is critical — without it, the animation fires every time the element enters the viewport.

Staggered Children

const container = {
  hidden: {},
  visible: {
    transition: { staggerChildren: 0.1, delayChildren: 0.2 },
  },
}

const item = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0, transition: { duration: 0.3 } },
}

function ProductGrid({ products }: { products: Product[] }) {
  return (
    <motion.div
      variants={container}
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true }}
      className="grid grid-cols-3 gap-4"
    >
      {products.map(product => (
        <motion.div key={product.id} variants={item}>
          <ProductCard product={product} />
        </motion.div>
      ))}
    </motion.div>
  )
}
Enter fullscreen mode Exit fullscreen mode

Hover and Tap States

function Button({ children, onClick }: ButtonProps) {
  return (
    <motion.button
      onClick={onClick}
      whileHover={{ scale: 1.02 }}
      whileTap={{ scale: 0.98 }}
      transition={{ type: 'spring', stiffness: 400, damping: 17 }}
    >
      {children}
    </motion.button>
  )
}
Enter fullscreen mode Exit fullscreen mode

Spring physics feel more natural than linear transitions for interactive elements.

Layout Animations

function ExpandableCard({ title, content }: CardProps) {
  const [expanded, setExpanded] = useState(false)

  return (
    <motion.div layout onClick={() => setExpanded(!expanded)}>
      <motion.h3 layout>{title}</motion.h3>
      {expanded && (
        <motion.p
          initial={{ opacity: 0 }}
          animate={{ opacity: 1 }}
          exit={{ opacity: 0 }}
        >
          {content}
        </motion.p>
      )}
    </motion.div>
  )
}
Enter fullscreen mode Exit fullscreen mode

layout prop automatically animates size changes — no explicit width/height animation.

AnimatePresence for Exit Animations

import { AnimatePresence, motion } from 'framer-motion'

function Notifications({ notifications }: { notifications: Notification[] }) {
  return (
    <AnimatePresence>
      {notifications.map(notification => (
        <motion.div
          key={notification.id}
          initial={{ opacity: 0, x: 100 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ opacity: 0, x: 100 }}
          transition={{ duration: 0.2 }}
        >
          {notification.message}
        </motion.div>
      ))}
    </AnimatePresence>
  )
}
Enter fullscreen mode Exit fullscreen mode

Without AnimatePresence, elements just disappear — no exit animation.

Performance Tips

// 1. Use style prop for non-animating styles (skips motion overhead)
<motion.div animate={{ opacity: 1 }} style={{ position: 'absolute' }} />

// 2. Disable animations for users who prefer reduced motion
import { useReducedMotion } from 'framer-motion'

function AnimatedCard() {
  const shouldReduce = useReducedMotion()
  return (
    <motion.div
      animate={{ y: shouldReduce ? 0 : [0, -10, 0] }}
    />
  )
}

// 3. Avoid animating hundreds of elements simultaneously
// Cap stagger at ~20 items, virtualize the rest
Enter fullscreen mode Exit fullscreen mode

The AI SaaS Starter Kit ships with Framer Motion animations pre-configured on the landing page and dashboard — all using GPU-composited properties. $99 one-time.

Top comments (0)