Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Debouncing i Performance Optimization

Gdy użytkownik wpisuje w search, nie chcesz wysyłać API request przy każdej literze. Debouncing rozwiązuje ten problem!

Czym jest Debouncing? ⏱️

Debouncing to opóźnienie wykonania funkcji do momentu, gdy użytkownik przestanie działać.

Problem - Search bez debounce:

1function SearchPirates() {
2  const [query, setQuery] = useState("");
3  const [results, setResults] = useState<Pirate[]>([]);
4
5  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
6    const value = e.target.value;
7    setQuery(value);
8    
9    // ❌ API call przy KAŻDEJ literze!
10    fetchPirates(value).then(setResults);
11  };
12
13  return <input value={query} onChange={handleChange} />;
14}

Problem:

  • User pisze "Jack Sparrow"
  • To 12 liter = 12 API calls!
  • Marnowanie bandwidth, obciążanie serwera

Rozwiązanie - Debouncing:

1import { useState, useEffect } from 'react';
2
3function SearchPirates() {
4  const [query, setQuery] = useState("");
5  const [debouncedQuery, setDebouncedQuery] = useState("");
6  const [results, setResults] = useState<Pirate[]>([]);
7
8  // Debounce - czekaj 500ms po ostatniej zmianie
9  useEffect(() => {
10    const timer = setTimeout(() => {
11      setDebouncedQuery(query);
12    }, 500);
13
14    return () => clearTimeout(timer);
15  }, [query]);
16
17  // Fetch tylko gdy debouncedQuery się zmieni
18  useEffect(() => {
19    if (debouncedQuery) {
20      fetchPirates(debouncedQuery).then(setResults);
21    }
22  }, [debouncedQuery]);
23
24  return (
25    <div>
26      <input
27        value={query}
28        onChange={(e) => setQuery(e.target.value)}
29        placeholder="Szukaj piratów..."
30      />
31      {results.map(pirate => <div key={pirate.id}>{pirate.name}</div>)}
32    </div>
33  );
34}

Efekt:

  • User pisze "Jack Sparrow"
  • Czekamy 500ms po ostatniej literze
  • Tylko 1 API call! (zamiast 12)

Custom Hook - useDebounce 🎣

Zrób reużywalny hook:

1function useDebounce<T>(value: T, delay: number): T {
2  const [debouncedValue, setDebouncedValue] = useState<T>(value);
3
4  useEffect(() => {
5    const timer = setTimeout(() => {
6      setDebouncedValue(value);
7    }, delay);
8
9    return () => clearTimeout(timer);
10  }, [value, delay]);
11
12  return debouncedValue;
13}
14
15// Użycie - super proste!
16function SearchPirates() {
17  const [query, setQuery] = useState("");
18  const debouncedQuery = useDebounce(query, 500);
19  const [results, setResults] = useState<Pirate[]>([]);
20
21  useEffect(() => {
22    if (debouncedQuery) {
23      fetchPirates(debouncedQuery).then(setResults);
24    }
25  }, [debouncedQuery]);
26
27  return (
28    <input
29      value={query}
30      onChange={(e) => setQuery(e.target.value)}
31      placeholder="Szukaj piratów..."
32    />
33  );
34}

Throttling - Ograniczanie częstotliwości 🚦

Throttling to wykonywanie funkcji max raz na X ms (niezależnie od ilości wywołań).

Różnica Debounce vs Throttle:

| | Debounce | Throttle | |---|----------|----------| | Kiedy wykonuje | Po X ms ciszy | Co X ms | | Użycie | Search, resize, typing | Scroll, mouse move |

Przykład - Scroll tracking:

1function useThrottle<T>(value: T, limit: number): T {
2  const [throttledValue, setThrottledValue] = useState<T>(value);
3  const lastRan = useRef(Date.now());
4
5  useEffect(() => {
6    const handler = setTimeout(() => {
7      if (Date.now() - lastRan.current >= limit) {
8        setThrottledValue(value);
9        lastRan.current = Date.now();
10      }
11    }, limit - (Date.now() - lastRan.current));
12
13    return () => clearTimeout(handler);
14  }, [value, limit]);
15
16  return throttledValue;
17}
18
19// Użycie - scroll position
20function ScrollTracker() {
21  const [scrollY, setScrollY] = useState(0);
22  const throttledScrollY = useThrottle(scrollY, 200);
23
24  useEffect(() => {
25    const handleScroll = () => setScrollY(window.scrollY);
26    window.addEventListener('scroll', handleScroll);
27    return () => window.removeEventListener('scroll', handleScroll);
28  }, []);
29
30  return <div>Scroll: {throttledScrollY}px</div>;
31}

React.memo - Memo komponenty 🧠

React.memo
zapobiega niepotrzebnemu re-renderowaniu komponentów.

Bez memo:

1function PostCard({ post }: { post: Post }) {
2  console.log("Rendering PostCard:", post.id);
3  return <div>{post.content}</div>;
4}
5
6// Parent re-renderuje → WSZYSTKIE PostCard re-renderują!

Z memo:

1import { memo } from 'react';
2
3const PostCard = memo(function PostCard({ post }: { post: Post }) {
4  console.log("Rendering PostCard:", post.id);
5  return <div>{post.content}</div>;
6});
7
8// Parent re-renderuje → PostCard re-renderuje TYLKO jeśli post się zmienił!

useMemo - Memoizacja wartości 💾

useMemo
cache'uje wynik kosztownej operacji.

1function PirateStats({ pirates }: { pirates: Pirate[] }) {
2  // ❌ Bez memo - liczy przy KAŻDYM renderze
3  const totalLevel = pirates.reduce((sum, p) => sum + p.level, 0);
4
5  // ✅ Z memo - liczy tylko gdy pirates się zmienią
6  const totalLevel = useMemo(() => {
7    console.log("Calculating total level...");
8    return pirates.reduce((sum, p) => sum + p.level, 0);
9  }, [pirates]);
10
11  return <div>Total Level: {totalLevel}</div>;
12}

Kiedy używać useMemo:

  • Kosztowne obliczenia (filtrowanie dużych list, sortowanie)
  • Tworzenie obiektów/tablic przekazywanych jako props

Kiedy NIE używać:

  • Proste operacje (dodawanie, string concat)
  • Wartości prymitywne (numbers, strings)

useCallback - Memoizacja funkcji 🔗

useCallback
cache'uje funkcję między renderami.

Problem:

1function Parent() {
2  const [count, setCount] = useState(0);
3
4  // ❌ Nowa funkcja przy każdym renderze!
5  const handleClick = () => console.log("Clicked");
6
7  return <MemoizedChild onClick={handleClick} />;  
8  // Child re-renderuje mimo memo, bo handleClick jest zawsze "nowy"
9}

Rozwiązanie:

1import { useCallback } from 'react';
2
3function Parent() {
4  const [count, setCount] = useState(0);
5
6  // ✅ Ta sama funkcja między renderami
7  const handleClick = useCallback(() => {
8    console.log("Clicked");
9  }, []);  // Pusty array - funkcja nigdy się nie zmienia
10
11  return <MemoizedChild onClick={handleClick} />;  
12  // Child NIE re-renderuje (handleClick jest ten sam)
13}

Kiedy używać useCallback:

  • Przekazujesz funkcję do zmemoizowanego komponentu (
    React.memo
    )
  • Funkcja jest dependency w
    useEffect

Key optimization - Lists 📝

key prop jest KRYTYCZNY dla wydajności list!

1// ❌ BAD - używanie index
2{pirates.map((pirate, index) => (
3  <PirateCard key={index} pirate={pirate} />
4))}
5
6// ✅ GOOD - używanie unique ID
7{pirates.map((pirate) => (
8  <PirateCard key={pirate.id} pirate={pirate} />
9))}

Dlaczego index jest zły:

  • Gdy usuniesz element, indeksy się zmieniają
  • React myśli że to inne elementy → niepotrzebne re-rendery

Podsumowanie 🎓

Debouncing - czekaj X ms po ostatniej akcji (search) ✅ Throttling - wykonuj max raz na X ms (scroll) ✅ React.memo - zapobiega re-renderom komponentów ✅ useMemo - cache'uje wyniki kosztownych operacji ✅ useCallback - cache'uje funkcje ✅ key prop - używaj unique ID, nie index

Performance Rule: Optymalizuj tylko gdy jest problem - przedwczesna optymalizacja to zło!

Do zobaczenia! 🚀

Przejdź do CodeWorlds