React Performance Optimization: Ein umfassender Guide

5. Dezember 2024
10 min
Lukas Ernst
Performance Optimierung Visualisierung
Tutorials

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.

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.