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 dlaczegoReact renderuje komponenty. To fundament optymalizacji!
Cykl renderowania (Render cycle)
Każde renderowanie komponentu w React przechodzi przez 4 fazy:
Coś się zmienia – stan (state), właściwości (props), kontekst (context)
React wywołuje funkcję komponentu – generuje nowy wirtualny DOM (Virtual DOM)
Uzgadnianie (Reconciliation) – React porównuje nowy Virtual DOM ze starym
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:
Jego stan się zmienia (setState)
Jego props się zmieniają
Rodzic się re-renderuje (i to jest często problem!)
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.
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)
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 arrayuseEffect/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ę.
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.
Zainstaluj React DevTools
Otwórz Profiler tab
Kliknij Record
Wykonaj akcje w aplikacji
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:
filteredTodos obliczane przy każdym renderze
stats obliczane przy każdym renderze
TodoItem renderuje się gdy KTÓRYKOLWIEK todo się zmieni
Funkcje onToggle, onDelete tworzone przy każdym renderze
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
// ❌ 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ę:
✓Profiluj – React 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ć
✅ Lazy loading – React.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 React – typy 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!