Performance ist entscheidend für den Erfolg jeder React-Anwendung. Langsame Apps führen zu schlechter Nutzererfahrung, niedrigeren Conversion-Raten und schlechteren SEO-Rankings. In diesem Guide zeigen wir bewährte Strategien zur Optimierung Ihrer React-Apps.
Inhaltsverzeichnis
- 1. Performance Monitoring: Was messen wir?
- 1.1 Core Web Vitals
- 1.2 React DevTools Profiler
- 2. React.memo und Memoization
- 2.1 React.memo für Component Memoization
- 2.2 useMemo für teure Berechnungen
- 3. useCallback für Event Handlers
- 3.1 Problem: Neue Funktionen bei jedem Render
- 3.2 Lösung: useCallback verwenden
- 4. Code Splitting und Lazy Loading
- 4.1 Route-based Code Splitting
- 4.2 Component-based Code Splitting
- 4.3 Preloading Strategien
- 5. Virtualisierung für große Datenmengen
- 5.1 React Window für lange Listen
- 5.2 Variable Größen mit VariableSizeList
- 6. Image Optimization
- 6.1 Next.js Image Component
- 6.2 Progressive Image Loading
- 7. Bundle Optimization
- 7.1 Webpack Bundle Analyzer
- 7.2 Tree Shaking optimieren
- 7.3 Dynamic Imports für bedingte Features
- 8. State Management Optimierung
- 8.1 Context API Performance
- 9. Performance Monitoring in Production
- 9.1 Error Boundaries für Performance
- 9.2 Custom Performance Hooks
- 10. Fazit und Checklist
- 10.1 Performance Optimization Checklist
Performance Monitoring: Was messen wir?
Core Web Vitals
Google's Core Web Vitals sind essentiell für SEO und Nutzererfahrung:
-
Largest Contentful Paint (LCP) - Ladezeit des größten Inhalts-Elements
-
First Input Delay (FID) - Zeit bis zur ersten Interaktion
-
Cumulative Layout Shift (CLS) - Visuelle Stabilität
// Web Vitals Monitoring implementieren
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'
function sendToAnalytics({ name, value, id }: Metric) {
// Senden Sie die Daten an Ihr Analytics-System
gtag('event', name, {
event_category: 'Web Vitals',
event_label: id,
value: Math.round(name === 'CLS' ? value * 1000 : value),
non_interaction: true,
})
}
// Metriken erfassen
getCLS(sendToAnalytics)
getFID(sendToAnalytics)
getFCP(sendToAnalytics)
getLCP(sendToAnalytics)
getTTFB(sendToAnalytics)
React DevTools Profiler
import { Profiler } from 'react'
function onRenderCallback(
id: string,
phase: 'mount' | 'update',
actualDuration: number,
baseDuration: number,
startTime: number,
commitTime: number
) {
console.log('Component:', id)
console.log('Phase:', phase)
console.log('Actual duration:', actualDuration)
console.log('Base duration:', baseDuration)
}
function App() {
return (
<Profiler
id="App"
onRender={onRenderCallback}
>
<Header />
<Main />
<Footer />
</Profiler>
)
}
React.memo und Memoization
React.memo für Component Memoization
// Ohne Memoization - Component wird bei jedem Parent-Render neu gerendert
const ExpensiveComponent = ({ data, onUpdate }) => {
console.log('ExpensiveComponent rendered') // Wird oft geloggt
return (
<div>
{data.map((item) => (
<ComplexItem
key={item.id}
item={item}
onUpdate={onUpdate}
/>
))}
</div>
)
}
// Mit React.memo - Component wird nur bei Props-Änderungen gerendert
const ExpensiveComponent = React.memo(({ data, onUpdate }) => {
console.log('ExpensiveComponent rendered') // Wird seltener geloggt
return (
<div>
{data.map((item) => (
<ComplexItem
key={item.id}
item={item}
onUpdate={onUpdate}
/>
))}
</div>
)
})
// Benutzerdefinierte Vergleichsfunktion
const ExpensiveComponent = React.memo(
({ data, onUpdate }) => {
// Component logic
},
(prevProps, nextProps) => {
// Nur re-rendern wenn sich relevante Props geändert haben
return (
prevProps.data.length === nextProps.data.length &&
prevProps.data.every(
(item, index) =>
item.id === nextProps.data[index]?.id && item.lastModified === nextProps.data[index]?.lastModified
)
)
}
)
useMemo für teure Berechnungen
import { useMemo } from 'react'
const DataVisualization = ({ rawData, filters, sortBy }) => {
// Teure Datenverarbeitung wird nur bei Änderungen ausgeführt
const processedData = useMemo(() => {
console.log('Processing data...') // Sollte nur bei Changes geloggt werden
return rawData
.filter((item) => filters.every((filter) => filter.test(item)))
.sort((a, b) => {
if (sortBy === 'date') return new Date(b.date) - new Date(a.date)
if (sortBy === 'name') return a.name.localeCompare(b.name)
return 0
})
.map((item) => ({
...item,
formattedDate: new Intl.DateTimeFormat('de-DE').format(new Date(item.date)),
category: getCategoryForItem(item), // Weitere teure Operation
}))
}, [rawData, filters, sortBy])
const chartConfig = useMemo(() => {
return {
data: processedData,
options: {
responsive: true,
plugins: {
legend: { position: 'top' },
title: { display: true, text: `Data Analysis (${processedData.length} items)` },
},
},
}
}, [processedData])
return (
<div>
<Chart {...chartConfig} />
<DataTable data={processedData} />
</div>
)
}
useCallback für Event Handlers
Problem: Neue Funktionen bei jedem Render
// Problematisch - neue Funktion bei jedem Render
const TodoList = ({ todos, onUpdate }) => {
return (
<div>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
// Neue Funktion bei jedem Render = unnötige Re-Renders
onToggle={() => onUpdate(todo.id, { completed: !todo.completed })}
onDelete={() => onUpdate(todo.id, null)}
/>
))}
</div>
)
}
Lösung: useCallback verwenden
import { useCallback } from 'react'
const TodoList = ({ todos, onUpdate }) => {
// Callback-Funktionen werden nur bei Dependency-Änderungen neu erstellt
const handleToggle = useCallback(
(todoId, completed) => {
onUpdate(todoId, { completed })
},
[onUpdate]
)
const handleDelete = useCallback(
(todoId) => {
onUpdate(todoId, null)
},
[onUpdate]
)
const handleEdit = useCallback(
(todoId, newText) => {
onUpdate(todoId, { text: newText })
},
[onUpdate]
)
return (
<div>
{todos.map((todo) => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={handleToggle}
onDelete={handleDelete}
onEdit={handleEdit}
/>
))}
</div>
)
}
// TodoItem Component profitiert von stabilen Callbacks
const TodoItem = React.memo(({ todo, onToggle, onDelete, onEdit }) => {
console.log(`Rendering TodoItem ${todo.id}`) // Sollte minimal sein
return (
<div className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id, !todo.completed)}
/>
<span className={todo.completed ? 'completed' : ''}>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>Delete</button>
</div>
)
})
Code Splitting und Lazy Loading
Route-based Code Splitting
import { lazy, Suspense } from 'react'
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'
// Lazy-loaded Components
const Home = lazy(() => import('./pages/Home'))
const About = lazy(() => import('./pages/About'))
const Dashboard = lazy(() => import('./pages/Dashboard'))
const UserProfile = lazy(() => import('./pages/UserProfile'))
// Loading Component
const LoadingSpinner = () => (
<div className="flex items-center justify-center min-h-screen">
<div className="animate-spin rounded-full h-32 w-32 border-b-2 border-blue-600" />
<span className="ml-4 text-lg">Loading...</span>
</div>
)
function App() {
return (
<Router>
<div className="app">
<Header />
<main>
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route
path="/"
element={<Home />}
/>
<Route
path="/about"
element={<About />}
/>
<Route
path="/dashboard"
element={<Dashboard />}
/>
<Route
path="/profile/:userId"
element={<UserProfile />}
/>
</Routes>
</Suspense>
</main>
<Footer />
</div>
</Router>
)
}
Component-based Code Splitting
import { useState, lazy, Suspense } from 'react'
// Heavy Components lazy laden
const HeavyChart = lazy(() => import('./HeavyChart'))
const DataExporter = lazy(() => import('./DataExporter'))
const AdvancedFilters = lazy(() => import('./AdvancedFilters'))
const Dashboard = () => {
const [activeTab, setActiveTab] = useState('overview')
const [showAdvancedFilters, setShowAdvancedFilters] = useState(false)
return (
<div className="dashboard">
<div className="tabs">
<button
onClick={() => setActiveTab('overview')}
className={activeTab === 'overview' ? 'active' : ''}
>
Overview
</button>
<button
onClick={() => setActiveTab('charts')}
className={activeTab === 'charts' ? 'active' : ''}
>
Charts
</button>
<button
onClick={() => setActiveTab('export')}
className={activeTab === 'export' ? 'active' : ''}
>
Export
</button>
</div>
<div className="tab-content">
{activeTab === 'overview' && (
<div>
<h2>Dashboard Overview</h2>
<p>Basic dashboard information loads immediately</p>
<button onClick={() => setShowAdvancedFilters(!showAdvancedFilters)}>
{showAdvancedFilters ? 'Hide' : 'Show'} Advanced Filters
</button>
{showAdvancedFilters && (
<Suspense fallback={<div>Loading filters...</div>}>
<AdvancedFilters />
</Suspense>
)}
</div>
)}
{activeTab === 'charts' && (
<Suspense fallback={<div>Loading charts...</div>}>
<HeavyChart />
</Suspense>
)}
{activeTab === 'export' && (
<Suspense fallback={<div>Loading export tools...</div>}>
<DataExporter />
</Suspense>
)}
</div>
</div>
)
}
Preloading Strategien
import { lazy } from 'react'
// Component mit Preload-Funktion
const HeavyComponent = lazy(() => import('./HeavyComponent'))
// Preload-Funktion für Hover/Focus
const preloadHeavyComponent = () => {
import('./HeavyComponent')
}
const HomePage = () => {
return (
<div>
<h1>Welcome</h1>
<nav>
<Link
to="/heavy"
onMouseEnter={preloadHeavyComponent} // Preload on hover
onFocus={preloadHeavyComponent} // Preload on focus
>
Heavy Page
</Link>
</nav>
</div>
)
}
Virtualisierung für große Datenmengen
React Window für lange Listen
import { FixedSizeList as List } from 'react-window'
interface Item {
id: string
name: string
description: string
avatar: string
}
interface ItemRendererProps {
index: number
style: React.CSSProperties
data: Item[]
}
const ItemRenderer = ({ index, style, data }: ItemRendererProps) => {
const item = data[index]
return (
<div
style={style}
className="list-item"
>
<img
src={item.avatar}
alt={item.name}
className="avatar"
/>
<div className="content">
<h3>{item.name}</h3>
<p>{item.description}</p>
</div>
</div>
)
}
const VirtualizedList = ({ items }: { items: Item[] }) => {
return (
<List
height={600} // Container-Höhe
itemCount={items.length}
itemSize={80} // Höhe pro Item
itemData={items} // Daten für Items
width="100%"
>
{ItemRenderer}
</List>
)
}
// Usage mit 10.000+ Items - nur sichtbare Items werden gerendert
const App = () => {
const [items] = useState(() => generateLargeDataset(10000))
return (
<div>
<h1>10,000 Items - Virtualized</h1>
<VirtualizedList items={items} />
</div>
)
}
Variable Größen mit VariableSizeList
import { VariableSizeList as List } from 'react-window'
const VariableItemRenderer = ({ index, style, data }) => {
const item = data[index]
return (
<div
style={style}
className="variable-item"
>
<h3>{item.title}</h3>
<p>{item.content}</p>
{item.image && (
<img
src={item.image}
alt={item.title}
/>
)}
</div>
)
}
const VariableSizeVirtualList = ({ items }) => {
// Funktion zur Berechnung der Item-Höhe
const getItemSize = (index) => {
const item = items[index]
let height = 60 // Base height
if (item.content?.length > 100) height += 40
if (item.image) height += 200
if (item.tags?.length > 0) height += 30
return height
}
return (
<List
height={600}
itemCount={items.length}
itemSize={getItemSize}
itemData={items}
width="100%"
>
{VariableItemRenderer}
</List>
)
}
Image Optimization
Next.js Image Component
import Image from 'next/image'
const OptimizedImageGallery = ({ images }) => {
return (
<div className="gallery">
{images.map((img, index) => (
<div
key={img.id}
className="gallery-item"
>
<Image
src={img.url}
alt={img.alt}
width={400}
height={300}
placeholder="blur"
blurDataURL={img.blurDataURL}
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
priority={index < 3} // Priorität für first 3 images
quality={85}
className="gallery-image"
/>
</div>
))}
</div>
)
}
Progressive Image Loading
import { useState, useCallback } from 'react'
const ProgressiveImage = ({ src, placeholderSrc, alt, className }) => {
const [imgSrc, setImgSrc] = useState(placeholderSrc || src)
const [isLoading, setIsLoading] = useState(true)
const onLoad = useCallback(() => {
setIsLoading(false)
}, [])
const onError = useCallback(() => {
setImgSrc(placeholderSrc)
setIsLoading(false)
}, [placeholderSrc])
useEffect(() => {
const img = new window.Image()
img.src = src
img.onload = () => {
setImgSrc(src)
setIsLoading(false)
}
img.onerror = onError
}, [src, onError])
return (
<div className={`progressive-image ${className}`}>
<img
src={imgSrc}
alt={alt}
onLoad={onLoad}
onError={onError}
className={`${isLoading ? 'loading' : 'loaded'}`}
/>
{isLoading && (
<div className="loading-overlay">
<div className="spinner" />
</div>
)}
</div>
)
}
Bundle Optimization
Webpack Bundle Analyzer
# Bundle-Größe analysieren
npm install --save-dev webpack-bundle-analyzer
# Build mit Analyse
npm run build && npx webpack-bundle-analyzer build/static/js/*.js
Tree Shaking optimieren
// Schlecht - Importiert gesamte Library
import * as _ from 'lodash'
import moment from 'moment'
// Besser - Nur benötigte Funktionen
import debounce from 'lodash/debounce'
import isEqual from 'lodash/isEqual'
import { format } from 'date-fns'
// Tree-shaking freundliche Imports
import { Button, Input, Modal } from '@company/ui-library'
// Anstatt
import UILibrary from '@company/ui-library'
const { Button, Input, Modal } = UILibrary
Dynamic Imports für bedingte Features
const AdvancedFeatures = () => {
const [showChart, setShowChart] = useState(false)
const [ChartComponent, setChartComponent] = useState(null)
const loadChart = async () => {
if (!ChartComponent) {
// Chart-Library wird nur geladen wenn benötigt
const { Chart } = await import('react-chartjs-2')
setChartComponent(() => Chart)
}
setShowChart(true)
}
return (
<div>
<button onClick={loadChart}>Show Advanced Chart</button>
{showChart && ChartComponent && (
<Suspense fallback={<div>Loading chart...</div>}>
<ChartComponent
data={chartData}
options={chartOptions}
/>
</Suspense>
)}
</div>
)
}
State Management Optimierung
Context API Performance
// Problematisch - Ein Context für alles
const AppContext = createContext()
const AppProvider = ({ children }) => {
const [user, setUser] = useState(null)
const [theme, setTheme] = useState('light')
const [notifications, setNotifications] = useState([])
const [cart, setCart] = useState([])
// Alle Consumer re-rendern bei jeder State-Änderung
const value = { user, setUser, theme, setTheme, notifications, setNotifications, cart, setCart }
return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}
// Besser - Getrennte Contexts
const UserContext = createContext()
const ThemeContext = createContext()
const NotificationContext = createContext()
const CartContext = createContext()
// Noch besser - Context mit Reducer und Memoization
const UserContext = createContext()
const UserProvider = ({ children }) => {
const [state, dispatch] = useReducer(userReducer, initialState)
// Memoize Context-Value
const value = useMemo(() => ({ state, dispatch }), [state])
return <UserContext.Provider value={value}>{children}</UserContext.Provider>
}
// Custom Hook mit Selector-Pattern
const useUser = (selector = (state) => state) => {
const context = useContext(UserContext)
if (!context) throw new Error('useUser must be used within UserProvider')
return useMemo(() => selector(context.state), [context.state, selector])
}
// Usage - Component re-rendert nur bei relevanten Änderungen
const UserProfile = () => {
const userName = useUser((state) => state.name)
const userEmail = useUser((state) => state.email)
return (
<div>
<h1>{userName}</h1>
<p>{userEmail}</p>
</div>
)
}
Performance Monitoring in Production
Error Boundaries für Performance
class PerformanceErrorBoundary extends React.Component {
constructor(props) {
super(props)
this.state = { hasError: false, error: null }
}
static getDerivedStateFromError(error) {
return { hasError: true, error }
}
componentDidCatch(error, errorInfo) {
// Log Performance-relevante Fehler
console.error('Performance Error:', error, errorInfo)
// Sende an Monitoring-Service
if (typeof window !== 'undefined') {
window.gtag?.('event', 'exception', {
description: error.toString(),
fatal: false,
})
}
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<button onClick={() => this.setState({ hasError: false, error: null })}>Try again</button>
</div>
)
}
return this.props.children
}
}
Custom Performance Hooks
import { useEffect, useRef } from 'react'
// Hook zur Messung von Component-Render-Zeit
export const useRenderTime = (componentName) => {
const renderStartTime = useRef()
useEffect(() => {
renderStartTime.current = performance.now()
})
useEffect(() => {
if (renderStartTime.current) {
const renderTime = performance.now() - renderStartTime.current
console.log(`${componentName} render time: ${renderTime.toFixed(2)}ms`)
// Sende an Analytics wenn Render-Zeit > Threshold
if (renderTime > 100) {
gtag('event', 'slow_render', {
event_category: 'Performance',
event_label: componentName,
value: Math.round(renderTime),
})
}
}
})
}
// Hook für Memory Usage Monitoring
export const useMemoryMonitoring = () => {
useEffect(() => {
if ('memory' in performance) {
const logMemory = () => {
const memory = performance.memory
console.log({
usedJSHeapSize: (memory.usedJSHeapSize / 1048576).toFixed(2) + ' MB',
totalJSHeapSize: (memory.totalJSHeapSize / 1048576).toFixed(2) + ' MB',
jsHeapSizeLimit: (memory.jsHeapSizeLimit / 1048576).toFixed(2) + ' MB',
})
}
const interval = setInterval(logMemory, 30000) // Every 30 seconds
return () => clearInterval(interval)
}
}, [])
}
// Usage in Components
const HeavyComponent = () => {
useRenderTime('HeavyComponent')
useMemoryMonitoring()
// Component logic...
}
Fazit und Checklist
Performance Optimization Checklist
Rendering Optimierung:
-
React.memo für expensive components
-
useMemo für teure Berechnungen
-
useCallback für Event Handlers
-
Vermeidung von Inline-Objects und -Functions in JSX
Code Splitting:
-
Route-based Code Splitting implementiert
-
Lazy Loading für heavy components
-
Preloading-Strategien für kritische Routen
Bundle Optimierung:
-
Tree Shaking aktiviert
-
Bundle-Größe analysiert
-
Unnötige Dependencies entfernt
-
Dynamic Imports für bedingte Features
Daten und State:
-
Virtualisierung für große Listen
-
Optimierte State Management Patterns
-
Efficient API Data Fetching
-
Caching-Strategien implementiert
Assets:
-
Image Optimization
-
Lazy Loading für Images
-
Progressive Enhancement
-
Critical CSS inline
Monitoring:
-
Core Web Vitals Tracking
-
Performance Monitoring in Production
-
Error Boundaries implementiert
-
Memory Leak Detection
Performance-Optimierung ist ein kontinuierlicher Prozess. Messen Sie regelmäßig, identifizieren Sie Bottlenecks und wenden Sie die passenden Optimierungsstrategien an. Mit den gezeigten Techniken können Sie React-Anwendungen erstellen, die sowohl für Entwickler als auch für Endnutzer eine hervorragende Performance bieten.


