Przejdź do treści

Wydajne animacje Framer Motion w Next.js

Opanuj optymalizację Framer Motion pod kątem Core Web Vitals. Twórz płynne animacje w Next.js bez negatywnego wpływu na LCP, INP oraz CLS.

Maciej Sala

Founder StriveLab

10 min czytaniaOpublikowano 10 kwietnia 2026 (Aktualizacja 13 lipca 2026)

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

Code
npm install motion

W nowych projektach instaluj pakiet motion i importuj API z motion/react:

Code
'use client' // Animacje wymagają Client Component
 
import { motion } from 'motion/react'
 
export function FadeIn({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 20 }}
      animate={{ opacity: 1, y: 0 }}
      transition={{ duration: 0.5 }}
    >
      {children}
    </motion.div>
  )
}

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':

Code
import * as motion from 'motion/react-client'
 
export function ServerRenderedBadge() {
  return <motion.div animate={{ opacity: 1 }}>Gotowe</motion.div>
}

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.

ScenariuszNajlepszy wybór
Hover, focus, prosty fade-inCSS transition/keyframes
Hero i pierwszy widok strony contentowejstatyczny HTML/CSS, minimum JS
Karty wchodzące przy scrollu niżej na stronieMotion ładowany leniwie albo whileInView
Modale, toasty, elementy z exitMotion + AnimatePresence
Dashboard z filtrowaniem, drag, reorder, layout transitionsMotion, ale z pomiarem bundla i INP
Parallax, duży scroll-driven storytellingostroż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.

Code
// PREFERUJ — zwykle bez layoutu i paintu, ale zawsze mierz
<motion.div
  initial={{ opacity: 0, scale: 0.95, y: 20 }}
  animate={{ opacity: 1, scale: 1, y: 0 }}
  transition={{ duration: 0.3 }}
>
  {children}
</motion.div>
 
// UNIKAJ — powoduje layout recalculation
<motion.div
  initial={{ width: 0, height: 0 }}
  animate={{ width: '100%', height: 'auto' }}
/>

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:

Code
// components/motion-features.ts
import { domAnimation } from 'motion/react'
 
export default domAnimation
Code
'use client'
 
import { LazyMotion } from 'motion/react'
import * as m from 'motion/react-m'
 
const loadFeatures = () =>
  import('./motion-features').then((module) => module.default)
 
export function MotionShell({ children }: { children: React.ReactNode }) {
  return (
    <LazyMotion features={loadFeatures} strict>
      <m.div initial={{ opacity: 0 }} whileInView={{ opacity: 1 }}>
        {children}
      </m.div>
    </LazyMotion>
  )
}

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:

Code
'use client'
 
import { motion } from 'motion/react'
 
export function ScrollRevealCard({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0, y: 40 }}
      whileInView={{ opacity: 1, y: 0 }}
      viewport={{ once: true, margin: '0px 0px 100px 0px' }}
      transition={{ duration: 0.6, ease: 'easeOut' }}
      className="rounded-xl border p-6"
    >
      {children}
    </motion.div>
  )
}

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

Code
'use client'
 
import { motion } from 'motion/react'
 
const containerVariants = {
  hidden: {},
  visible: {
    transition: {
      staggerChildren: 0.1, // 100ms między dziećmi
    },
  },
}
 
const itemVariants = {
  hidden: { opacity: 0, y: 20 },
  visible: { opacity: 1, y: 0 },
}
 
export function StaggeredList({ items }: { items: string[] }) {
  return (
    <motion.ul
      variants={containerVariants}
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true }}
      className="space-y-3"
    >
      {items.map((item) => (
        <motion.li
          key={item}
          variants={itemVariants}
          transition={{ duration: 0.4 }}
          className="rounded-lg bg-white p-4 shadow-sm"
        >
          {item}
        </motion.li>
      ))}
    </motion.ul>
  )
}

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:

Code
'use client'
 
import { motion, AnimatePresence } from 'motion/react'
import { useState } from 'react'
 
type Item = {
  id: string
  name: string
  category: 'design' | 'dev' | 'marketing'
}
 
export function FilterableGrid({ items }: { items: Item[] }) {
  const [filter, setFilter] = useState('all')
 
  const filtered =
    filter === 'all' ? items : items.filter((item) => item.category === filter)
 
  return (
    <div>
      <div className="mb-6 flex gap-2">
        {['all', 'design', 'dev', 'marketing'].map((cat) => (
          <button
            key={cat}
            onClick={() => setFilter(cat)}
            className={`rounded-full px-4 py-2 ${
              filter === cat ? 'bg-blue-600 text-white' : 'bg-gray-100'
            }`}
          >
            {cat}
          </button>
        ))}
      </div>
 
      <motion.div layout className="grid grid-cols-3 gap-4">
        <AnimatePresence mode="popLayout">
          {filtered.map((item) => (
            <motion.div
              key={item.id}
              layout
              initial={{ opacity: 0, scale: 0.8 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.8 }}
              transition={{ duration: 0.3 }}
              className="rounded-lg border p-4"
            >
              <h3>{item.name}</h3>
              <p>{item.category}</p>
            </motion.div>
          ))}
        </AnimatePresence>
      </motion.div>
    </div>
  )
}

6. Animacja wejścia strony przez template.tsx

Code
// app/template.tsx
'use client'
 
import { motion } from 'motion/react'
 
export default function Template({ children }: { children: React.ReactNode }) {
  return (
    <motion.div
      initial={{ opacity: 0 }}
      animate={{ opacity: 1 }}
      transition={{ duration: 0.3 }}
    >
      {children}
    </motion.div>
  )
}

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:

Code
// 1. Nie animuj agresywnie elementów above the fold
// Hero, nagłówek i nawigacja lepiej utrzymać statyczne dla szybszego perceived performance
 
// 2. Animuj opacity lub transform
<motion.div initial={{ opacity: 0 }} animate={{ opacity: 1 }}>
  {/* Brak zmian layoutu */}
</motion.div>
 
// 3. Rezerwuj miejsce dla animowanego bloku
// Problemem nie jest transform, tylko brak stabilnego layoutu wokół elementu

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:

  1. Nie sortuj i nie filtruj dużych kolekcji synchronicznie w handlerze kliknięcia.
  2. Nie animuj layout na bardzo dużych gridach bez wirtualizacji albo limitu elementów.
  3. Unikaj animacji zależnych od onScroll, jeśli każda klatka aktualizuje stan Reacta.
  4. Przy cięższych zmianach UI używaj wirtualizacji albo dzielenia pracy; useTransition może zmienić priorytet renderu, ale nie usuwa kosztu obliczeń.
  5. 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:

Code
'use client'
 
import { MotionConfig } from 'motion/react'
 
export function MotionProvider({ children }: { children: React.ReactNode }) {
  return <MotionConfig reducedMotion="user">{children}</MotionConfig>
}

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:

Code
'use client'
 
import { motion, useReducedMotion } from 'motion/react'
 
export function AccessibleReveal({ children }: { children: React.ReactNode }) {
  const shouldReduceMotion = useReducedMotion()
 
  return (
    <motion.div
      initial={shouldReduceMotion ? { opacity: 0 } : { opacity: 0, y: 24 }}
      animate={shouldReduceMotion ? { opacity: 1 } : { opacity: 1, y: 0 }}
      transition={{ duration: shouldReduceMotion ? 0.15 : 0.35 }}
    >
      {children}
    </motion.div>
  )
}

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ć, ile motion dokłada do entrypointu

  • W 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ę.

  • useReportWebVitals w 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:

Code
// app/_components/web-vitals.tsx
'use client'
 
import { useReportWebVitals } from 'next/web-vitals'
 
export function WebVitals() {
  useReportWebVitals((metric) => {
    if (
      metric.name === 'LCP' ||
      metric.name === 'INP' ||
      metric.name === 'CLS'
    ) {
      navigator.sendBeacon('/api/vitals', JSON.stringify(metric))
    }
  })
 
  return null
}

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 transform i opacity, 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 motion nie wpadł do wspólnego entrypointu całej aplikacji bez potrzeby?

  • Czy Lighthouse, bundle analyzer i dane terenowe nie pokazują regresji?

Micro-interactions w Next.js: małe animacje bez dużego kosztu

Code
'use client'
 
import { motion } from 'motion/react'
 
export function MotionButton() {
  return (
    <motion.button
      whileHover={{ scale: 1.02 }}
      whileTap={{ scale: 0.98 }}
      transition={{ type: 'spring', stiffness: 400, damping: 17 }}
      className="rounded-lg bg-blue-600 px-6 py-3 text-white"
    >
      Kliknij
    </motion.button>
  )
}
 
// Animowany toggle
export function AnimatedToggle({
  isOn,
  toggle,
}: {
  isOn: boolean
  toggle: () => void
}) {
  return (
    <button
      type="button"
      aria-pressed={isOn}
      aria-label="Przełącz funkcję"
      onClick={toggle}
      className={`flex h-8 w-14 cursor-pointer items-center rounded-full p-1 ${
        isOn ? 'bg-blue-600' : 'bg-gray-300'
      }`}
    >
      <motion.div
        layout
        className="h-6 w-6 rounded-full bg-white shadow"
        style={{ marginLeft: isOn ? 'auto' : 0 }}
        transition={{ type: 'spring', stiffness: 500, damping: 30 }}
      />
    </button>
  )
}
Audyt techniczny i optymalizacja pod kątem SEO i GEO.
Audyt techniczny SEO

Często zadawane pytania

Czy Framer Motion działa z Server Components?

Nie bezpośrednio, ponieważ animacje wymagają stanu i interaktywności, więc komponent z motion musi mieć dyrektywę 'use client'. Możesz jednak owijać Server Components w cienki Client Component z animacją, np. <FadeIn><ServerComponent /></FadeIn>. Dzięki temu treść renderuje się na serwerze, a po stronie klienta dochodzi tylko warstwa animacji.

Czy dodatkowy bundle JS z animacji to problem?

Zależy od kontekstu. Dla strony usługowej, gdzie liczy się każdy kilobajt i LCP, zacznij od zwykłych przejść CSS albo LazyMotion i lekkiego m. Kod ładuj warunkowo tylko tam, gdzie treść pozostaje dostępna bez niego. Dla dashboardów i aplikacji SaaS koszt biblioteki bywa w pełni akceptowalny, ponieważ użytkownik i tak ładuje sporo kodu aplikacji. Tam warto ładować ciężkie animacje w miejscach, gdzie są potrzebne.

Jaka jest alternatywa dla Framer Motion?

Do prostych efektów wystarczą animacje i przejścia czystego CSS — nie dokładają biblioteki JavaScript, choć nadal zużywają zasoby renderowania. Do animacji opartych na fizyce sprężyny sprawdza się react-spring, a do złożonych timeline'ów i sekwencji — gsap. Sam ekosystem Motion ma też lżejszą odmianę (motion/mini) dla niskopoziomowych animacji opartych o Web Animations API. Nie jest to jednak zamiennik komponentów React z motion/react.

Dlaczego animowanie width i height jest wolne?

Bo zmiana wymiarów elementu wymusza reflow, ponieważ przeglądarka musi ponownie przeliczyć układ dotkniętej części strony, a zwykle także ją odmalować. transform i opacity przeglądarka często może obsłużyć na etapie kompozycji bez zmiany layoutu. Nie gwarantuje to jednak osobnej warstwy, użycia GPU ani płynności — duże powierzchnie i nadmiar warstw też kosztują.

Jak nazywa się dziś pakiet Framer Motion?

Aktualny zalecany pakiet npm to motion, a import w React wygląda tak: motion/react. Starszy pakiet framer-motion pozostaje ścieżką kompatybilności dla istniejących projektów, ale w nowych wdrożeniach lepiej zaczynać od motion. Migracja do Motion for React polega zwykle na podmianie pakietu i importów.

Jak sprawdzić, czy animacje psują Core Web Vitals?

Najpierw porównaj build bez animacji i z animacjami w Lighthouse oraz analizatorze bundla, bo to pokaże koszt JavaScriptu, LCP, CLS i TBT. Potem sprawdź dane terenowe w Google Search Console, CrUX/PageSpeed Insights albo własny pomiar przez useReportWebVitals w Next.js. Lighthouse pomaga znaleźć przyczynę, ale o SEO decydują przede wszystkim dane z realnych użytkowników.

Czy trzeba obsługiwać prefers-reduced-motion?

Tak. Dobre animacje muszą respektować systemową preferencję ograniczenia ruchu. W Motion możesz użyć MotionConfig reducedMotion="user" dla całej aplikacji albo hooka useReducedMotion() w pojedynczym komponencie. Najczęściej nie trzeba usuwać całej animacji, ponieważ wystarczy zastąpić przesunięcie lub skalowanie prostym fade-in.

O autorze

Maciej Sala

Maciej Sala — Product Manager i Frontend Developer z bogatym doświadczeniem w marketingu internetowym oraz SEO. Na co dzień pracuje z Reactem, Next.js i TypeScriptem, a ostatnio także z Astro i narzędziami do automatyzacji procesów AI. Sprawnie łączy perspektywę produktową z praktycznym podejściem do kodu. Przez kilka lat był związany z branżą gier wideo jako project manager i game designer. Absolwent historii na Uniwersytecie Jagiellońskim oraz studiów podyplomowych z marketingu internetowego na AGH w Krakowie. Po godzinach trenuje na siłowni, maluje figurki i rozwijam własne projekty.

Pomagam przekładać takie tematy na konkretne wdrożenia w frontendzie, SEO, analityce i procesie produktowym.

Skontaktuj się ze mną

Biblioteka wiedzy na temat Next.js

Czytaj dalej

Zobacz więcej wpisów
Core Web Vitals — jak przyspieszyć stronę i poprawić pozycję w Google

Google od wielu lat podkreśla, że wydajność i doświadczenie użytkownika mają znaczenie i Core Web Vitals są częścią sygnałów page experience. Najważniejsze jest jednak to, że wreszcie mierzą coś bardzo konkretnego: czy strona błyskawicznie pokazuje treść, czy odpowiednio reaguje na interakcje i czy nie „skacze" nieprzyjemnie podczas ładowania.

Maciej Sala

Maciej Sala

Founder StriveLab

Pagespeed 100/100 w Next.js — case study optymalizacji strony usługowej

Mamy tu typową stronę usługową zbudowaną w Next.js, wyposażoną w Google Analytics, Google Tag Manager , prosty formularz kontaktowy, galerię zdjęć i animacje – czyli standardowy zestaw. Na początku testy PageSpeed Insights pokazały wyniki: 67/100 na urządzeniach mobilnych i 89/100 na komputerach stacjonarnych.

Maciej Sala

Maciej Sala

Founder StriveLab

Jak blokować kod psujący LCP i INP w Next.js?

Wydajność rzadko psuje się jednym wielkim commitem. Częściej tak: jedna biblioteka, jeden cięższy obraz, jeden skrypt third-party, jedna tabelka bez wirtualizacji. Każda zmiana z osobna wygląda niewinnie. Po miesiącu LCP świeci na czerwono, bundle jest spuchnięty, a nikt nie potrafi wskazać winnego PR-a. Bramka wydajnościowa odwraca tę kolejność — zły PR dostaje czerwony status, zanim w ogóle trafi na produkcję.

Maciej Sala

Maciej Sala

Founder StriveLab