Framer Motion unggul untuk kasus yang tidak bisa ditangani CSS dengan bersih: animasi layout (pengurutan daftar, kartu yang berkembang via prop layout), animasi keluar saat komponen unmount via AnimatePresence, gerakan berbasis fisika seperti spring dan drag dengan momentum, serta transisi elemen bersama antar halaman menggunakan layoutId. Jika animasi bergantung pada state JavaScript atau membutuhkan chaining gesture, Framer Motion adalah pilihan yang tepat.
Bundle penuh Framer Motion (sekarang bernama Motion) berukuran sekitar 34KB setelah dimampatkan. Anda bisa mengurangi muatan awal menjadi sekitar 6KB dengan menggunakan komponen LazyMotion bersama domAnimation, yang mencakup fitur paling umum — animate, initial, exit, dan transition — untuk sekitar 90% kasus penggunaan. domMax hanya perlu ditambahkan di tempat yang benar-benar diperlukan.
Transform dan transisi CSS berjalan di thread compositor GPU, sepenuhnya terpisah dari eksekusi JavaScript. Artinya animasi tidak bersaing dengan event handler, proses render, atau kode lain untuk waktu CPU. Framer Motion tetap menggunakan requestAnimationFrame di thread utama untuk orkestrasinya, meski menerapkan akselerasi GPU via transform: translate3d() dan will-change. Untuk animasi masuk sederhana seperti fade-in atau slide-up, CSS jelas lebih cepat.
Mengganti sebagian penggunaan Framer Motion dengan CSS menghemat 50KB bundle dan meningkatkan skor Lighthouse sebesar 8 poin di halaman yang kritis terhadap performa. Dari sisi animasi, membungkus komponen motion dalam LazyMotion dengan domAnimation dan menambahkan will-change: transform ke elemen yang sering dianimasikan berhasil menurunkan skor layout shift terkait animasi dari 0,12 menjadi 0,02.
Komponen Framer Motion adalah Client Components dan memerlukan direktif 'use client'. Membungkus bagian besar halaman dalam komponen motion memaksa bagian tersebut keluar dari server, sehingga kehilangan manfaat Server Component. Jaga Framer Motion di level daun — bungkus elemen interaktif individual, bukan keseluruhan layout halaman — agar pengambilan data dan struktur halaman tetap berjalan di sisi server.
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.
| Dimensi | Framer Motion | CSS Animations |
|---|---|---|
| Ukuran bundle | Sekitar 34KB minified dan gzipped | 0KB tambahan ke bundle JavaScript |
| Thread eksekusi | Berjalan di main thread lewat JavaScript | Berjalan di compositor thread untuk transform dan opacity |
| Dukungan gesture | Drag, hover, dan tap gesture bawaan | Butuh event handling JavaScript manual |
| Animasi layout | Transisi layout otomatis berbasis FLIP | Butuh keyframes manual untuk perubahan layout |
| Orkestrasi | Variants deklaratif dan staggered children | Chaining animation-delay manual |
| Batas performa | Bisa drop frame di perangkat low-end saat pemakaian berat | Konsisten mencapai 60fps untuk transform dan opacity |
| Paling cocok untuk | UI kompleks, interaktif, berbasis gesture | Transisi sederhana dan animasi halaman statis |
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.