DEV Community

Brad Wrenne
Brad Wrenne

Posted on

I Evicted Framer Motion From a Client Site and Cut the Bundle by 27%

I run a small web agency in South Florida. We build Next.js sites for local businesses — construction companies, engineering firms, med spas. The kind of companies where every second of load time costs real money.

Last month I was doing a performance audit on a client site we'd built and noticed something ugly: the mobile Lighthouse score was stuck in the low 70s. Desktop was fine — 95+. But mobile was getting murdered by Total Blocking Time.

I opened the bundle analyzer and there it was. Framer Motion was the single largest dependency in the entire project. We were using it for maybe six animations — a few fade-ins on scroll, a mobile nav slide, some hover effects. Six animations. 40KB+ of JavaScript shipped to every visitor.

That's when I decided to evict it entirely.

The Problem With Animation Libraries in 2026

Here's what nobody tells you about Framer Motion (or Motion, as it's now called): it's an incredible library. The API is beautiful. The spring physics are best-in-class. For complex, orchestrated, interactive animations, nothing else comes close.

But for the stuff most business websites actually need — fade-in on scroll, slide transitions, hover states — it's a sledgehammer for a finishing nail.

The issue isn't just the bundle size. It's the execution cost. Framer Motion runs JavaScript on every animation frame. On a flagship phone, that's fine. On the mid-range Android that most of your actual visitors carry, it's a TBT nightmare. Every animation that fires during page load adds to Total Blocking Time, and TBT is the metric that drags your mobile Lighthouse score through the floor.

What I Replaced It With

Two things: CSS transitions and IntersectionObserver. That's it. No new library. No npm install. Just platform APIs.

Fade-in on scroll (before)

import { motion } from 'framer-motion';

function FadeInSection({ children }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      whileInView={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
      viewport={{ once: true }}
    >
      {children}
    </motion.div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Fade-in on scroll (after)

.fade-in {
  opacity: 0;
  transform: translateY(16px);
  transition: opacity 0.5s ease, transform 0.5s ease;
}

.fade-in.visible {
  opacity: 1;
  transform: translateY(0);
}
Enter fullscreen mode Exit fullscreen mode
import { useEffect, useRef } from 'react';

function FadeInSection({ children }) {
  const ref = useRef(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          entry.target.classList.add('visible');
          observer.unobserve(entry.target);
        }
      },
      { threshold: 0.1 }
    );
    if (ref.current) observer.observe(ref.current);
    return () => observer.disconnect();
  }, []);

  return (
    <div ref={ref} className="fade-in">
      {children}
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Same visual result. Zero JavaScript executing during the animation itself — the browser's compositor handles the CSS transition on the GPU. No main thread blocking. No TBT impact.

Mobile nav (before)

<motion.div
  initial={{ x: '100%' }}
  animate={{ x: isOpen ? 0 : '100%' }}
  transition={{ type: 'spring', damping: 25 }}
>
  {/* nav content */}
</motion.div>
Enter fullscreen mode Exit fullscreen mode

Mobile nav (after)

.mobile-nav {
  transform: translateX(100%);
  transition: transform 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.mobile-nav.open {
  transform: translateX(0);
}
Enter fullscreen mode Exit fullscreen mode

You lose the spring physics. You gain a mobile nav that doesn't ship 40KB of JavaScript to parse before it can slide open. For a business website, that's a trade I'll make every time.

Hover effects

If you're importing an animation library for hover effects, stop. CSS has had :hover transitions since 2010.

.card {
  transition: transform 0.2s ease, box-shadow 0.2s ease;
}

.card:hover {
  transform: translateY(-2px);
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
Enter fullscreen mode Exit fullscreen mode

Done. No React re-renders. No JavaScript. Just CSS doing what CSS was built to do.

The Results

After removing Framer Motion and replacing all animations with CSS transitions + IntersectionObserver:

  • Bundle size: down 27%
  • Mobile Lighthouse Performance: 72 → 89
  • Total Blocking Time: dropped by ~180ms
  • Desktop Lighthouse: stayed at 100/100/100/100 (no visual regression)
  • Time to Interactive: improved by ~0.4s on mobile

The site looks identical. Every animation still fires. The mobile nav still slides. Cards still lift on hover. Sections still fade in on scroll. But now the browser is doing the animation work on the compositor thread instead of the main thread, and 40KB of JavaScript never gets shipped in the first place.

When You Should Keep Framer Motion

This isn't an anti-Framer-Motion post. Keep it if you need:

  • Layout animations (layout prop) — CSS can't animate between DOM position changes
  • AnimatePresence for exit animations — CSS has no concept of "animate this element as it unmounts"
  • Complex orchestrated sequences where multiple elements need coordinated spring physics
  • Drag and gesture interactions — this is where Framer truly shines

If you're building a creative portfolio, a SaaS product with complex UI transitions, or anything with drag-and-drop, Framer Motion earns its bundle cost.

But if you're building business websites — landing pages, service pages, marketing sites — and you're importing Framer Motion for fade-ins and hover effects, you're shipping a race car engine to power a bicycle.

The Takeaway

Every npm dependency is a tax your visitors pay on every page load. For business websites where conversion matters, that tax needs to justify itself. When I audited the animations we were actually using, none of them required JavaScript animation. They were all simple transitions that CSS handles natively, on the GPU, with zero main thread cost.

The rule I follow now: CSS first. IntersectionObserver for scroll triggers. Reach for a JS animation library only when CSS literally can't do what you need.

27% smaller bundle. 17-point Lighthouse jump on mobile. Same visual experience. No library needed.


I'm Brad, founder of Sumorai — we build high-performance Next.js websites and AI automation systems for growing businesses in South Florida. If you're dealing with similar performance issues on a client site, reach out.

Top comments (0)