5 Performance-Fehler, die Ihre React-App ausbremsen
Die meisten React-Apps sind langsamer als noetig. Hier sind die 5 haeufigsten Performance-Fehler, die ich in Produktions-Codebasen sehe — und wie Sie jeden einzelnen beheben.

Ich habe im Laufe der Jahre Hunderte von React-Codebasen ueberprüft. Kundenprojekte, Open-Source-Repos, interne Tools, Startup-MVPs. Die gleichen Performance-Probleme tauchen immer wieder auf. Keine exotischen Randfaelle — grundlegende, vermeidbare Fehler, die sich aufsummieren, bis sich die App traege anfuehlt und Nutzer abspringen.
Hier sind die fuenf, die ich am haeufigsten sehe, mit echten Fixes, die Sie heute anwenden koennen.
1. Unnoetige Re-Renders durch fehlende Memoization
Das ist das haeufigste Performance-Problem in React-Apps. Eine Elternkomponente rendert sich neu, und jede Kindkomponente rendert mit — auch wenn sich ihre Props nicht geaendert haben. Multiplizieren Sie das ueber einen tiefen Komponentenbaum mit Dutzenden von Eintraegen in einer Liste, und Sie haben eine App, die bei jedem Tastendruck stottert.
Das Problem:
function Dashboard({ user }) {
const [searchQuery, setSearchQuery] = useState("");
const stats = calculateStats(user.transactions);
return (
<div>
<SearchBar value={searchQuery} onChange={setSearchQuery} />
<StatsPanel stats={stats} />
<TransactionList transactions={user.transactions} />
</div>
);
}
Jedes Mal, wenn sich searchQuery aendert, wird calculateStats erneut ausgefuehrt — obwohl sich user.transactions nicht geaendert hat. StatsPanel und TransactionList rendern sich ebenfalls grundlos neu.
Der Fix:
function Dashboard({ user }) {
const [searchQuery, setSearchQuery] = useState("");
const stats = useMemo(
() => calculateStats(user.transactions),
[user.transactions]
);
const handleSearchChange = useCallback(
(value: string) => setSearchQuery(value),
[]
);
return (
<div>
<SearchBar value={searchQuery} onChange={handleSearchChange} />
<MemoizedStatsPanel stats={stats} />
<MemoizedTransactionList transactions={user.transactions} />
</div>
);
}
const MemoizedStatsPanel = memo(StatsPanel);
const MemoizedTransactionList = memo(TransactionList);
Drei Dinge sind hier passiert:
useMemoverhindert, dasscalculateStatsbei jedem Render ausgefuehrt wird. Es berechnet nur neu, wenn sichuser.transactionstatsaechlich aendert.useCallbackstabilisiert die Referenz auf den Change-Handler, sodassSearchBarnicht bei jedem Render eine "neue" Funktion sieht.memoumschliesst die Kindkomponenten, sodass sie das Re-Rendering ueberspringen, wenn ihre Props flach gleich sind.
Ein Wort der Vorsicht: Wickeln Sie nicht blind jede Komponente in memo. Profilieren Sie zuerst mit dem React DevTools Profiler. Memoization hat Kosten — der flache Vergleich selbst. Wenden Sie es dort an, wo es zaehlt: teure Berechnungen, grosse Listen und Komponenten, die sich haeufig mit unveraenderten Props neu rendern.
2. Aufgeblaehte Bundles durch fehlendes Code Splitting
Ich sehe regelmaessig React-Apps, die alles in einem einzigen JavaScript-Bundle ausliefern. Das Admin-Dashboard, die Einstellungsseite, das selten besuchte Hilfe-Center — alles wird beim ersten Seitenbesuch vorab geladen. Der Nutzer wollte eine Landingpage sehen, und sein Browser hat gerade 2 MB JavaScript heruntergeladen, das er noch nicht braucht.
Das Problem:
import AdminDashboard from "./pages/AdminDashboard";
import Settings from "./pages/Settings";
import HelpCenter from "./pages/HelpCenter";
import Landing from "./pages/Landing";
function App() {
return (
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/help" element={<HelpCenter />} />
</Routes>
);
}
Statische Imports bedeuten, dass der Bundler alles in einen Chunk packt. Egal welche Route der Nutzer besucht, er laedt alles herunter.
Der Fix:
import { lazy, Suspense } from "react";
const AdminDashboard = lazy(() => import("./pages/AdminDashboard"));
const Settings = lazy(() => import("./pages/Settings"));
const HelpCenter = lazy(() => import("./pages/HelpCenter"));
import Landing from "./pages/Landing"; // Keep critical path static
function App() {
return (
<Suspense fallback={<PageSkeleton />}>
<Routes>
<Route path="/" element={<Landing />} />
<Route path="/admin" element={<AdminDashboard />} />
<Route path="/settings" element={<Settings />} />
<Route path="/help" element={<HelpCenter />} />
</Routes>
</Suspense>
);
}
React.lazy mit dynamischem import() weist den Bundler an, diese Seiten in separate Chunks aufzuteilen. Sie werden bei Bedarf geladen, wenn der Nutzer zu dieser Route navigiert. Ihr initialer Bundle bleibt klein, und Ihre Time to Interactive sinkt deutlich.
Wenn Sie Next.js verwenden, bekommen Sie routenbasiertes Code Splitting mit dem App Router kostenlos. Aber Sie muessen trotzdem bewusst Component-Level Splitting fuer schwere Drittanbieter-Bibliotheken einsetzen:
import dynamic from "next/dynamic";
const RichTextEditor = dynamic(() => import("@/components/RichTextEditor"), {
loading: () => <EditorSkeleton />,
ssr: false,
});
Das ist entscheidend fuer Bibliotheken wie Rich-Text-Editoren, Charting-Bibliotheken oder PDF-Renderer, die leicht 200-500 KB zu Ihrem Bundle hinzufuegen koennen.
3. State zu hoch im Komponentenbaum platziert
Reacts Rendering-Modell ist Top-down. Wenn sich der State einer Komponente aendert, rendert sie sich neu — und ihr gesamter Unterbaum ebenso. Wenn Sie State "der Bequemlichkeit halber" an die Spitze des Baums heben, sagen Sie React, dass bei jeder State-Aenderung alles darunter neu gerendert werden soll.
Das Problem:
function App() {
const [selectedFilter, setSelectedFilter] = useState("all");
const [isModalOpen, setIsModalOpen] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
return (
<Layout>
<Header />
<Sidebar filter={selectedFilter} onFilterChange={setSelectedFilter} />
<MainContent>
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<ProductList filter={selectedFilter} />
<Modal isOpen={isModalOpen} onClose={() => setIsModalOpen(false)} />
</MainContent>
<Footer />
</Layout>
);
}
Das Tippen in die Suchleiste loest ein State-Update in App aus. Das rendert Header, Sidebar, Footer, ProductList und Modal neu — keines davon interessiert sich fuer die Suchanfrage.
Der Fix:
Druecken Sie State dorthin, wo er tatsaechlich verwendet wird:
function App() {
return (
<Layout>
<Header />
<SidebarWithFilter />
<MainContent>
<SearchSection />
<ProductList />
</MainContent>
<Footer />
</Layout>
);
}
function SearchSection() {
const [searchQuery, setSearchQuery] = useState("");
return (
<div>
<SearchBar query={searchQuery} onChange={setSearchQuery} />
<SearchResults query={searchQuery} />
</div>
);
}
Jetzt rendert das Tippen in der Suchleiste nur SearchSection und ihre Kinder neu. Alles andere bleibt unberuehrt.
Das Prinzip ist einfach: State sollte so nah wie moeglich an den Komponenten leben, die ihn lesen. Wenn zwei Geschwisterkomponenten den gleichen State brauchen, heben Sie ihn zum naechsten gemeinsamen Elternteil — aber nicht hoeher. Fuer wirklich globalen State (Auth, Theme, Locale) verwenden Sie eine State-Management-Bibliothek, die selektive Subscriptions unterstuetzt, wie Zustand oder Jotai, sodass nur die Komponenten neu rendern, die einen bestimmten State-Slice konsumieren.
4. Unoptimierte Bilder in Next.js
Bilder sind typischerweise die schwersten Assets auf einer Webseite. Ich habe Next.js-Apps gesehen, in denen Entwickler einfache <img>-Tags mit riesigen, unkomprimierten PNGs verwenden. Das Ergebnis: eine Seite, die 8 MB an Bildern laedt, kein Lazy Loading, keine responsive Groessenanpassung und eine Largest Contentful Paint-Zeit, bei der Lighthouse weint.
Das Problem:
function ProjectCard({ project }) {
return (
<div className="card">
<img src={`/images/${project.cover}`} alt={project.title} />
<h3>{project.title}</h3>
<p>{project.description}</p>
</div>
);
}
Das liefert das Bild in voller Aufloesung in Originalgroesse aus, unabhaengig vom Viewport oder Geraet des Nutzers. Keine Formatkonvertierung, kein Lazy Loading, kein responsives Srcset.
Der Fix:
import Image from "next/image";
function ProjectCard({ project }) {
return (
<div className="card">
<Image
src={`/images/${project.cover}`}
alt={project.title}
width={600}
height={400}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
placeholder="blur"
blurDataURL={project.blurHash}
/>
<h3>{project.title}</h3>
<p>{project.description}</p>
</div>
);
}
Die Next.js Image-Komponente gibt Ihnen automatisch mehrere Dinge:
- Automatische Formatkonvertierung zu WebP oder AVIF basierend auf Browser-Unterstuetzung
- Responsive Groessenanpassung ueber das
sizes-Attribut — der Browser laedt nur die benoetigte Groesse herunter - Lazy Loading standardmaessig — Bilder unterhalb des sichtbaren Bereichs werden erst geladen, wenn der Nutzer in ihre Naehe scrollt
- Blur-Platzhalter — zeigt eine leichtgewichtige Vorschau, waehrend das vollstaendige Bild laedt, und eliminiert Layout-Verschiebungen
Fuer Bilder im sichtbaren Bereich (Hero-Banner, primaerer Inhalt) fuegen Sie priority hinzu, um Lazy Loading zu deaktivieren und sie vorzuladen:
<Image src={heroImage} alt="Hero" priority sizes="100vw" />
Diese eine Aenderung allein kann das Seitengewicht um 60-80 % reduzieren und Ihre Core Web Vitals-Werte dramatisch verbessern.
5. Blockierung des Main Thread mit schweren Berechnungen
Der Main Thread des Browsers uebernimmt Rendering, Benutzereingaben, Animationen und JavaScript-Ausfuehrung — alles auf einem einzigen Thread. Wenn Sie eine schwere Berechnung synchron ausfuehren, friert alles andere ein. Der Nutzer klickt einen Button und nichts passiert fuer 500ms. Animationen ruckeln. Scrollen reagiert nicht mehr.
Das Problem:
function AnalyticsDashboard({ rawData }) {
const [processedData, setProcessedData] = useState(null);
useEffect(() => {
// This blocks the main thread for 200-800ms
const result = processLargeDataset(rawData); // Sorting, filtering, aggregating 50k+ rows
setProcessedData(result);
}, [rawData]);
if (!processedData) return <Spinner />;
return <Charts data={processedData} />;
}
processLargeDataset laeuft synchron auf dem Main Thread. Wenn es 400ms dauert, ist die gesamte UI fuer 400ms eingefroren. Der Nutzer kann nicht scrollen, tippen oder mit irgendetwas interagieren.
Der Fix — verschieben Sie es in einen Web Worker:
// workers/dataProcessor.ts
self.onmessage = (event: MessageEvent) => {
const rawData = event.data;
const result = processLargeDataset(rawData);
self.postMessage(result);
};
function AnalyticsDashboard({ rawData }) {
const [processedData, setProcessedData] = useState(null);
const workerRef = useRef<Worker | null>(null);
useEffect(() => {
workerRef.current = new Worker(
new URL("../workers/dataProcessor.ts", import.meta.url)
);
workerRef.current.onmessage = (event: MessageEvent) => {
setProcessedData(event.data);
};
workerRef.current.postMessage(rawData);
return () => {
workerRef.current?.terminate();
};
}, [rawData]);
if (!processedData) return <Spinner />;
return <Charts data={processedData} />;
}
Die schwere Berechnung laeuft jetzt auf einem separaten Thread. Der Main Thread bleibt frei fuer Benutzerinteraktionen, Animationen und Rendering. Die UI bleibt die ganze Zeit responsiv.
Fuer weniger extreme Faelle, bei denen die Berechnung 50-150ms dauert, koennen Sie die Arbeit in kleinere Stuecke aufteilen, indem Sie requestIdleCallback oder scheduler.yield() verwenden, anstatt einen vollstaendigen Worker hochzufahren. Aber fuer alles, was grosse Datensaetze verarbeitet, Berichte generiert oder komplexe Algorithmen ausfuehrt, sind Web Workers nicht optional — sie sind notwendig.
Der Kompoundeffekt
Keiner dieser Fehler wird Ihre App allein zum Absturz bringen. Das macht sie so gefaehrlich. Jeder fuegt 100-300ms Verzoegerung hinzu, und Nutzer erleben die Summe. Eine Komponente, die sich unnoetig neu rendert, plus ein aufgeblaehtes Bundle, plus unoptimierte Bilder, plus eine Main-Thread-Berechnung bedeuten, dass Ihre App 4 Sekunden braucht, um interaktiv zu werden, statt 1.
Profilieren Sie vor der Optimierung. Verwenden Sie den React DevTools Profiler, um Re-Render-Hotspots zu finden. Verwenden Sie Lighthouse und Web Vitals, um die reale Nutzerauswirkung zu messen. Verwenden Sie den Network-Tab Ihres Browsers, um uebergrosse Bundles und Bilder aufzuspueren. Verwenden Sie den Performance-Tab, um lange Tasks zu identifizieren, die den Main Thread blockieren.
Das sind Muster, die ich in jedem Projekt anwende — von Restaurantplattformen bis zu medizinischen SaaS-Systemen. Performance ist kein Feature, das man spaeter hinzufuegt. Es ist eine Qualitaet, die man vom ersten Commit an pflegt.
Verwandte Projekte
RestoHub
Restaurants verlieren keine 30 % mehr an Uber Eats — sie bekommen ihr eigenes Bestell-, Menü-, Website- und Treueprogramm-System auf einer Plattform. Vollwertiges Uber-Eats-Erlebnis, aber das Restaurant behält jeden Cent.
Poulet Rouge
Den kompletten digitalen Auftritt für Poulet Rouge aufgebaut — eine Restaurantkette mit über 100 Standorten in Quebec. Interaktiver Filialfinder, Online-Bestell-Widgets, Nährwertrechner und zweisprachige SEO, die bei Google tatsächlich rankt.
Danil Ulmashev
Full Stack Developer
Interesse an einer Zusammenarbeit?