Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Paweł Łukasiewicz: programista blogger
Paweł Łukasiewicz
2026-02-03
Udostępnij Udostępnij Kontakt
Wprowadzenie

Aplikacja działa, komponenty renderują się poprawnie, wszystko wygląda świetnie. Ale po dodaniu kilkudziesięciu komponentów, większej ilości danych i bardziej złożonej logiki nagle zauważasz... aplikacja zwolniła. Przewijanie (scrollowanie) jest niezgrabne, interakcje opóźnione, użytkownicy narzekają.

To jest moment, gdy optymalizacja wydajności staje się priorytetem. React jest szybki od razu po instalacji (out of the box), ale bez świadomego podejścia do wydajności łatwo o problemy. Na szczęście React daje nam potężne narzędzia: React.memo, useMemo, useCallback, leniwe ładowanie (lazy loading), dzielenie kodu (code splitting) i wiele więcej.

W tym wpisie nauczymy się identyfikować wąskie gardła wydajnościowe (bottlenecki), poznamy wszystkie techniki optymalizacji, zrozumiemy kiedy ich używać (i kiedy NIE używać!), a także nauczymy się profilować aplikację. To będzie techniczny wpis pełen praktycznych przykładów – po nim Twoja aplikacja będzie działać błyskawicznie!

Jak React renderuje komponenty?

Zanim zaczniemy optymalizować, musimy zrozumieć kiedy i dlaczego React renderuje komponenty. To fundament optymalizacji!

Cykl renderowania (Render cycle)

Każde renderowanie komponentu w React przechodzi przez 4 fazy:

  1. Coś się zmieniastan (state), właściwości (props), kontekst (context)
  2. React wywołuje funkcję komponentu – generuje nowy wirtualny DOM (Virtual DOM)
  3. Uzgadnianie (Reconciliation)React porównuje nowy Virtual DOM ze starym
  4. Zatwierdzanie (Commit)React aktualizuje prawdziwy DOM tylko tam gdzie trzeba
💡 Virtual DOM to lekka kopia prawdziwego DOM-u trzymana w pamięci. React porównuje dwa Virtual DOM-y aby znaleźć minimalne zmiany potrzebne w prawdziwym DOM-ie.

Kiedy komponent się ponownie renderuje?

Komponent renderuje się ponownie (re-renderuje) gdy:

  1. Jego stan się zmienia (setState)
  2. Jego props się zmieniają
  3. Rodzic się re-renderuje (i to jest często problem!)
  4. Context się zmienia (jeśli komponent go używa)
function Parent() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {/* ❌ Re-renderuje się przy każdym count++ mimo że nie używa count! */}
      <Child />
    </div>
  );
}

function Child() {
  console.log('Child rendered');
  return <div>I am child</div>;
}
Problem: Child nie używa count, ale re-renderuje się przy każdej zmianie count w Parent!

React.memo – memoizacja komponentów

React.memo to technika memoizacji (zapamiętywania wyników). Zapobiega niepotrzebnym re-renderom komponentu jeśli props się nie zmieniły.

📖 Memoizacja to zapamiętywanie wyników kosztownych operacji. Przy kolejnym wywołaniu z tymi samymi parametrami zwracamy zapamiętany wynik zamiast ponownie wykonywać obliczenia.

Podstawowe użycie

import { memo } from 'react';

interface ChildProps {
  name: string;
}

// Bez memo
function Child({ name }: ChildProps) {
  console.log('Child rendered');
  return <div>Hello {name}</div>;
}

// Z memo
const MemoizedChild = memo(Child);

// Lub bezpośrednio
const Child = memo(function Child({ name }: ChildProps) {
  console.log('Child rendered');
  return <div>Hello {name}</div>;
});

export default Child;
Jak działa?
  • React porównuje poprzednie props z nowymi props
  • Jeśli są identyczne (porównanie płytkie - shallow comparison) – nie renderuje
  • Jeśli różne – renderuje normalnie

Przykład z mierzeniem

import { memo, useState } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState('Paweł');
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      <button onClick={() => setName('Anna')}>Change name</button>
      
      {/* Child re-renderuje się TYLKO gdy name się zmieni */}
      <MemoizedChild name={name} />
    </div>
  );
}

const MemoizedChild = memo(function Child({ name }: { name: string }) {
  console.log('Child rendered');
  return <div>Hello {name}</div>;
});

Kliknij "Count" 10 razy – Child się nie renderuje! Kliknij "Change name" – Child się renderuje.

Własna funkcja porównująca (Custom comparison function)

Czasem potrzebujesz własnej logiki porównywania. Na przykład: komponent wyświetla tylko ID i imię użytkownika, więc nie chcesz re-renderować gdy zmienia się pole "lastLogin".

Praktyczne zastosowania: karty użytkowników (ignorujesz datę logowania), karty produktów (ignorujesz statystyki wyświetleń), komponenty wyświetlające część danych z dużego obiektu.

interface UserProps {
  user: {
    id: number;
    name: string;
    lastLogin: Date;  // To nas nie interesuje w UI
  };
}

const UserCard = memo(
  function UserCard({ user }: UserProps) {
    return (
      <div>
        <h3>{user.name}</h3>
        <p>ID: {user.id}</p>
      </div>
    );
  },
  (prevProps, nextProps) => {
    // Return true = NIE renderuj
    // Return false = renderuj
    return (
      prevProps.user.id === nextProps.user.id &&
      prevProps.user.name === nextProps.user.name
      // Ignorujemy lastLogin – nie wpływa na UI
    );
  }
);

Kiedy używać React.memo?

✅ Używaj gdy:
  • Komponent renderuje się często
  • Komponent jest kosztowny (duża lista, złożone obliczenia)
  • Props zmieniają się rzadko
  • Komponent otrzymuje te same props wielokrotnie
❌ NIE używaj gdy:
  • Komponent jest prosty (jedna linia JSX)
  • Props zmieniają się przy każdym renderze rodzica
  • Przedwczesna optymalizacja (najpierw zmierz - measure first!)
useMemo – memoizacja wartości

useMemo memoizuje wynik obliczenia – wykonuje funkcję tylko gdy zależności (dependencies) się zmienią.

Podstawowe użycie

import { useMemo } from 'react';

function ExpensiveComponent({ items }: { items: number[] }) {
  // ❌ Bez memoizacji - oblicza przy każdym renderze
  const sum = items.reduce((acc, item) => acc + item, 0);
  
  // ✅ Z memoizacją - oblicza tylko gdy items się zmienią
  const sum = useMemo(() => {
    console.log('Calculating sum...');
    return items.reduce((acc, item) => acc + item, 0);
  }, [items]);  // Tablica zależności
  
  return <div>Sum: {sum}</div>;
}

Praktyczny przykład: Filtrowanie listy

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState<'all' | 'active' | 'completed'>('all');
  const [searchQuery, setSearchQuery] = useState('');

  // ❌ Bez memoizacji - filtruje przy każdym renderze (nawet gdy searchQuery się zmienia)
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  // ✅ Z memoizacją - filtruje tylko gdy todos lub filter się zmienią
  const filteredTodos = useMemo(() => {
    console.log('Filtering todos...');
    return todos.filter(todo => {
      if (filter === 'active') return !todo.completed;
      if (filter === 'completed') return todo.completed;
      return true;
    });
  }, [todos, filter]);  // Nie zależy od searchQuery!

  return (
    <div>
      <input 
        value={searchQuery} 
        onChange={(e) => setSearchQuery(e.target.value)}
        placeholder="Search..."
      />
      {/* filteredTodos nie jest ponownie obliczane gdy searchQuery się zmienia! */}
      {filteredTodos.map(todo => <TodoItem key={todo.id} todo={todo} />)}
    </div>
  );
}

Kiedy używać useMemo?

✅ Używaj gdy:
  • Obliczenie jest kosztowne (duże listy, złożona matematyka)
  • Wartość używana w dependency array innego hooka
  • Obiekt/array przekazywany do React.memo komponentu
❌ NIE używaj gdy:
  • Obliczenie jest proste (dodawanie dwóch liczb)
  • Wartość używana tylko raz w JSX
  • Przedwczesna optymalizacja
// ❌ Overkill - to jest szybkie!
const sum = useMemo(() => a + b, [a, b]);

// ✅ To ma sens - to jest kosztowne
const sortedAndFiltered = useMemo(() => {
  return items
    .filter(item => item.price > 100)
    .sort((a, b) => b.price - a.price);
}, [items]);
useCallback – memoizacja funkcji

useCallback memoizuje funkcję – zwraca tę samą referencję gdy zależności (dependencies) się nie zmienią.

Problem: Funkcje tworzone przy każdym renderze

W JavaScript każda funkcja to nowy obiekt. Przy każdym renderze React tworzy NOWĄ funkcję, nawet jeśli kod jest identyczny. To psuje React.memo!

function Parent() {
  const [count, setCount] = useState(0);
  
  // ❌ Nowa funkcja przy każdym renderze!
  const handleClick = () => {
    console.log('Clicked');
  };
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {/* Child re-renderuje się mimo React.memo bo handleClick to zawsze nowa funkcja! */}
      <MemoizedChild onClick={handleClick} />
    </div>
  );
}

const MemoizedChild = memo(function Child({ onClick }: { onClick: () => void }) {
  console.log('Child rendered');
  return <button onClick={onClick}>Click me</button>;
});

Rozwiązanie: useCallback

import { useCallback } from 'react';

function Parent() {
  const [count, setCount] = useState(0);
  
  // ✅ Ta sama funkcja dopóki dependencies się nie zmienią
  const handleClick = useCallback(() => {
    console.log('Clicked');
  }, []);  // Pusta dependency array = funkcja nigdy się nie zmienia
  
  return (
    <div>
      <button onClick={() => setCount(count + 1)}>Count: {count}</button>
      {/* Child NIE re-renderuje się gdy count się zmienia! */}
      <MemoizedChild onClick={handleClick} />
    </div>
  );
}

Funkcja z zależnościami (dependencies)

Czasem funkcja musi używać wartości ze stanu. Masz dwa podejścia: dodać do zależności (słabe) lub użyć funkcyjnej aktualizacji stanu (functional update) – lepsze!

function TodoList() {
  const [todos, setTodos] = useState<Todo[]>([]);

  // ❌ Słabe rozwiązanie - funkcja zmienia się za każdym razem gdy todos się zmienią
  const handleToggle = useCallback((id: number) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  }, [todos]);  // Zależność od todos

  // ✅ Lepiej - functional update, brak dependency
  const handleToggleBetter = useCallback((id: number) => {
    setTodos(prevTodos =>   // prevTodos = aktualne todos
      prevTodos.map(todo =>
        todo.id === id ? { ...todo, completed: !todo.completed } : todo
      )
    );
  }, []);  // Nigdy się nie zmienia!

  return (
    <div>
      {todos.map(todo => (
        <MemoizedTodoItem 
          key={todo.id} 
          todo={todo} 
          onToggle={handleToggleBetter} 
        />
      ))}
    </div>
  );
}
💡 Funkcyjna aktualizacja stanu: Zamiast setTodos(todos.map(...)) piszesz setTodos(prevTodos => prevTodos.map(...)). React przekaże aktualną wartość jako argument.

Kiedy używać useCallback?

✅ Używaj gdy:
  • Funkcja przekazywana do React.memo komponentu
  • Funkcja w dependency array useEffect/useMemo
  • Funkcja przekazywana do wielu dzieci
❌ NIE używaj gdy:
  • Funkcja używana tylko w JSX (onClick w tym samym komponencie)
  • Przedwczesna optymalizacja
Leniwe ładowanie i dzielenie kodu (Lazy loading i Code Splitting)

Leniwe ładowanie (Lazy loading) to technika ładowania kodu komponentu dopiero gdy jest potrzebny. Zamiast ładować całą aplikację (500KB) od razu, ładujesz tylko to co widzi użytkownik (150KB), resztę dopiero gdy nawiguje dalej.

Dzielenie kodu (Code splitting) to podział aplikacji na mniejsze pliki (chunks) ładowane niezależnie.

React.lazy – leniwe ładowanie komponentów

React.lazy pozwala importować komponenty dynamicznie. Kod komponentu pobiera się z serwera dopiero gdy ma być wyrenderowany.

Kiedy używać: Strony po zalogowaniu (Dashboard, Profile), rzadko używane funkcje (edytor zdjęć, eksport PDF), duże biblioteki (edytor tekstu, wykresy 3D).

import { lazy, Suspense } from 'react';

// ❌ Import synchroniczny - kod ładuje się OD RAZU
import Dashboard from './pages/Dashboard';
import Profile from './pages/Profile';

// ✅ Lazy import - kod ładuje się DOPIERO gdy potrzebny
const Dashboard = lazy(() => import('./pages/Dashboard'));
const Profile = lazy(() => import('./pages/Profile'));
const Settings = lazy(() => import('./pages/Settings'));

function App() {
  return (
    <Routes>
      <Route path="/" element={<Home />} />
      <Route 
        path="/dashboard" 
        element={
          <Suspense fallback={<div>Ładowanie...</div>}>
            <Dashboard />
          </Suspense>
        } 
      />
      <Route 
        path="/profile" 
        element={
          <Suspense fallback={<div>Ładowanie...</div>}>
            <Profile />
          </Suspense>
        } 
      />
    </Routes>
  );
}
📦 Rezultat: Zamiast jednego pliku bundle 500KB masz:
  • main.js: 150KB (zawsze ładowany)
  • dashboard.js: 120KB (ładowany tylko na /dashboard)
  • profile.js: 100KB (ładowany tylko na /profile)
  • settings.js: 130KB (ładowany tylko na /settings)
Zysk: Pierwsza wizyta 150KB zamiast 500KB = 3x szybciej!

Komponent opakowujący dla wielu tras (Suspense wrapper)

Zamiast powtarzać <Suspense> dla każdej trasy, stwórz komponent pomocniczy. Będziesz miał spójny wygląd ładowania i łatwiejszą konserwację.

function LazyRoute({ children }: { children: React.ReactNode }) {
  return (
    <Suspense fallback={
      <div className="loading-container">
        <Spinner />
        <p>Ładowanie...</p>
      </div>
    }>
      {children}
    </Suspense>
  );
}

function App() {
  return (
    <Routes>
      <Route path="/dashboard" element={<LazyRoute><Dashboard /></LazyRoute>} />
      <Route path="/profile" element={<LazyRoute><Profile /></LazyRoute>} />
    </Routes>
  );
}

Wstępne ładowanie (Preloading)

Możesz załadować komponent ZANIM użytkownik kliknie link. Gdy najedzie myszką - zacznij pobierać kod. Gdy kliknie - kod już gotowy!

const Dashboard = lazy(() => import('./pages/Dashboard'));

function Navigation() {
  return (
    <nav>
      <Link 
        to="/dashboard"
        onMouseEnter={() => {
          // Preload na hover - pobierz kod Dashboard
          import('./pages/Dashboard');
        }}
      >
        Dashboard
      </Link>
    </nav>
  );
}
Wirtualizacja list (Windowing / Virtualization)

Wirtualizacja (Virtualization) to renderowanie tylko widocznych elementów listy. Masz 10,000 produktów? Renderuj tylko 20 widocznych na ekranie! Gdy użytkownik przewija - podmieniaj elementy.

Problem bez wirtualizacji: 10,000 elementów DOM = wolne przewijanie, duże zużycie pamięci.
Z wirtualizacją: Renderujesz ~20 elementów (ile mieści się na ekranie) = płynne przewijanie!

Biblioteka react-window

react-window to lekka biblioteka do wirtualizacji. Prosta, szybka, wystarczająca dla większości przypadków.

npm install react-window
import { FixedSizeList } from 'react-window';

interface Item {
  id: number;
  title: string;
}

function VirtualList({ items }: { items: Item[] }) {
  // Funkcja renderująca pojedynczy wiersz
  const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => (
    <div style={style}>  {/* style zawiera pozycjonowanie z react-window */}
      {items[index].title}
    </div>
  );

  return (
    <FixedSizeList
      height={600}        // Wysokość kontenera
      itemCount={items.length}  // Ile elementów w sumie
      itemSize={50}       // Wysokość jednego elementu
      width="100%"
    >
      {Row}
    </FixedSizeList>
  );
}
Bez virtualization: 10,000 elementów = 10,000 węzłów DOM
Z virtualization: 10,000 elementów = ~20 węzłów DOM (tylko widoczne)

react-virtualized (bardziej zaawansowana)

react-virtualized to starsza, ale bardziej funkcjonalna biblioteka.

npm install react-virtualized

Obsługuje:

  • Dynamiczne wysokości elementów - elementy mogą mieć różne wysokości
  • Widok siatki (Grid view) - nie tylko listy, ale siatki 2D
  • Nieskończone przewijanie (Infinite scroll) - ładowanie danych przy przewijaniu
  • Przewijanie do elementu (Scroll to item) - programowe przewijanie
Debouncing i Throttling - optymalizacja częstych zdarzeń

Obie techniki służą do ograniczania częstotliwości wykonywania funkcji. Używamy ich dla zdarzeń występujących bardzo często (pisanie, scroll, resize).

Debouncing – czekaj aż użytkownik przestanie działać

Debouncing (opóźnianie) wykonuje funkcję dopiero gdy użytkownik przestanie wykonywać akcję przez określony czas.

Przykład: Pole wyszukiwania - nie wysyłaj zapytania przy każdej literze, tylko gdy użytkownik przestanie pisać przez 500ms.

Zastosowania: pole wyszukiwania, autozapis formularza, walidacja, autouzupełnianie.

import { useState, useEffect } from 'react';
import { useDebounce } from './hooks/useDebounce';

function SearchUsers() {
  const [query, setQuery] = useState('');
  const debouncedQuery = useDebounce(query, 500);  // Czekaj 500ms po przestaniu pisania
  const [results, setResults] = useState([]);

  useEffect(() => {
    if (debouncedQuery) {
      // To wykona się dopiero 500ms PO zaprzestaniu pisania
      fetch(`/api/search?q=${debouncedQuery}`)
        .then(res => res.json())
        .then(setResults);
    }
  }, [debouncedQuery]);

  return (
    <div>
      <input 
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Szukaj..."
      />
      {/* Search wywołuje się 500ms PO zaprzestaniu pisania */}
      {results.map(user => <UserCard key={user.id} user={user} />)}
    </div>
  );
}
💡 Jak działa debouncing:
Użytkownik pisze "React":
R → timer start → Re → reset timer → Rea → reset → React → przestaje pisać → po 500ms wywołaj funkcję
Efekt: 1 zapytanie zamiast 5!

Throttling – limituj częstotliwość wykonywania

Throttling (dławienie) ogranicza częstotliwość do maksymalnie raz na X milisekund. Nawet jeśli zdarzenie wystąpi 100 razy/sekundę, funkcja wykona się max np. 5 razy (co 200ms).

Zastosowania: scroll (infinite scroll), resize okna, ruch myszki, gry (aktualizacja pozycji).

import { useState, useRef, useCallback } from 'react';

function useThrottle<T extends (...args: any[]) => any>(
  callback: T,
  delay: number
): T {
  const lastRan = useRef(Date.now());

  return useCallback(
    ((...args) => {
      const now = Date.now();
      
      if (now - lastRan.current >= delay) {
        callback(...args);
        lastRan.current = now;
      }
    }) as T,
    [callback, delay]
  );
}

// Użycie - scroll handler
function InfiniteScroll() {
  const handleScroll = useThrottle(() => {
    console.log('Scroll event');
    // Sprawdź czy załadować więcej danych
  }, 200);  // Max raz na 200ms

  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [handleScroll]);

  return <div>{/* content */}</div>;
}
🔄 Debouncing vs Throttling:
Debouncing: Wykonaj RAZ po zaprzestaniu (wyszukiwanie)
Throttling: Wykonuj REGULARNIE co X ms podczas akcji (scroll)
Profilowanie wydajności (Performance Profiling)

Profilowanie to mierzenie wydajności aplikacji. Zamiast zgadywać - mierzysz i widzisz dokładnie które komponenty są wolne!

React DevTools Profiler

Narzędzie wbudowane w rozszerzenie React DevTools. Pozwala nagrać interakcje i zobaczyć co się renderowało, jak długo i dlaczego.

  1. Zainstaluj React DevTools
  2. Otwórz Profiler tab
  3. Kliknij Record
  4. Wykonaj akcje w aplikacji
  5. Stop recording

Zobaczysz:

  • Które komponenty się renderowały
  • Jak długo trwał każdy render
  • Dlaczego komponent się renderował

Profiler API w kodzie

Możesz też dodać profilowanie w kodzie za pomocą komponentu <Profiler>.

import { Profiler } from 'react';

function onRenderCallback(
  id: string,
  phase: 'mount' | 'update',    // mount = pierwsze, update = ponowne
  actualDuration: number,        // Czas spędzony na renderowaniu (ms)
  baseDuration: number,          // Szacowany czas bez memoizacji (ms)
  startTime: number,
  commitTime: number
) {
  console.log(`${id} (${phase}) took ${actualDuration}ms`);
}

function App() {
  return (
    <Profiler id="TodoList" onRender={onRenderCallback}>
      <TodoList />
    </Profiler>
  );
}
Praktyczny przykład: Optymalizacja Todo App

Przed optymalizacją:

// ❌ Wiele problemów wydajnościowych
function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState('all');

  // ❌ PROBLEM 1: Filtrowanie przy KAŻDYM renderze
  const filteredTodos = todos.filter(todo => {
    if (filter === 'active') return !todo.completed;
    if (filter === 'completed') return todo.completed;
    return true;
  });

  // ❌ PROBLEM 2: Stats obliczane przy KAŻDYM renderze
  const stats = {
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  };

  return (
    <div>
      <TodoStats stats={stats} />
      <TodoFilters filter={filter} setFilter={setFilter} />
      {filteredTodos.map(todo => (
        <TodoItem 
          key={todo.id} 
          todo={todo}
          onToggle={(id) => {
            setTodos(todos.map(t => 
              t.id === id ? { ...t, completed: !t.completed } : t
            ));
          }}
          onDelete={(id) => {
            setTodos(todos.filter(t => t.id !== id));
          }}
        />
      ))}
    </div>
  );
}

function TodoItem({ todo, onToggle, onDelete }) {
  return (
    <div>
      <input 
        type="checkbox" 
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
}
Problemy:
  1. filteredTodos obliczane przy każdym renderze
  2. stats obliczane przy każdym renderze
  3. TodoItem renderuje się gdy KTÓRYKOLWIEK todo się zmieni
  4. Funkcje onToggle, onDelete tworzone przy każdym renderze

Po optymalizacji:

// ✅ Zoptymalizowane
function TodoApp() {
  const [todos, setTodos] = useState<Todo[]>([]);
  const [filter, setFilter] = useState('all');

  // Memoizacja filtrowania
  const filteredTodos = useMemo(() => {
    return todos.filter(todo => {
      if (filter === 'active') return !todo.completed;
      if (filter === 'completed') return todo.completed;
      return true;
    });
  }, [todos, filter]);

  // Memoizacja statystyk
  const stats = useMemo(() => ({
    total: todos.length,
    completed: todos.filter(t => t.completed).length,
    active: todos.filter(t => !t.completed).length
  }), [todos]);

  // Memoizacja funkcji
  const handleToggle = useCallback((id: number) => {
    setTodos(prev => prev.map(t => 
      t.id === id ? { ...t, completed: !t.completed } : t
    ));
  }, []);

  const handleDelete = useCallback((id: number) => {
    setTodos(prev => prev.filter(t => t.id !== id));
  }, []);

  return (
    <div>
      <MemoizedTodoStats stats={stats} />
      <MemoizedTodoFilters filter={filter} setFilter={setFilter} />
      {filteredTodos.map(todo => (
        <MemoizedTodoItem 
          key={todo.id} 
          todo={todo}
          onToggle={handleToggle}
          onDelete={handleDelete}
        />
      ))}
    </div>
  );
}

// Memoizowane komponenty
const MemoizedTodoItem = memo(function TodoItem({ 
  todo, 
  onToggle, 
  onDelete 
}: TodoItemProps) {
  return (
    <div>
      <input 
        type="checkbox" 
        checked={todo.completed}
        onChange={() => onToggle(todo.id)}
      />
      <span>{todo.text}</span>
      <button onClick={() => onDelete(todo.id)}>Delete</button>
    </div>
  );
});

const MemoizedTodoStats = memo(function TodoStats({ stats }: { stats: Stats }) {
  return (
    <div>
      <span>Total: {stats.total}</span>
      <span>Active: {stats.active}</span>
      <span>Completed: {stats.completed}</span>
    </div>
  );
});

const MemoizedTodoFilters = memo(function TodoFilters({ 
  filter, 
  setFilter 
}: FiltersProps) {
  return (
    <div>
      <button onClick={() => setFilter('all')}>All</button>
      <button onClick={() => setFilter('active')}>Active</button>
      <button onClick={() => setFilter('completed')}>Completed</button>
    </div>
  );
});
Rezultat:
  • TodoItem renderuje się TYLKO gdy ten konkretny todo się zmieni
  • TodoStats renderuje się TYLKO gdy stats się zmienią
  • TodoFilters renderuje się TYLKO gdy filter się zmieni
  • Filtrowanie i stats obliczane tylko gdy potrzeba
Najlepsze praktyki (Best Practices)

1. Najpierw mierz, potem optymalizuj (Measure First, Optimize Later)

Nie optymalizuj "na ślepo". Najpierw zmierz wydajność za pomocą React DevTools Profiler!

// ❌ Przedwczesna optymalizacja
const sum = useMemo(() => a + b, [a, b]);

// ✅ Najpierw zmierz czy jest problem!
// Użyj React DevTools Profiler
⚠️ Przedwczesna optymalizacja to źródło problemów! Dodawanie memo/useMemo/useCallback wszędzie:
- Komplikuje kod
- Dodaje narzut (porównywanie deps też kosztuje!)
- Może nie przynieść korzyści

2. Nie optymalizuj wszystkiego

// ❌ Overkill - prosty komponent
const SimpleDiv = memo(function SimpleDiv() {
  return <div>Hello</div>;
});

// ✅ Optymalizuj kosztowne komponenty
const ExpensiveList = memo(function ExpensiveList({ items }) {
  return items.map(item => <ComplexItem key={item.id} item={item} />);
});

3. useCallback + memo razem

useCallback bez memo nie ma sensu:

// ❌ useCallback bez memo - bez efektu
function Parent() {
  const handleClick = useCallback(() => {}, []);
  return <Child onClick={handleClick} />; // Child nie jest memo!
}

// ✅ useCallback + memo - działa
const MemoChild = memo(Child);
function Parent() {
  const handleClick = useCallback(() => {}, []);
  return <MemoChild onClick={handleClick} />;
}

4. Aktualizacja funkcyjna lepsza niż zależności (Functional updates)

// ❌ Zbędna dependency
const increment = useCallback(() => {
  setCount(count + 1);
}, [count]); // Zmienia się za każdym razem!

// ✅ Functional update, brak dependency
const increment = useCallback(() => {
  setCount(prev => prev + 1);
}, []); // Nigdy się nie zmienia!

5. Właściwy key dla list

// ❌ Index jako key - problemy z re-orderowaniem
{items.map((item, index) => <Item key={index} item={item} />)}

// ✅ Unikalne ID jako key
{items.map(item => <Item key={item.id} item={item} />)}

6. Lazy load tras, nie wszystkie komponenty

// ✅ Lazy load całe strony
const Dashboard = lazy(() => import('./pages/Dashboard'));

// ❌ Nie lazy load małych komponentów
const Button = lazy(() => import('./components/Button'));

7. Używaj produkcyjnego buildu

# Development build - wolny!
npm run dev

# Production build - szybki!
npm run build
npm run preview
Production build:
  • Minifikacja - usunięcie białych znaków
  • Tree shaking - usunięcie nieużywanego kodu
  • Dead code elimination - usunięcie martwego kodu
  • Optymalizacje - różne optymalizacje kompilatora
Różnica: Development może być 3-5x wolniejszy!
Lista kontrolna optymalizacji (Checklist)

Gdy aplikacja zwalnia, przejdź przez tę listę:

  • ProfilujReact DevTools Profiler
  • Znajdź bottleneck (wąskie gardło) – który komponent renderuje się za często/długo?
  • React.memo – dla komponentów renderujących się bez powodu
  • useMemo – dla kosztownych obliczeń
  • useCallback – dla funkcji przekazywanych do memo komponentów
  • Lazy loading – dla tras/dużych komponentów
  • Virtualization (wirtualizacja) – dla długich list (1000+ elementów)
  • Debouncing – dla wyszukiwania (search)/autozapisu (autosave)
  • Throttling – dla scroll/resize/animacji
  • Code splitting (dzielenie kodu) – dla dużego bundle'a
  • Production build – zawsze testuj na production!
Podsumowanie

To był techniczny, ale bardzo ważny wpis! Nauczyliśmy się:

  • Cykl renderowania (Render cycle) – jak React renderuje komponenty
  • React.memo – memoizacja komponentów, kiedy używać
  • useMemo – memoizacja wartości, praktyczne przykłady
  • useCallback – memoizacja funkcji, aktualizacja funkcyjna (functional updates)
  • Lazy loadingReact.lazy, Suspense, dzielenie kodu (code splitting)
  • Virtualization (wirtualizacja)react-window dla długich list
  • Debouncing/Throttling – optymalizacja eventów
  • Profiling (profilowanie)React DevTools, jak mierzyć wydajność
  • Praktyczny przykład – optymalizacja Todo App
  • Best Practices (najlepsze praktyki) – 7 złotych zasad
  • Checklist (lista kontrolna) – krok po kroku co sprawdzić

Wydajność to nie luksus – to konieczność. Użytkownicy oczekują błyskawicznych aplikacji. Teraz masz wszystkie narzędzia by dostarczyć im dokładnie to!

W kolejnym wpisie poznamy zaawansowane TypeScript w Reacttypy użytkowe (utility types), typy generyczne (generics) w komponentach, strażnicy typów (type guards), typy warunkowe (conditional types). Nauczymy się jak wykorzystać pełnię możliwości TypeScript!

Zadanie dla Ciebie

Zoptymalizuj swoją aplikację:

  1. Użyj React DevTools Profiler – znajdź najwolniejszy komponent
  2. Dodaj React.memo do 3 komponentów
  3. Użyj useMemo dla kosztownego obliczenia
  4. Dodaj lazy loading dla jednej trasy
  5. (Bonus) Zaimplementuj virtualization (wirtualizację) dla długiej listy