Framer Motion (sekarang diganti nama menjadi Motion) berbobot sekitar 34KB yang dimampatkan — 32KB jika Anda menggunakan impor penuh. Animasi CSS murni berjalan di thread compositor dan menambahkan 0KB ke bundle JavaScript Anda. Portofolio saya (matthewswong.com) menggunakan Framer Motion untuk transisi halaman dan animasi komponen. Saya juga membangun proyek di mana mengganti Framer Motion dengan CSS menghemat 50KB bundle dan meningkatkan skor Lighthouse sebesar 8 poin.
Transform dan transisi CSS berjalan di thread compositor GPU — sepenuhnya terpisah dari thread JavaScript utama. Tidak ada eksekusi JavaScript berarti animasi tidak bersaing dengan event handler, render, atau kode lain untuk waktu CPU. Animasi JavaScript (termasuk Framer Motion) berjalan di thread utama via requestAnimationFrame. Framer Motion menggunakan akselerasi GPU secara cerdas, tapi orkestrasi JavaScript tetap terjadi di thread utama.
Bundle 34KB Framer Motion penting untuk performa, terutama di mobile. Kabar baiknya: komponen LazyMotion Motion mengurangi bundle awal menjadi ~6KB dengan memuat fitur sesuai permintaan. Gunakan LazyMotion dengan domAnimation untuk fitur yang paling umum (animate, initial, exit, transition) — ini mencakup 90% kasus penggunaan dengan 6KB alih-alih 34KB.
CSS Animations (compositor thread)
──────────────────────────────────────────
Main Thread [JS execution] [React render] [other work]
│
Compositor Thread ├── transform: translateY(-10px) ← GPU, silky smooth
└── opacity: 0 → 1 ← GPU, no main thread
Framer Motion (main thread orchestration)
──────────────────────────────────────────
Main Thread [JS] [React] [Motion RAF loop] [other work] ← competing
│
Compositor Thread └── final GPU transform ← still GPU
(after JS orchestration)
Bundle size comparison:
CSS animations: 0 KB added to bundle
Framer Motion (full): ~34 KB minified+gzip
Framer Motion (LazyMotion + domAnimation): ~6 KB initial
GSAP (for comparison): ~23 KB
When each wins:
┌────────────────────┬──────────────────┬────────────────────────┐
│ Use CSS for: │ Use Framer for: │ Skip entirely: │
├────────────────────┼──────────────────┼────────────────────────┤
│ Hover effects │ Layout animations│ Animating width/height │
│ Loading spinners │ Exit animations │ (causes reflow) │
│ Skeleton shimmer │ Drag gestures │ Animating box-shadow │
│ Button press │ Spring physics │ (use filter: drop- │
│ Color transitions │ Page transitions │ shadow instead) │
│ Focus rings │ Shared elements │ │
└────────────────────┴──────────────────┴────────────────────────┘Dari mengoptimalkan skor Lighthouse matthewswong.com setelah menambahkan Framer Motion: bungkus semua komponen motion dalam LazyMotion dengan domAnimation dan domMax hanya di mana diperlukan. Juga, tambahkan will-change: transform ke elemen yang sering dianimasikan — ini memberi petunjuk ke browser untuk mempromosikannya ke lapisan compositor mereka sendiri sebelum animasi dimulai. Kombinasi ini menurunkan skor layout shift terkait animasi saya dari 0,12 menjadi 0,02.
CSS menang untuk: (1) Animasi sederhana dan stateless — efek hover, spinner loading, shimmer skeleton, umpan balik penekanan tombol. Ini selalu harus CSS. (2) Situs portofolio dan halaman marketing di mana 34KB JavaScript library animasi mengurangi poin Lighthouse nyata. (3) Animasi yang tidak bergantung pada state JavaScript — jika animasi berjalan dengan cara yang sama terlepas dari state aplikasi, CSS keyframe adalah alat yang tepat.
// CSS — simple fade-in (prefer this for static animations)
// globals.css
@keyframes fadeIn {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-fade-in {
animation: fadeIn 0.3s ease-out forwards;
}
// Tailwind arbitrary value (no CSS needed):
<div className="animate-[fadeIn_0.3s_ease-out_forwards]">...</div>
// ──────────────────────────────────────────────────
// Framer Motion — LazyMotion for minimal bundle
import { LazyMotion, domAnimation, m } from "framer-motion"
// Wrap app once (e.g., in layout.tsx)
export default function Layout({ children }) {
return (
<LazyMotion features={domAnimation}>
{children}
</LazyMotion>
)
}
// Use m.div instead of motion.div — works with LazyMotion
<m.div
initial={{ opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -8 }}
transition={{ duration: 0.3 }}
>
Content
</m.div>
// Layout animation — impossible in CSS alone
<m.li layout key={item.id}> {/* animates position change automatically */}
{item.name}
</m.li>
// Exit animations with AnimatePresence
import { AnimatePresence } from "framer-motion"
<AnimatePresence mode="wait">
{isVisible && (
<m.div
key="modal"
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
>
<Modal />
</m.div>
)}
</AnimatePresence>Framer Motion menang untuk: (1) Animasi layout — menganimasikan antar state layout (pengurutan daftar, kartu yang berkembang) dengan prop layout. (2) Animasi keluar — menganimasikan komponen saat unmount. AnimatePresence menangani ini dengan elegan. (3) Gerakan berbasis fisika — animasi spring, drag dengan momentum, chaining gesture. (4) Transisi elemen bersama antar halaman — layoutId Framer Motion menangani ini; CSS saja tidak bisa.
Komponen Framer Motion adalah Client Components — mereka membutuhkan browser DOM dan React client runtime. Setiap komponen yang dibungkus dalam <motion.div> memerlukan 'use client'. Dalam aplikasi Next.js App Router, membungkus bagian besar halaman Anda dalam komponen motion memaksa bagian-bagian tersebut menjadi Client Components, kehilangan manfaat Server Component. Jaga Framer Motion di level daun — bungkus elemen interaktif individual, bukan bagian halaman yang seluruhnya.
Pendekatan saya saat ini di matthewswong.com: CSS Tailwind animate-* untuk animasi mikro (hover, focus, loading state), Framer Motion dengan LazyMotion untuk animasi makro (transisi halaman, penampilan konten, gesture interaktif). Halaman masuk dengan fade-in Framer Motion, tapi setiap state hover tombol adalah Tailwind CSS transition-colors. Ini menjaga bundle tetap fokus — Framer Motion mendapatkan 6KB-nya dengan fitur yang tidak bisa ditandingi CSS.
Untuk proyek apa pun dengan animasi interaktif (drag, spring, transisi layout, animasi keluar), saya menggunakan Framer Motion dengan LazyMotion. DX sangat baik — state animasi deklaratif, animasi layout bersama, dan dukungan gesture yang membutuhkan berminggu-minggu untuk diimplementasikan dengan benar dalam vanilla JS. Untuk proyek yang lebih sederhana atau halaman yang kritis terhadap performa, saya menggunakan CSS saja.
Ukur sebelum mengoptimalkan. Panel Performa Chrome DevTools menampilkan frame rate animasi dan hambatan thread utama. Panel Rendering menampilkan paint flashing (merah berarti repainting) dan lapisan kompositing. Animasi CSS yang hanya menganimasikan properti transform dan opacity akan ditampilkan sebagai compositor-thread saja. Untuk Framer Motion, verifikasi bahwa elemen yang Anda animasikan adalah GPU-composited.