Performance 12 Min. Lesezeit

Framer Motion Performance: Layout-Animationen, reduced-motion & Lazy-Mount

Framer Motion performant einsetzen: Layout-Animationen vs. CSS-transform, prefers-reduced-motion-Handling, Lazy-Mount für schwere Animations-Trees und Profiling-Tipps.

Framer Motion ist das de-facto Standard-Animations-Framework für React. Es abstrahiert die Web Animations API und bietet intuitive Declarative-Animation-Patterns. Das Problem: es wird oft so eingesetzt, dass es die Seiten-Performance verschlechtert — zu viel JavaScript, zu viele Layout-Reflows, kein reduced-motion-Respekt. Dieser Artikel zeigt, wie man es richtig macht.

Das Kernprinzip: Layout vs. Transform

Jede CSS-Eigenschaft fällt in eine von drei Kategorien, die unterschiedlich teuer zu animieren sind:

KategorieEigenschaftenKosten
Layoutwidth, height, top, left, margin, paddingSehr teuer (Layout + Paint + Composite)
Paintbackground-color, border, color, box-shadowMittel (Paint + Composite)
Compositetransform, opacityGünstig (nur Composite — GPU)

Immer wenn möglich, animiert man nur transform und opacity. Framer Motion macht das mit layout-Prop oft unsichtbar schwer:

// layout={true} kann Layout-Animationen auslösen — teuer
<motion.div layout>
  {items.map((item) => <Item key={item.id} {...item} />)}
</motion.div>

// Besser: nur explizite transform-basierte Animationen
<motion.div
  initial={{ opacity: 0, y: 20 }}
  animate={{ opacity: 1, y: 0 }}
  transition={{ duration: 0.3, ease: 'easeOut' }}
>
  {children}
</motion.div>

Wann layout=true akzeptabel ist

layout-Prop ist sinnvoll, wenn sich die tatsächliche Größe oder Position eines Elements ändert — z.B. beim Aufklappen eines Akkordeons oder beim Neu-Sortieren einer Liste. In diesem Fall gibt es keine CSS-only-Alternative. Aber: layout nie auf Listencontainern mit vielen Kindern einsetzen — es berechnet Layout-Änderungen für jedes Kind einzeln.

prefers-reduced-motion: Pflicht, nicht Optional

Nutzer, die “Bewegung reduzieren” in ihren Systemeinstellungen aktiviert haben, tun das aus gesundheitlichen Gründen — vestibuläre Störungen, Epilepsie, Photosensitivität. Animationen können bei diesen Nutzern körperliche Beschwerden auslösen.

Framer Motion bietet useReducedMotion():

import { motion, useReducedMotion } from 'framer-motion';

function AnimatedCard({ children }) {
  const shouldReduceMotion = useReducedMotion();

  const variants = {
    hidden: shouldReduceMotion
      ? { opacity: 0 }         // Kein Transform bei reduced-motion
      : { opacity: 0, y: 30 },
    visible: shouldReduceMotion
      ? { opacity: 1 }
      : { opacity: 1, y: 0 },
  };

  return (
    <motion.div
      variants={variants}
      initial="hidden"
      whileInView="visible"
      viewport={{ once: true }}
      transition={{ duration: shouldReduceMotion ? 0.01 : 0.4 }}
    >
      {children}
    </motion.div>
  );
}

Alternativ: eine globale CSS-Regel deckt alle Framer Motion Animationen ab:

/* global.css — deckt auch Framer Motion ab */
@media (prefers-reduced-motion: reduce) {
  *,
  ::before,
  ::after {
    animation-duration: 0.01ms !important;
    transition-duration: 0.01ms !important;
  }
}

Die CSS-Regel ist die sicherere Option, weil sie unabhängig von der JavaScript-Ausführung funktioniert. Beide Methoden kombinieren ist der robusteste Ansatz.

Bundle-Optimierung: Tree-Shaking und Lazy-Loading

Framer Motion ist ein umfangreiches Paket. Ohne Tree-Shaking landet die gesamte Bibliothek im Bundle — auch Features, die man nicht nutzt.

// Ungünstig: importiert das gesamte Framer Motion Paket
import { motion, AnimatePresence, useScroll, useTransform, LayoutGroup } from 'framer-motion';

// Besser: nur nutzen, was man wirklich braucht
import { motion } from 'framer-motion';

Framer Motion unterstützt Tree-Shaking, aber nur wenn der Bundler ES-Module-Imports korrekt auflöst. In Vite/Astro-Projekten ist das standardmäßig der Fall.

Lazy-Mount für schwere Animations-Trees

Wenn eine animierte Komponente nur unter bestimmten Bedingungen sichtbar ist (Modal, Sidebar, Drawer), sollte sie lazy gemountet werden:

import { lazy, Suspense, useState } from 'react';

// Lazy-Import: Framer Motion wird nur geladen, wenn die Komponente benötigt wird
const AnimatedModal = lazy(() => import('./AnimatedModal'));

export function App() {
  const [open, setOpen] = useState(false);

  return (
    <div>
      <button onClick={() => setOpen(true)}>Öffnen</button>
      {open && (
        <Suspense fallback={null}>
          <AnimatedModal onClose={() => setOpen(false)} />
        </Suspense>
      )}
    </div>
  );
}

AnimatePresence: korrekt einsetzen

AnimatePresence ermöglicht Exit-Animationen — wenn eine Komponente aus dem DOM entfernt wird. Falsch eingesetzt, blockiert es das Unmounting.

import { AnimatePresence, motion } from 'framer-motion';

export function NotificationStack({ notifications }) {
  return (
    <AnimatePresence initial={false}>
      {notifications.map((notification) => (
        <motion.div
          key={notification.id}
          initial={{ opacity: 0, x: 50 }}
          animate={{ opacity: 1, x: 0 }}
          exit={{ opacity: 0, x: 50 }}
          transition={{ duration: 0.2 }}
        >
          {notification.message}
        </motion.div>
      ))}
    </AnimatePresence>
  );
}

initial={false} bei AnimatePresence verhindert, dass beim ersten Render alle Kinder ihre Enter-Animation abspielen — nur neue Elemente animieren.

Häufiger Fehler: AnimatePresence ohne stabilen Key

// FALSCH: AnimatePresence erkennt Änderungen über den key-Prop
// Wenn key sich bei jedem Render ändert, animiert alles neu
<AnimatePresence>
  <motion.div key={Math.random()}>...</motion.div>
</AnimatePresence>

// RICHTIG: stabiler, eindeutiger key
<AnimatePresence>
  {items.map((item) => (
    <motion.div key={item.id}>...</motion.div>
  ))}
</AnimatePresence>

Astro-Integration: Framer Motion in Islands

In Astro werden React-Inseln mit einer der client:-Direktiven hydratisiert. Framer Motion-Animationen laufen ausschließlich clientseitig — kein SSR.

---
// Astro-Seite
import AnimatedSection from '../components/molecules/AnimatedSection.jsx';
---

<!-- client:visible: Hydratisierung + Animation erst wenn sichtbar -->
<AnimatedSection client:visible>
  <h2>Unsere Leistungen</h2>
  <!-- ... -->
</AnimatedSection>
// components/molecules/AnimatedSection.jsx
import { motion, useReducedMotion } from 'framer-motion';

export default function AnimatedSection({ children }) {
  const reduced = useReducedMotion();

  return (
    <motion.section
      initial={reduced ? { opacity: 0 } : { opacity: 0, y: 40 }}
      whileInView={reduced ? { opacity: 1 } : { opacity: 1, y: 0 }}
      viewport={{ once: true, margin: '-100px' }}
      transition={{ duration: reduced ? 0.1 : 0.5, ease: 'easeOut' }}
    >
      {children}
    </motion.section>
  );
}

viewport={{ once: true }} stellt sicher, dass die Animation nur einmal ausgeführt wird — beim ersten Einblenden. Ohne once: true wird sie bei jedem Scroll-Ereignis wiederholt.

Profiling mit React DevTools

Wenn Animationen Frames droppen (unter 60fps), hilft das React DevTools Profiler-Panel:

  1. React DevTools öffnen → Profiler Tab
  2. Aufzeichnung starten
  3. Animierte Interaktion ausführen
  4. Aufzeichnung stoppen
  5. Flamegraph nach langen Render-Balken durchsuchen

Häufige Ursachen für Frame-Drops:

  • Re-Renders ganzer Teilbäume bei jeder Animation (fehlende memo-Nutzung)
  • layout-Animationen auf vielen Elementen gleichzeitig
  • useMotionValue in Callbacks, die zu häufig aufgerufen werden

Weiterführende Artikel


Animations-Konzept für Ihr Webprojekt? Wender Media entwickelt performante Microinteractions und Übergänge — barrierefrei und GPU-optimiert. Kontakt: info@wendermedia.info.

Individuelle Beratung gewünscht?

Wender Media unterstützt Sie bei der praktischen Umsetzung — von der technischen Konzeption bis zum Launch. Schreiben Sie uns, wir antworten innerhalb von 24 Stunden.

Jetzt Beratung anfragen

Kostenlos & unverbindlich — info@wendermedia.info

Verwandte Artikel