Was macht React Performance?
Optimierungsmuster für React 18/19 und Next.js, abgeleitet von Vercel Labs react-best-practices (MIT, v1.0.0). Diese Skill organisiert Regeln nach Priorität und bietet Entscheidungsbäume für aktive Code-Reviews und Refaktorisierungen.
Wann aktivieren
- Beim Schreiben oder Überprüfen von React/Next.js-Code auf Performance
- Bei der Diagnose von langsamen Seitenladezeiten, trägen Interaktionen oder hoher CPU-Auslastung auf dem Client
- Bei der Überprüfung der Bundle-Größe oder von Regressionen bei Lighthouse Core Web Vitals
- Beim Entfernen von Waterfalls in Server Components / API-Routen
- Beim Reduzieren clientseitiger Neu-Renderings
- Bei der Optimierung von langen Listen, Animationen oder Hydratation
- Bei der Überprüfung von Optimierungsentscheidungen in PRs, die
app/,pages/,components/oder Datenabfrageschichten betreffen
Prioritätsindex
| Priorität | Kategorie | Präfix | Wann es wichtig ist |
|---|---|---|---|
| 1 — KRITISCH | Waterfalls beseitigen | async- | Immer wenn await auf unabhängiges await folgt |
| 2 — KRITISCH | Bundle-Größenoptimierung | bundle- | Erstladungs-JS, Imports auf Routenebene, Drittanbieter-Bibliotheken |
| 3 — HOCH | Serverseitige Performance | server- | RSC, Server Actions, API-Routen, SSR |
| 4 — MITTEL-HOCH | Clientseitiges Datenabrufen | client- | SWR / TanStack Query / rohes fetch in Hooks |
| 5 — MITTEL | Neu-Renderungsoptimierung | rerender- | Häufige Statusaktualisierungen, Eltern-Kind-Fan-Out |
| 6 — MITTEL | Rendering Performance | rendering- | Lange Listen, Animationen, Hydratation |
| 7 — NIEDRIG-MITTEL | JavaScript Performance | js- | Heiße Schleifen, häufige Allokationen |
| 8 — NIEDRIG | Fortgeschrittene Muster | advanced- | Effect-Event-Integration, stabile Refs |
1. Waterfalls beseitigen (KRITISCH)
„Waterfalls sind der Performance-Killer Nummer eins" — jedes sequenzielle
awaitfügt vollständige Netzwerklatenz hinzu.
Günstige Bedingungen vor await
Überprüfe synchrone Bedingungen (Props, Umgebung, hartcodierte Flags), bevor entfernte Daten abgerufen werden.
// FALSCH
async function Page({ id }: { id: string }) {
const flag = await getFlag("show-page");
if (!flag || !id) return null;
const data = await getData(id);
// ...
}
// RICHTIG — zuerst mit günstiger synchroner Bedingung kurzschließen
async function Page({ id }: { id: string }) {
if (!id) return null;
const flag = await getFlag("show-page");
if (!flag) return null;
const data = await getData(id);
}
await bis zur Verwendung verschieben
Verschiebe await in den Zweig, der es verwendet.
// FALSCH — await vor der Entscheidung, ob die Daten benötigt werden
const user = await getUser(id);
if (mode === "guest") return renderGuest();
return renderUser(user);
// RICHTIG
if (mode === "guest") return renderGuest();
const user = await getUser(id);
return renderUser(user);
Promise.all für unabhängige Arbeiten
// FALSCH — sequenziell
const user = await getUser(id);
const posts = await getPosts(id);
const followers = await getFollowers(id);
// RICHTIG — parallel
const [user, posts, followers] = await Promise.all([
getUser(id),
getPosts(id),
getFollowers(id),
]);
Partielle Abhängigkeiten — früh starten, spät awaiten
// RICHTIG — alle Promises starten, nur awaiten, wenn jedes Ergebnis benötigt wird
const userP = getUser(id);
const postsP = getPosts(id);
const profile = await getProfile(id);
if (profile.private) return null;
const [user, posts] = await Promise.all([userP, postsP]);
Suspense für Streaming
Setze <Suspense>-Grenzen nah an den Daten, sodass die Seite das zeichnen kann, was möglich ist, während langsamere Unterbäume nachgeladen werden. Der Kompromiss: Layout-Shift, wenn Inhalte eintreffen — reserviere Platz (Skeleton oder min-height).
Server Components: Parallelisierung durch Komposition
// FALSCH — Geschwister-await laufen sequenziell innerhalb einer Komponente
export default async function Page() {
const user = await getUser();
const cart = await getCart();
return <View user={user} cart={cart} />;
}
// RICHTIG — in Kinder aufteilen, React führt sie parallel aus
export default async function Page() {
return (
<View>
<UserSection />
<CartSection />
</View>
);
}
2. Bundle-Größenoptimierung (KRITISCH)
Direkte Imports, keine Fassadendateien (Barrel)
Fassaden-index.ts-Dateien zwingen den Bundler, den gesamten Modul-Graphen zu durchlaufen, selbst wenn Tree-Shaking den Großteil entfernt. Direkte Imports sparen in vielen realen Anwendungen 200-800ms Erstladungs-JS.
// FALSCH
import { Button, Card, Modal } from "@/components";
// RICHTIG
import { Button } from "@/components/Button";
import { Card } from "@/components/Card";
import { Modal } from "@/components/Modal";
Next.js 13.5+ bietet Optimize Package Imports, das dies für aufgeführte Pakete automatisiert — nutze es; manuelle direkte Imports sind dennoch für nicht aufgeführte Bibliotheken erforderlich.
Statisch analysierbare Pfade
// FALSCH — macht Bundler/Analyse zunichte
const mod = await import(`./pages/${name}`);
// RICHTIG — explizit pro Zweig
const mod = name === "home" ? await import("./pages/home") : await import("./pages/about");
Dynamische Imports für schwere Komponenten
import dynamic from "next/dynamic";
const HeavyChart = dynamic(() => import("./HeavyChart"), {
loading: () => <Skeleton />,
ssr: false, // wenn nur clientseitig
});
Drittanbieter-Skripte verschieben
Lade Analytik, Logging, Support-Widgets NACH der Hydratation. Verwende next/script mit strategy="afterInteractive" (Standard) oder "lazyOnload".
Bedingtes Modulladen
if (user.role === "admin") {
const { AdminPanel } = await import("./admin/AdminPanel");
// ...
}
Vorausladen bei Hover/Fokus
Löse <link rel="preload"> oder import() bei Hover aus, sodass das Bundle im Cache ist, wenn der Benutzer klickt.
3. Serverseitige Performance (HOCH)
Server Actions wie API-Routen authentifizieren
Jede "use server"-Funktion ist ein öffentlicher Endpunkt. Authentifiziere UND autorisiere innerhalb der Aktion — verlasse dich niemals auf die Absicherung der aufrufenden Client-Komponente.
"use server";
export async function deleteUser(formData: FormData) {
const session = await getSession();
if (!session?.user) throw new Error("Unauthorized");
const targetId = String(formData.get("id"));
if (session.user.role !== "admin" && session.user.id !== targetId) {
throw new Error("Forbidden");
}
await db.user.delete({ where: { id: targetId } });
}
React.cache() für anforderungsbezogene Deduplizierung
import { cache } from "react";
export const getUser = cache(async (id: string) => {
return db.user.findUnique({ where: { id } });
});
React.cache dedupliziert innerhalb einer einzelnen Anforderung. Drei Aufrufe von getUser("1") aus verschiedenen Server Components im selben Render = eine DB-Abfrage.
LRU-Cache für anforderungsübergreifende Daten
Für Daten, die sich NICHT pro Anforderung ändern (Konfiguration, Nachschlagetabellen), außerhalb von React mit einem LRU-Cache oder unstable_cache cachen.
Doppelte Serialisierung in RSC-Props vermeiden
Wenn eine Server Component dieselben Daten in mehrere Client Components rendert, werden die Daten einmal pro Verbraucher serialisiert. Hebe die Client Component an und übergib Kinder.
Statische I/O auf Modulebene anheben
// RICHTIG — wird einmal beim Modulladen ausgeführt
const fontData = readFileSync(fontPath);
export async function Page() {
return <Banner font={fontData} />;
}
Kein veränderlicher Modulstatus auf Modulebene in RSC/SSR
Modulstatus auf dem Server wird von allen Anforderungen gemeinsam genutzt — eine Race Condition zwischen Benutzern. Verwende stattdessen anforderungsspezifischen Speicher (headers(), cookies(), asynchroner Kontext).
An Client Components übergebene Daten minimieren
Serialisiere nur das, was der Client benötigt. Entferne Felder, paginiere, projiziere Spalten auf der DB-Ebene.
Verschachtelte Abrufe mit Promise.all pro Element parallelisieren
const users = await getUsers();
const enriched = await Promise.all(
users.map(async (u) => ({ ...u, posts: await getPostsFor(u.id) })),
);
after() für nicht blockierende Arbeiten verwenden
Next.js 15 after() führt Arbeiten aus, nachdem die Antwort gesendet wurde — Logging, Cache-Warming, Analytik.
import { after } from "next/server";
export async function GET() {
const data = await getData();
after(() => logAnalytics(data));
return Response.json(data);
}
4. Clientseitiges Datenabrufen (MITTEL-HOCH)
SWR / TanStack Query für Deduplizierung
Mehrere Komponenten, die useUser(id) aufrufen, sollten eine einzige Netzwerkanforderung und einen Cache-Eintrag gemeinsam nutzen. Verwende SWR oder TanStack Query — baue niemals dein eigenes useEffect + fetch für gemeinsam genutzte Daten.
Globale Ereignis-Listener deduplizieren
// FALSCH — jede Komponente fügt ihren eigenen hinzu
useEffect(() => {
window.addEventListener("scroll", handler);
return () => window.removeEventListener("scroll", handler);
}, []);
// RICHTIG — einzelner gemeinsam genutzter Listener über einen Hook + globales Subject
const useScroll = createScrollHook(); // Singleton-Subject unter der Haube
Passive Listener für Scroll
window.addEventListener("scroll", handler, { passive: true });
Verbessert die Scroll-Glätte; der Listener kann kein preventDefault() aufrufen.
localStorage: Version + minimieren
- Speichere immer ein
version-Feld; erhöhe bei Schemaänderung und migriere oder verwerfe alte Daten - Halte Payloads klein —
localStorageist synchron und blockiert den Hauptthread
5. Neu-Renderungsoptimierung (MITTEL)
Kein Abonnieren von Status, der nur in Callbacks verwendet wird
// FALSCH — rendert jedes Mal neu, wenn count sich ändert
const count = useStore((s) => s.count);
const handler = () => doSomething(count);
// RICHTIG — einmal beim Aufruf lesen
const handler = () => {
const count = useStore.getState().count;
doSomething(count);
};
Teure Arbeit in memoized Komponenten auslagern
// RICHTIG — Kind rendert nur neu, wenn `items` sich ändert
const Heavy = memo(function Heavy({ items }: { items: Item[] }) {
return <Chart data={transform(items)} />;
});
Standardmäßig nicht primitive Props anheben
// FALSCH — neues Array pro Render bricht memo
<List items={items ?? []} />
// RICHTIG
const EMPTY: Item[] = [];
<List items={items ?? EMPTY} />
Primitive Abhängigkeiten in Effekten
// FALSCH — neue Objekt-Identität pro Render
useEffect(() => {}, [{ id, name }]);
// RICHTIG — Primitive
useEffect(() => {}, [id, name]);
Abgeleitete Booleans abonnieren, nicht Rohwerte
// FALSCH — rendert bei jeder Warenkorbänderung neu
const cart = useStore((s) => s.cart);
const hasItems = cart.length > 0;
// RICHTIG — rendert nur neu, wenn der Leerzustand umkippt
const hasItems = useStore((s) => s.cart.length > 0);
Während des Render ableiten, niemals via useEffect
// FALSCH
const [full, setFull] = useState("");
useEffect(() => setFull(`${first} ${last}`), [first, last]);
// RICHTIG
const full = `${first} ${last}`;
Funktionelles setState für stabile Callbacks
// RICHTIG
const increment = useCallback(() => setCount((c) => c + 1), []);
Lazy State Initializer für teure Werte
const [tree] = useState(() => parseTree(largeInput));
Memo für einfache Primitive vermeiden
useMemo(() => x + 1, [x]) ist Overhead. Memo lohnt sich bei Objekt-Identität und teuren Berechnungen.
Hooks mit unabhängigen Abhängigkeiten aufteilen
// FALSCH — beide Selektoren laufen erneut, wenn sich eine Quelle ändert
const { a, b } = useSomething(source1, source2);
// RICHTIG
const a = useA(source1);
const b = useB(source2);
Interaktionslogik in Event-Handler verschieben
Event-Handler werden nur bei der Benutzeraktion ausgeführt — useEffect läuft erneut, wenn sich Abhängigkeiten ändern.
startTransition für nicht dringende Aktualisierungen
const [pending, startTransition] = useTransition();
startTransition(() => setFilters(newFilters));
useDeferredValue für teure Renderings
const deferredQuery = useDeferredValue(query);
const results = useMemo(() => expensiveSearch(deferredQuery), [deferredQuery]);
useRef für flüchtige, häufige Werte
Für Werte, die sich oft ändern, aber kein Neu-Rendering auslösen sollen (Zeitstempel, letzte Taste, Akkumulatoren).
Keine Komponenten innerhalb von Komponenten definieren
// FALSCH — Inner ist bei jedem Outer-Render eine neue Komponente
function Outer() {
const Inner = () => <span />;
return <Inner />;
}
Jeder Render erzeugt einen neuen Inner-Typen, was die Reconciliation verhindert und Kinder aushängt.
6. Rendering Performance (MITTEL)
Das Wrapper-Element animieren, nicht das SVG
Die Transformation eines <div>-Wrappers um ein SVG ist GPU-beschleunigt; die Transformation des SVGs selbst löst Paint aus.
content-visibility: auto für lange Listen
.row { content-visibility: auto; contain-intrinsic-size: auto 80px; }
Der Browser überspringt das Rendering außerhalb des Bildschirms — großer Gewinn für Listen mit Hunderten von Zeilen.
Statisches JSX anheben
const STATIC_HEADER = <h1>Title</h1>;
function Page() {
return <>{STATIC_HEADER}<Body /></>;
}
SVG: Koordinaten-Präzision reduzieren
d="M10.123456,20.654321" → d="M10.12,20.65". Jede Ziffer kostet Bytes; der visuelle Unterschied ist subpixelgenau.
Kein Hydration-Flimmern via Inline-Script
Für Werte, die vor der Hydration benötigt werden (Theme, Locale), ein <script> inline einfügen, das document.documentElement.dataset.* setzt, bevor React mountet.
Erwartete Hydration-Mismatches eng begrenzt unterdrücken
<time suppressHydrationWarning>{new Date().toLocaleString()}</time>
Nur für bekannte, divergierende Blattknoten verwenden — niemals für einen Baum, der andere Kinder enthält.
<Activity> für Ein-/Ausblenden statt Mount/Unmount
React 19 <Activity mode="visible|hidden"> hält den Baumstatus und Effekte gemountet, blendet aber aus — günstiger als Unmount/Remount für Tabs und Akkordeons.
Ternärer Operator statt && für bedingtes Rendern
// FALSCH — `0` rendert als Textknoten
{count && <Badge>{count}</Badge>}
// RICHTIG
{count > 0 ? <Badge>{count}</Badge> : null}
useTransition für Ladezustände
Paare startTransition mit der Aktion; React zeigt die vorherige UI als isPending, während der nächste Zustand berechnet wird.
React DOM Ressourcen-Hinweise
import { preload, preconnect } from "react-dom";
preload("/api/critical", { as: "fetch" });
preconnect("https://api.example.com")
…
(gekürzt für die Übersetzung)