React UI animation is a topic where developer enthusiasm and production reality regularly collide. Framer Motion makes it trivial to add spring physics and layout animations to every component. The result looks great in development. Then you open DevTools on a mid-range Android device over a 4G connection and watch your Interaction to Next Paint score crater. I’ve been through this cycle enough times to have a clear framework now for when to animate, what tool to reach for, and — critically — what to cut. Here’s how I think about React animation performance optimization in 2026.

The Problem: Animation Libraries Add Real Weight

Framer Motion, the dominant animation library for React, adds around 50-60KB gzipped to your bundle. On a fast connection and a high-end device, irrelevant. On a mobile device in India or Southeast Asia on a mid-tier data plan, that’s a noticeable delay before the page becomes interactive. The bundle size isn’t the only issue — Framer Motion’s layout animations trigger browser reflows, and if you’re animating layout properties (width, height, top, left) rather than transform and opacity, you’re burning GPU cycles unnecessarily.

This isn’t a reason to avoid Framer Motion. It’s a reason to use it deliberately.

My Decision Tree for React Animations

Step 1: Can CSS do this?

Before reaching for any JavaScript animation library, I ask whether pure CSS transitions can handle it. Hover states, simple fade-ins, slide-in drawers, tooltip reveals — all of these work in CSS without a single line of JavaScript. CSS animations run on the compositor thread, which means they don’t block the main thread and don’t get janky under load.

My rule: if an animation is triggered by a CSS state change (hover, focus, active, a class toggle), use CSS. Only move to JS when you need:

  • Animations that depend on runtime values (drag distance, scroll position, dynamic content height)
  • Sequenced or chained animations across multiple elements
  • Spring physics or interrupt-able animations (something that reverses mid-animation based on user input)
  • Layout animations (an element changing position in the DOM and animating to its new position)

Step 2: Framer Motion or GSAP?

If CSS won’t cover it, the next question is which library. My general split:

  • Framer Motion: component-level animations, React state-driven transitions, layout animations, the AnimatePresence exit animation pattern. Lives in the React component tree.
  • GSAP: scroll-driven animations, complex timelines, canvas or SVG animations, anything where precise timing control matters more than React integration. Lives outside the component tree.

Mixing both on the same project is fine — I often use CSS for hover states, Framer Motion for component transitions, and GSAP for scroll-driven effects. They don’t conflict if you’re clear about ownership.

Step 3: What’s the Reduced Motion fallback?

This is non-negotiable for me: every animated React component I build checks the prefers-reduced-motion media query. Users who have enabled this in their OS settings (common for people with vestibular disorders) should get an instant transition, not a flying element. Framer Motion supports this via the useReducedMotion hook. GSAP supports it via matchMedia.

The Interesting Technical Part: Layout Animations Without Reflow

The most common performance mistake I see in animated React UIs: triggering layout animations by changing width, height, top, or left properties. Every change to these properties causes a layout recalculation across the entire document, which is expensive.

The fix is the FLIP technique (First, Last, Invert, Play) — and Framer Motion’s layout prop implements this for you automatically. Instead of animating the dimension change directly, the browser:

  1. Reads the element’s starting position (First)
  2. Makes the layout change instantly (Last)
  3. Calculates the difference and applies an inverted transform to make the element appear in its old position (Invert)
  4. Animates the transform to zero, making it appear to move smoothly to the new position (Play)

Since transforms don’t trigger layout recalculations, this is dramatically faster than animating layout properties directly. In practice, this means adding layout to a Framer Motion component and letting it handle the math. The gotcha: every element that might be affected by the layout shift also needs layout on it, or you get jumpy neighbouring elements.

Bundle Optimization: Only Ship What You Use

Framer Motion supports tree-shaking, but only if you import from specific subpaths rather than the root. If you’re doing simple transitions and don’t need the physics engine, you can reduce the footprint meaningfully by using the m component (lightweight version) instead of motion and importing LazyMotion with the domAnimation feature set rather than loading the full library.

For projects where bundle size is critical — a Web3 app I worked on had a 200KB JS budget for the initial bundle — I’ve completely replaced Framer Motion with CSS transitions plus a custom useInView hook built on IntersectionObserver. The IntersectionObserver hook adds a class when an element enters the viewport; CSS handles the animation. Zero JS animation library, near-zero performance cost.

Accessibility Beyond Reduced Motion

Animation accessibility is more than just prefers-reduced-motion. A few other things I check:

  • Don’t animate focus indicators — users navigating by keyboard need to see exactly where focus is at all times
  • Avoid animations that loop indefinitely without a pause mechanism (WCAG 2.1, Success Criterion 2.2.2)
  • Don’t use animation to convey critical information — if the only way to know an action succeeded is by watching a spinner, add an accessible status message alongside it
  • Test on actual mid-range Android hardware, not just DevTools device emulation — the emulator underestimates real-world jank

What This Looks Like in a Real Project

On a React dashboard I built for a Web3 platform, we had complex state transitions: wallet connection flow (connecting → confirming → connected, with loading states), transaction status cards that animate in when new events arrive, and a modal system with enter/exit animations. We used Framer Motion for all of these. The AnimatePresence wrapper handled the exit animations cleanly, which is the one thing CSS genuinely cannot do without JavaScript — animating an element as it’s unmounted from the DOM.

Total animation-related JS: about 38KB gzipped after tree-shaking. Acceptable for a dashboard app where the user has already authenticated and the initial load is less critical than the ongoing interaction quality. For a marketing landing page, that number would be too high and I’d reach for CSS instead.