blogprojectshajspace
Back to blog

i deleted framer motion and nothing broke

·3 min read

My portfolio used to run on Framer Motion. Every hover effect, every entrance animation, every subtle transition—powered by a 50kb JavaScript library.

Then I noticed animations stuttering on my old MacBook Air. JavaScript animations run on the main thread, competing with rendering and user input. CSS transforms run on the GPU. Different thread, smoother animations.

So I deleted Framer Motion. The site got faster and nothing broke.

the conversion

25 components. Same pattern every time: replace Framer Motion components with HTML elements, swap animation props for CSS classes.

Hover effects:

jsx
// before
<motion.button
  whileHover={{ scale: 1.02 }}
  whileTap={{ scale: 0.98 }}
>
  click me
</motion.button>

// after
<button className="hover:scale-[1.02] active:scale-[0.98] transition-transform duration-200">
  click me
</button>

Entrance animations needed a hook:

ts
export function useEntranceAnimation(delay = 0) {
  const [isVisible, setIsVisible] = useState(false)
  const ref = useRef(null)

  useEffect(() => {
    const observer = new IntersectionObserver(
      (entries) => {
        if (entries[0].isIntersecting) {
          setTimeout(() => setIsVisible(true), delay)
          observer.unobserve(entries[0].target)
        }
      },
      { threshold: 0.1, rootMargin: '-40% 0px' }
    )

    if (ref.current) observer.observe(ref.current)
    return () => observer.disconnect()
  }, [delay])

  return {
    ref,
    className: isVisible
      ? 'opacity-100 translate-y-0'
      : 'opacity-0 translate-y-5'
  }
}

Intersection Observer detects viewport entry. CSS classes trigger transitions. GPU handles the actual animation.

the hard part

AnimatePresence was tricky. Framer Motion handles mount/unmount transitions automatically. I had to build a custom component that manages visibility state and applies CSS transitions during both enter and exit phases.

Worth it though. No more framework magic I couldn't debug.

the results

Metric Before After
Bundle size +50kb 0
Animation thread Main GPU
Scroll stuttering Sometimes Never
Hover jank Occasional None

First Contentful Paint improved. Time to Interactive improved. The site just feels snappier.

bonus: centralized timing

All animations now use CSS custom properties:

css
:root {
  --motion-base: 200ms;
  --motion-ease-ios: cubic-bezier(0.22, 1, 0.36, 1);
}

.animate {
  transition: transform var(--motion-base) var(--motion-ease-ios);
}

One variable change adjusts the feel of every animation on the site.

when framer motion makes sense

Complex gesture sequences. Physics-based springs. Coordinated multi-element animations. If you're building something like Stripe's homepage, use Framer Motion.

For a portfolio with hover effects and fade-ins? CSS is enough. The platform already has everything you need.

50kb sounds small. But it's 50kb to download, parse, and execute on every visit. Dependencies compound. Sometimes the best code is the code you delete.

Back to blog