Web-Animationen: Weniger ist mehr

Warum die meisten Web-Animationen mehr schaden als nützen — und wie man die wenigen, die wirklich helfen, korrekt implementiert.

CSSPerformanceUXAnimation

Warum Animationen oft mehr schaden als nützen

Animationen sind verführerisch. Sie machen eine Site lebendig, zeigen Aufwand und können — in der richtigen Dosis — die User Experience wirklich verbessern. Das Problem ist die Dosis.

Die häufigsten Fehler:

Animationen als Ablenkung. Wenn jedes Element beim Scrollen einfliegt, jeder Button beim Hover pulsiert und jede Transition 400ms dauert, kämpft die Animation mit dem Inhalt. Die Nutzer sehen die Animation, nicht das Produkt.

Animationen ohne Bedeutung. Eine gute Animation vermittelt etwas: Kausalität (dieser Button hat etwas ausgelöst), Hierarchie (dieses Modal kommt von diesem Element), oder Zustand (diese Komponente lädt). Eine Animation, die nur "schön aussieht", leistet keinen UX-Beitrag.

Animationen auf Performance-Kosten. Das ist der technische Schaden: Animationen, die Layout-Berechnungen oder Paint-Operationen triggern, fressen Hauptthread-Zeit und verursachen Jank — sichtbares Ruckeln, das sich anfühlt, als ob die Site kaputt wäre.

Der erste Schritt ist die Frage: Was kommuniziert diese Animation? Wenn die Antwort "nichts" ist, streichen.

Der Browser-Rendering-Pipeline: Layout, Paint, Composite

Um zu verstehen, welche Animationen teuer sind, muss man die Rendering-Pipeline verstehen. Der Browser durchläuft bei jedem Frame bis zu drei Phasen:

Layout (Reflow) — Der Browser berechnet Position und Größe jedes Elements. Teuer, weil eine Änderung an einem Element alle abhängigen Elemente neu berechnen kann. Ausgelöst durch Änderungen an width, height, margin, padding, top, left, font-size, und viele mehr.

Paint — Der Browser zeichnet Pixel für Elemente. Weniger teuer als Layout, aber nicht kostenlos. Ausgelöst durch Änderungen an color, background-color, border, box-shadow, text-decoration und andere visuelle Properties.

Composite — Der Browser kombiniert bereits gerenderte Ebenen. Das ist die einzige Phase, die auf der GPU läuft und den Hauptthread nicht blockiert. Nur transform und opacity lösen ausschließlich Composite aus.

Layout → Paint → Composite  (teuer: alles neu)
         Paint → Composite  (mittel: neu malen)
                 Composite  (günstig: nur verschieben)

Für ruckelfreie 60fps-Animationen muss man in der Composite-Phase bleiben.

Die goldene Regel: nur transform und opacity animieren

Das ist die wichtigste praktische Konsequenz aus der Rendering-Pipeline:

/* FALSCH: löst Layout aus */
.element {
  animation: slide-in 300ms ease;
}
@keyframes slide-in {
  from { left: -100px; }
  to   { left: 0; }
}

/* RICHTIG: nur Composite */
.element {
  animation: slide-in 300ms ease;
}
@keyframes slide-in {
  from { transform: translateX(-100px); }
  to   { transform: translateX(0); }
}

Beide Animationen sehen identisch aus. Aber die erste berechnet bei jedem Frame das Layout neu. Die zweite verschiebt eine bereits gerenderte Ebene auf der GPU.

Dasselbe gilt für Einblend-Animationen:

/* FALSCH: löst Paint aus */
@keyframes fade-in {
  from { visibility: hidden; }
  to   { visibility: visible; }
}

/* RICHTIG: nur Composite */
@keyframes fade-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

Und für Größenänderungen:

/* FALSCH: löst Layout aus */
@keyframes grow {
  from { width: 0; height: 0; }
  to   { width: 200px; height: 200px; }
}

/* RICHTIG: scale ist Composite */
@keyframes grow {
  from { transform: scale(0); }
  to   { transform: scale(1); }
}

Die Regel hat Ausnahmen: manchmal kann man width/height nicht vermeiden. Dann sollte man das animierte Element in seinen eigenen Compositor-Layer befördern (mehr dazu bei will-change).

will-change — wann es hilft und wann es schadet

will-change ist ein Hinweis an den Browser, dass sich eine Property bald ändern wird:

.animated-element {
  will-change: transform;
}

Was das bewirkt: Der Browser erstellt im Voraus einen eigenen Compositor-Layer für das Element. Wenn die Animation beginnt, ist der Layer bereits bereit — kein Setup-Overhead.

Wann will-change sinnvoll ist:

  • Komplexe Animationen mit vielen Elementen
  • Animationen, die durch User-Interaktion ausgelöst werden (Hover, Scroll)
  • Elemente, die sich häufig und regelmäßig bewegen

Wann will-change schadet:

  • Auf zu vielen Elementen gleichzeitig — jeder Layer kostet GPU-Speicher
  • Dauerhaft auf statischen Elementen — der Layer wird erzeugt und nie genutzt
  • Als Lösung für Layout-Thrashing — das ist eine falsche Optimierung
/* FALSCH: auf allen Elementen */
* { will-change: transform; }

/* RICHTIG: gezielt und temporär */
.card:hover { will-change: transform; }
.card { transition: transform 200ms ease; }

Noch besser: will-change via JavaScript dynamisch hinzufügen und entfernen:

const card = document.querySelector('.card')
card.addEventListener('mouseenter', () => {
  card.style.willChange = 'transform'
})
card.addEventListener('mouseleave', () => {
  card.style.willChange = 'auto'
})

So wird der Layer nur während der Interaktion erstellt und danach sofort freigegeben.

prefers-reduced-motion: kein Nice-to-have, sondern Pflicht

prefers-reduced-motion ist eine CSS Media Query, die meldet, ob der Nutzer in seinem Betriebssystem "Reduce Motion" aktiviert hat. Das ist keine Minderheit: Schätzungen zufolge nutzen 10–25% der Menschen mit vestibulären oder neurologischen Erkrankungen diese Einstellung.

/* Basis: Animation für alle */
.element {
  transition: transform 300ms ease;
}

/* Reduced: keine Animation für betroffene User */
@media (prefers-reduced-motion: reduce) {
  .element {
    transition: none;
  }
}

Oder der bessere Ansatz — opt-in statt opt-out:

/* Standard: keine Animation */
.element {
  /* statisch */
}

/* Nur animieren, wenn Animationen OK sind */
@media (prefers-reduced-motion: no-preference) {
  .element {
    transition: transform 300ms ease;
  }
}

In Vue/Nuxt:

<script setup>
const prefersReducedMotion = useMediaQuery('(prefers-reduced-motion: reduce)')
</script>

<template>
  <Transition :name="prefersReducedMotion ? '' : 'fade'">
    <div v-if="visible">Inhalt</div>
  </Transition>
</template>

Animationen zu deaktivieren heißt nicht, die Seite schlechter zu machen. Es heißt, sie für alle nutzbar zu machen.

Scroll-Animationen: IntersectionObserver statt scroll-Events

Ein häufiger Pattern: Elemente beim Scrollen einblenden. Die naive Implementierung hört auf scroll-Events:

// FALSCH: scroll-Event ist teuer
window.addEventListener('scroll', () => {
  elements.forEach(el => {
    if (el.getBoundingClientRect().top < window.innerHeight) {
      el.classList.add('visible')
    }
  })
})

getBoundingClientRect() zwingt den Browser zu einem synchronen Layout-Reflow bei jedem Scroll-Event. Auf mobilen Geräten bedeutet das sichtbares Ruckeln.

IntersectionObserver ist die korrekte Lösung:

const observer = new IntersectionObserver(
  (entries) => {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        entry.target.classList.add('visible')
        observer.unobserve(entry.target) // Nur einmal
      }
    })
  },
  { threshold: 0.1 } // 10% des Elements sichtbar
)

document.querySelectorAll('.animate-on-scroll').forEach(el => {
  observer.observe(el)
})

IntersectionObserver läuft off the main thread und benachrichtigt nur, wenn sich der Sichtbarkeitsstatus ändert — kein Polling, kein Layout-Thrashing.

CSS @keyframes für die eigentliche Animation:

.animate-on-scroll {
  opacity: 0;
  transform: translateY(20px);
}

.animate-on-scroll.visible {
  animation: reveal 400ms ease forwards;
}

@keyframes reveal {
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

@media (prefers-reduced-motion: reduce) {
  .animate-on-scroll.visible {
    animation: none;
    opacity: 1;
    transform: none;
  }
}

Konkrete Checkliste: Wann eine Animation gerechtfertigt ist

Vor jeder Animation diese Fragen:

  1. Kommuniziert sie etwas? Zustandswechsel, Kausalität, Orientierung, Feedback — oder nur Dekoration?
  2. Hält sie die 60fps-Regel? Nur transform und opacity, oder ist Layout/Paint dabei?
  3. Ist die Dauer angemessen? Micro-Interactions: 100–200ms. State-Transitions: 200–300ms. Page-Transitions: maximal 400ms. Alles über 500ms fühlt sich langsam an.
  4. Ist sie unterbrechbar? User sollten nicht auf das Ende einer Animation warten müssen, um interagieren zu können.
  5. Respektiert sie prefers-reduced-motion? Wenn nicht, ist sie nicht fertig.
  6. Läuft sie auf einem Low-End-Gerät? Das echte Testgerät ist nicht das MacBook Pro.

Wenn eine Animation alle sechs Punkte besteht, ist sie gerechtfertigt. Wenn nicht, ist Vereinfachung oder Streichung die bessere Entscheidung.

Motion-Bibliotheken (Motion for Vue / GSAP) — mit Bedacht

Bibliotheken wie Motion for Vue (früher Framer Motion für Vue) oder GSAP können Animationen vereinfachen — aber sie ändern nichts an den Grundprinzipien.

Eine schlechte Animation mit GSAP ist immer noch eine schlechte Animation. Eine Layout-trashende Animation mit Motion for Vue ist immer noch teuer.

Was diese Bibliotheken wirklich bieten:

  • Declarative Syntax<Motion :animate="{ x: 100 }" /> statt @keyframes
  • Spring-Physics — natürlichere, physikalische Bewegungen statt linearer Kurven
  • Gesture-Integration — Drag, Hover, Tap als First-Class-Features
  • Orchestrierungstagger, sequence, AnimatePresence für komplexe Choreographien
<script setup>
import { Motion } from 'motion/vue'
</script>

<template>
  <!-- Nur transform + opacity → GPU-beschleunigt -->
  <Motion
    :initial="{ opacity: 0, y: 20 }"
    :animate="{ opacity: 1, y: 0 }"
    :transition="{ duration: 0.3, ease: 'easeOut' }"
  >
    <div>Inhalt</div>
  </Motion>
</template>

Der Einsatz ist dann gerechtfertigt, wenn die Animationskomplexität die Kosten der Dependency rechtfertigt. Für einfache Transitions und Hover-Effekte ist vanilla CSS die bessere, leichtere Wahl. Für komplexe, physikalisch anmutende Interaktionen und Choreographien lohnt sich eine Bibliothek.

GSAP ist die Wahl für die komplexesten Animationen — ScrollTrigger, SVG-Morphing, Timeline-Orchestrierung. Aber GSAP ist auch 70KB+ und sollte nur geladen werden, wenn man diese Komplexität wirklich braucht.

Die Faustregel: CSS für alles Einfache. Motion for Vue für komplexere interaktive Animationen. GSAP für das Extrem-Ende.