Dlaczego animacje w Next.js mogą psuć wydajność?
Framer Motion, dziś rozwijany jako Motion for React, to aktualnie najpopularniejsza biblioteka animacji w React. Potrafi bardzo wiele, ale dokłada do klienta zauważalny kawałek JS. Trzeba przemyśleć, jak z niej korzystać, nie płacąc za to zbyt wysokim kosztem wydajności.
Instalacja Framer Motion w Next.js
W nowych projektach instaluj pakiet motion i importuj API z motion/react:
Podstawowe propsy to initial (stan początkowy), animate (stan docelowy), exit (stan przy odmontowaniu), transition (easing, duration, delay).
W App Routerze możesz też użyć motion/react-client, który jest oficjalnym punktem wejścia pozwalającym korzystać z komponentów Motion w pliku bez dodawania do całego modułu dyrektywy 'use client':
Ten import nadal tworzy granicę klientową i wysyła runtime potrzebny animacji. Pamiętaj, by nie pomylić go z LazyMotion, bo sam w sobie nie oznacza, że kod zostanie pobrany dopiero po wejściu elementu w viewport.
Jeśli utrzymujesz starszy projekt z framer-motion, nie musisz go przepisywać. Oficjalna ścieżka migracji do Motion for React to odinstalowanie framer-motion, instalacja motion i podmiana importów. Najlepiej to zrobić nie jako osobny refaktor, ale przy większej aktualizacji Reacta lub Next.js.
Kiedy Framer Motion ma sens, a kiedy wystarczy CSS?
Traktowanie biblioteki animacji jak domyślnego narzędzia do każdego ruchu w interfejsie jest błędne i tego nie rób. Na stronie usługowej, landing page'u albo blogu większość efektów wejścia, hoverów i prostych przejść zrobisz czystym CSS. Takie podejście nie dokłada biblioteki do JavaScriptu i zazwyczaj będzie najlepszym wyborem dla prostych efektów. Pamiętaj tylko, że CSS też kosztuje i animacja nadal może wykonywać layout, paint i compositing.
Motion to dobre rozwiązanie, gdy potrzebujesz stanu, gestów, animacji przy odmontowaniu, współdzielonych przejść między elementami, sprężyn albo koordynacji wielu elementów. Innymi słowy, używaj biblioteki tam, gdzie rozwiązuje problem, którego CSS nie rozwiązuje czytelnie. Jeśli CSS nie wystarcza, to wybierz Motion.
| Scenariusz | Najlepszy wybór |
|---|---|
| Hover, focus, prosty fade-in | CSS transition/keyframes |
| Hero i pierwszy widok strony contentowej | statyczny HTML/CSS, minimum JS |
| Karty wchodzące przy scrollu niżej na stronie | Motion ładowany leniwie albo whileInView |
Modale, toasty, elementy z exit | Motion + AnimatePresence |
| Dashboard z filtrowaniem, drag, reorder, layout transitions | Motion, ale z pomiarem bundla i INP |
| Parallax, duży scroll-driven storytelling | ostrożnie, zawsze z prefers-reduced-motion |
Wydajne animacje w Framer Motion, które nie psują Core Web Vitals
1. Preferuj transform i opacity
Przeglądarka może często animować transform (translate, scale, rotate) i opacity na etapie kompozycji, bez ponownego przeliczania układu. Animowanie width, height, top czy left zwykle uruchamia layout, a następnie paint. Ostateczny koszt zależy jednak od rozmiaru elementu, efektów wizualnych i decyzji przeglądarki o warstwach. will-change stosuj dopiero po pomiarze i tylko czasowo, a nadmiar warstw zwiększa zużycie pamięci.
2. Lazy loading Framer Motion bez ukrywania ważnej treści
Istotnym ograniczeniem Next.js jest to, że dynamiczny import Client Componenta wykonany bezpośrednio w Server Componencie nie ma obecnie gwarantowanego automatycznego code splittingu. next/dynamic oddziela kod i może ładować go warunkowo, gdy wywołujesz go wewnątrz Client Componenta. Opcja ssr: false działa wyłącznie w Client Components i jest przeznaczona głównie dla kodu wymagającego window lub document, a nie do ukrywania nagłówka, CTA czy treści SEO.
Dla Motion najbardziej przewidywalnym sposobem ograniczenia kosztu jest LazyMotion wraz z lekkim komponentem m. Pełny motion ma według dokumentacji około 34 kB, a m około 4,6 kB przed doładowaniem funkcji. Statyczne domAnimation zmniejsza główny runtime, ale dopiero funkcja zwracająca import() ładuje zestaw funkcji asynchronicznie:
Nie umieszczaj jednocześnie zwykłego komponentu motion pod LazyMotion, ponieważ może wciągnąć pełny zestaw funkcji i w ten sposób zniwelować jakiekolwiek oszczędności. Atrybut strict pomaga wykryć taki błąd. Rzeczywisty rozmiar chunka należy porównać w analizatorze bundla, gdyż deklarowane rozmiary biblioteki nie zawsze odpowiadają faktycznemu kosztowi dla konkretnej trasy po kompresji i .
3. Animacje przy scrollu z whileInView
Zamiast animować wszystko na starcie, po prostu animuj te elementy, gdy wchodzą do viewportu:
viewport.once: true uruchamia efekt tylko przy pierwszym wejściu. Dodatni dolny margin rozszerza obszar obserwacji i może uruchomić animację około 100 px przed wejściem elementu do fizycznego viewportu. Wartość ujemna działa odwrotnie, czyli będzie zawężać obszar i opóźniać start.
Wzorzec initial= niesie ze sobą dodatkowy koszt. Chodzi o styl zerowej przezroczystości, który jest już zawarty w prerenderowanym HTML-u. W konsekwencji, jeśli JavaScript nie zostanie pobrany lub hydratacja zakończy się błędem, treść może pozostać niewidoczna. Dlatego też nie należy stosować takiego efektu ujawniania jako warunku dostępu do ważnych informacji. Najbardziej niezawodna wersja domyślnie wyświetla czytelną treść, traktując animację jako progresywne ulepszenie.
Nie ukrywaj kandydata LCP przez opacity: 0
Elementy całkowicie przezroczyste są wykluczane z kandydatów LCP w Chromium. Jeśli hero, główny nagłówek albo największy obraz zaczyna od opacity: 0, użytkownik również go nie widzi, a jego późniejsze ujawnienie może przesunąć moment rozpoznania największej treści. To bezpośredni konflikt z celem LCP.
Dla treści pierwszego widoku ustaw initial={false}, pozostaw ją od początku widoczną albo animuj wyłącznie dekoracyjny element, który nie jest główną treścią. whileInView rezerwuj przede wszystkim dla sekcji poniżej pierwszego ekranu.
4. Staggered children i kaskadowe pojawianie się elementów
5. Layout animations i płynne zmiany układu
Framer Motion potrafi animować zmiany layoutu automatycznie. Dodaj layout prop, a pozycja i rozmiar elementu będą animowane przy każdej zmianie:
6. Animacja wejścia strony przez template.tsx
template.tsx montuje się ponownie przy każdej nawigacji (w odróżnieniu od layout.tsx), co sprawia, że animacja wejścia uruchamia się na każdej stronie.
Ten przykład nie realizuje animacji wyjścia, ponieważ samo dodanie AnimatePresence nie gwarantuje exit transition w App Routerze. Stare drzewo trasy może zostać usunięte, jeszcze zanim biblioteka zdoła je zatrzymać. W wypadku potrzeby pełnych przejść między dokumentami, rozważ natywne View Transitions po sprawdzeniu wsparcia docelowych przeglądarek. Nie opieraj krytycznej nawigacji na nieudokumentowanych obejściach routera.
W stronach bardziej opartych o treść i SEO-first traktuj page transitions ostrożnie, szczególnie jeśli przejście opóźnia pokazanie tytułu, blokuje scroll albo dokłada spory runtime tylko dla efektu. W aplikacji SaaS albo panelu administracyjnym taka animacja może pomagać w orientacji, ale na landing page'u zwykle lepiej zostawić nawigację natychmiastową.
Animacje a CLS: jak uniknąć przesunięć layoutu?
Sama zmiana transform lub opacity nie zmienia geometrii dokumentu, więc zwykle nie generuje wpisu . Nadal może jednak wizualnie zasłaniać treść albo wyjeżdżać poza kontener. CLS pojawia się przede wszystkim wtedy, gdy późno montujesz element, zmieniasz jego miejsce w układzie lub nie rezerwujesz przestrzeni. Pamiętaj też, że część przesunięć następujących krótko po interakcji użytkownika jest wyłączana z CLS — niski wynik nie dowodzi więc, że animacja jest wygodna.
Rozwiązania:
Jeśli animujesz kartę, modal, galerię albo sekcję ładowaną po czasie, ustaw stabilne wymiary kontenera: min-height, aspect-ratio, placeholder albo skeleton o takim samym rozmiarze. Użytkownik nie musi widzieć od razu finalnej zawartości, ale layout nie może zmieniać wysokości w trakcie ładowania.
Animacje a INP: jak nie blokować głównego wątku?
Animacja sama w sobie rzadko psuje , bo problem zaczyna się jak nachodzi na siebie kilka rzeczy: gdy interakcja uruchamia jednocześnie animację, ciężkie obliczenia, re-render dużej części drzewa Reacta i zmianę layoutu. Użytkownik klika filtr, karta ma płynnie przejść do nowego miejsca, a przeglądarka musi w tym samym momencie policzyć listę, zrenderować setki elementów i odmalować widok.
Pamiętaj o kilku zasadach praktycznych:
- Nie sortuj i nie filtruj dużych kolekcji synchronicznie w handlerze kliknięcia.
- Nie animuj
layoutna bardzo dużych gridach bez wirtualizacji albo limitu elementów. - Unikaj animacji zależnych od
onScroll, jeśli każda klatka aktualizuje stan Reacta. - Przy cięższych zmianach UI używaj wirtualizacji albo dzielenia pracy;
useTransitionmoże zmienić priorytet renderu, ale nie usuwa kosztu obliczeń. - Sprawdzaj React Profiler i Performance panel w Chrome DevTools, gdy INP rośnie po dodaniu animacji.
layout i AnimatePresence są bardzo wygodne, ale nie zwalniają z myślenia o koszcie renderowania. Animowanie 8 kart w filtrze portfolio to inny problem niż animowanie 800 elementów w tabeli produktowej.
INP mierzy czas od interakcji do następnej prezentacji klatki. Długa animacja, która zaczyna się już po pierwszym szybkim paint, nie musi sama podnieść wartości tej interakcji, choć nadal może klatkować i blokować kolejne kliknięcia. Dlatego obok INP sprawdzaj przebieg klatek, long tasks i responsywność podczas trwania efektu, a nie tylko pojedynczą liczbę.
prefers-reduced-motion w animacjach Framer Motion
Wydajność to nie tylko metryki Google, bo w skrajnych wypadkach animacje mogą przeszkadzać osobom z chorobą lokomocyjną, migrenami, zaburzeniami przedsionkowymi albo po prostu użytkownikom, którzy wyłączyli ruch w systemie. Jeżeli system mówi o ograniczeniu ruchu, wtedy interfejs powinien to uszanować.
Najprostszy wariant globalny:
reducedMotion="user" ogranicza transformacje i animacje layoutu u użytkowników z włączonym prefers-reduced-motion, ale zostawia bezpieczniejsze zmiany typu opacity. Dzięki temu rozwiązaniu, interfejs nadal może mieć subtelny feedback, bez agresywnego przesuwania i skalowania elementów.
W komponencie możesz też zdecydować sam:
To jest lepsze rozwiązanie niż całkowite wyłączanie feedbacku, bo użytkownik nadal widzi zmianę stanu, ale bez ruchu, który może być męczący albo fizycznie niekomfortowy.
Jak mierzyć wpływ animacji na Core Web Vitals?
Animacje najczęściej psują wydajność przez koszt JavaScriptu, hydratację, blokowanie głównego wątku i niestabilny layout. To trzeba zmierzyć w dwóch miejscach, w labie i w terenie.
Zielone progi w Core Web Vitals to LCP do 2,5 s, INP do 200 ms i CLS do 0,1, oceniane dla 75. percentyla odsłon. Nie optymalizuj ich jednak wyłącznie pod granicę, tylko porównuj tę samą trasę, segment urządzeń i wersję wdrożenia przed oraz po dodaniu animacji.
W labie, przed wdrożeniem:
Uruchom Lighthouse dla strony bez animacji i po dodaniu animacji
Sprawdź LCP, CLS i TBT; TBT traktuj jako laboratoryjny sygnał ryzyka dla INP
Użyj
@next/bundle-analyzer, żeby zobaczyć, ilemotiondokłada do entrypointuW Chrome DevTools Performance sprawdź, czy kliknięcie nie powoduje długich tasków i masowych re-renderów
W React Profiler zobacz, które komponenty renderują się po interakcji
W terenie, po wdrożeniu:
Google Search Console pokaże Core Web Vitals z danych CrUX w 28-dniowym oknie.
PageSpeed Insights połączy dane terenowe z laboratoryjną diagnozą.
Vercel Speed Insights albo własny RUM pokaże metryki w podziale na stronę, urządzenie i sesję.
useReportWebVitalsw Next.js pozwoli wysłać LCP, INP i CLS do własnej analityki.
Search Console i CrUX agregują dane i pojawiają się z opóźnieniem, dlatego nie nadają się do szybkiego testu A/B pojedynczego commita. Własny RUM pozwala dołączyć wersję wdrożenia, trasę i klasę urządzenia. Zbieraj dane zgodnie z zasadami prywatności i nie wysyłaj w URL-ach ani atrybutach danych osobowych.
Minimalny komponent pomiarowy:
Taki pomiar pomaga w znalezieniu odpowiedzi, czy animacja faktycznie pogarsza doświadczenie prawdziwych użytkowników na słabszych telefonach i wolniejszych połączeniach.
Lista kontrolna przed mergem animacji w Next.js
Przed dodaniem animacji do strony Next.js przejdź przez krótką listę:
- Czy efekt naprawdę wymaga Motion, czy wystarczy CSS?
- Czy animowany komponent nie jest elementem LCP?
Czy ważna treść pozostaje widoczna, gdy JavaScript lub hydratacja zawiedzie?
- Czy treść krytyczna renderuje się na serwerze?
Czy preferujesz
transformiopacity, a koszt potwierdzasz w DevTools?Czy kontener ma zarezerwowane miejsce i nie powoduje CLS?
Czy animacja działa sensownie z
prefers-reduced-motion?Czy animowany kontroler zachowuje semantykę, fokus i obsługę klawiatury?
Czy
motionnie wpadł do wspólnego entrypointu całej aplikacji bez potrzeby?Czy Lighthouse, bundle analyzer i dane terenowe nie pokazują regresji?

