We use cookies to enhance your experience on the site
CodeWorlds

Debouncing and Performance Optimization

When the user types in a search field, you don't want to send an API request for every keystroke. Debouncing solves this problem!

What is Debouncing? ⏱️

Debouncing is delaying function execution until the user stops acting.

Problem - Search without 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 on EVERY keystroke!
10    fetchPirates(value).then(setResults);
11  };
12
13  return <input value={query} onChange={handleChange} />;
14}

Problem:

  • User types "Jack Sparrow"
  • That's 12 characters = 12 API calls!
  • Wasting bandwidth, overloading the server

Solution - 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 - wait 500ms after last change
9  useEffect(() => {
10    const timer = setTimeout(() => {
11      setDebouncedQuery(query);
12    }, 500);
13
14    return () => clearTimeout(timer);
15  }, [query]);
16
17  // Fetch only when debouncedQuery changes
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="Search pirates..."
30      />
31      {results.map(pirate => <div key={pirate.id}>{pirate.name}</div>)}
32    </div>
33  );
34}

Result:

  • User types "Jack Sparrow"
  • We wait 500ms after the last keystroke
  • Only 1 API call! (instead of 12)

Custom Hook - useDebounce 🎣

Create a reusable 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// Usage - super simple!
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="Search pirates..."
32    />
33  );
34}

Throttling - Limiting Frequency 🚦

Throttling executes a function at most once every X ms (regardless of how many calls).

Difference between Debounce and Throttle:

| | Debounce | Throttle | |---|----------|----------| | When it executes | After X ms of silence | Every X ms | | Use case | Search, resize, typing | Scroll, mouse move |

Example - 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// Usage - 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 - Memoized Components 🧠

React.memo
prevents unnecessary re-rendering of components.

Without memo:

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

With 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-renders → PostCard re-renders ONLY if post changed!

useMemo - Value Memoization 💾

useMemo
caches the result of an expensive operation.

1function PirateStats({ pirates }: { pirates: Pirate[] }) {
2  // ❌ Without memo - calculates on EVERY render
3  const totalLevel = pirates.reduce((sum, p) => sum + p.level, 0);
4
5  // ✅ With memo - calculates only when pirates change
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}

When to use useMemo:

  • Expensive calculations (filtering large lists, sorting)
  • Creating objects/arrays passed as props

When NOT to use it:

  • Simple operations (addition, string concat)
  • Primitive values (numbers, strings)

useCallback - Function Memoization 🔗

useCallback
caches a function between renders.

Problem:

1function Parent() {
2  const [count, setCount] = useState(0);
3
4  // ❌ New function on every render!
5  const handleClick = () => console.log("Clicked");
6
7  return <MemoizedChild onClick={handleClick} />;
8  // Child re-renders despite memo, because handleClick is always "new"
9}

Solution:

1import { useCallback } from 'react';
2
3function Parent() {
4  const [count, setCount] = useState(0);
5
6  // ✅ Same function between renders
7  const handleClick = useCallback(() => {
8    console.log("Clicked");
9  }, []);  // Empty array - function never changes
10
11  return <MemoizedChild onClick={handleClick} />;
12  // Child does NOT re-render (handleClick stays the same)
13}

When to use useCallback:

  • Passing a function to a memoized component (
    React.memo
    )
  • Function is a dependency in
    useEffect

Key Optimization - Lists 📝

The key prop is CRITICAL for list performance!

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

Why index is bad:

  • When you remove an element, indices change
  • React thinks they're different elements → unnecessary re-renders

Summary 🎓

Debouncing - wait X ms after last action (search) ✅ Throttling - execute at most once every X ms (scroll) ✅ React.memo - prevents component re-renders ✅ useMemo - caches results of expensive operations ✅ useCallback - caches functions ✅ key prop - use unique IDs, not index

Performance Rule: Optimize only when there's a problem - premature optimization is the root of all evil!

See you there! 🚀

Go to CodeWorlds