Core Web Vitals Optimierung: INP, LCP und CLS mit Mustern aus der Praxis
Konkrete Optimierungsmuster für INP, LCP und CLS: Event-Handler-Tuning, responsive Bilder mit priority, Skeleton-Layouts gegen Layout-Shifts und Lighthouse-Profiling.
Rechtliches & Info
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.
Jede CSS-Eigenschaft fällt in eine von drei Kategorien, die unterschiedlich teuer zu animieren sind:
| Kategorie | Eigenschaften | Kosten |
|---|---|---|
| Layout | width, height, top, left, margin, padding | Sehr teuer (Layout + Paint + Composite) |
| Paint | background-color, border, color, box-shadow | Mittel (Paint + Composite) |
| Composite | transform, opacity | Gü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>
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.
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.
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.
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 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.
// 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>
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.
Wenn Animationen Frames droppen (unter 60fps), hilft das React DevTools Profiler-Panel:
Häufige Ursachen für Frame-Drops:
memo-Nutzung)layout-Animationen auf vielen Elementen gleichzeitiguseMotionValue in Callbacks, die zu häufig aufgerufen werdenAnimations-Konzept für Ihr Webprojekt? Wender Media entwickelt performante Microinteractions und Übergänge — barrierefrei und GPU-optimiert. Kontakt: info@wendermedia.info.
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 anfragenKostenlos & unverbindlich — info@wendermedia.info
Konkrete Optimierungsmuster für INP, LCP und CLS: Event-Handler-Tuning, responsive Bilder mit priority, Skeleton-Layouts gegen Layout-Shifts und Lighthouse-Profiling.
CLS systematisch reduzieren: Bilder mit width/height, font-display, Reserved Space für Embeds und Ad-Slots.
So funktioniert LCP-Optimierung — Hero-Bilder, Server, Schriften und Render-Blocking-Resources systematisch verbessern. Mit fetchpriority, Preload und CDN.