Browser & Performance Concepts
React runs inside the browser, and the browser has its own rules about how things work. Understanding these concepts will help you build apps that feel fast and smooth â not janky and frozen.
Event Delegation
What Is Event Delegation?
Think of it like a restaurant with one waiter for the whole floor instead of one waiter per table. Instead of attaching a listener to every single element, you attach one listener to a parent element and let events "bubble up" to it.
When you click a button inside a <div>, the click event doesn't just fire on the button. It travels upward through every parent element â button â div â section â body â document. This is called event bubbling.
function TodoList() {
const todos = ["Buy milk", "Walk the dog", "Write code"];
const handleClick = (e) => {
if (e.target.tagName === "LI") {
console.log("Clicked:", e.target.textContent);
}
};
return (
<ul onClick={handleClick}>
{todos.map((todo, i) => (
<li key={i}>{todo}</li>
))}
</ul>
);
}Instead of adding onClick to each <li>, we add one onClick to the <ul>. When any <li> is clicked, the event bubbles up to the <ul>, and we check which item was clicked.
How React Uses Event Delegation
Here's the secret: React already does event delegation for you. When you write onClick on a button, React doesn't actually attach a listener to that button. It attaches a single listener to the root of your app and handles all events from there.
function App() {
return (
<div>
<button onClick={() => console.log("Button 1")}>One</button>
<button onClick={() => console.log("Button 2")}>Two</button>
<button onClick={() => console.log("Button 3")}>Three</button>
</div>
);
}Even though it looks like three listeners, React uses one listener at the root. This is more memory-efficient, especially with large lists.
Remember: You don't need to implement event delegation manually in React â it's built in. But understanding the concept helps you debug event issues and write vanilla JavaScript when needed.
Throttling vs Debouncing
Both are techniques to limit how often a function runs. They solve similar problems but work differently.
Debouncing â "Wait Until They Stop"
Think of it like an elevator door: the door stays open as long as people keep entering. It only closes when no one has entered for a few seconds. Debouncing waits until the user stops doing something, then acts.
Best for: Search input, form validation, window resize handlers.
import { useState, useEffect } from "react";
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const timer = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(timer);
}, [value, delay]);
return debouncedValue;
}
function SearchBar() {
const [query, setQuery] = useState("");
const debouncedQuery = useDebounce(query, 500);
const [results, setResults] = useState([]);
useEffect(() => {
if (debouncedQuery) {
fetch(`/api/search?q=${debouncedQuery}`)
.then((res) => res.json())
.then(setResults);
}
}, [debouncedQuery]);
return (
<div>
<input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
/>
<ul>
{results.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}The user types "react hooks" â without debouncing, that's 11 API calls (one per keystroke). With a 500ms debounce, only one call fires after they stop typing.
Throttling â "Once Per Interval"
Think of it like a news ticker that updates once per second, no matter how many news stories come in. Throttling guarantees the function runs at most once every N milliseconds.
Best for: Scroll handlers, mouse move tracking, window resize, drag events.
import { useRef, useCallback } from "react";
function useThrottle(callback, delay) {
const lastRan = useRef(0);
return useCallback((...args) => {
const now = Date.now();
if (now - lastRan.current >= delay) {
lastRan.current = now;
callback(...args);
}
}, [callback, delay]);
}
function ScrollTracker() {
const handleScroll = useThrottle(() => {
console.log("Scroll position:", window.scrollY);
}, 200);
useEffect(() => {
window.addEventListener("scroll", handleScroll);
return () => window.removeEventListener("scroll", handleScroll);
}, [handleScroll]);
return <div style={{ height: "3000px" }}>Scroll me!</div>;
}Without throttling, scrolling fires the handler hundreds of times per second. With 200ms throttling, it fires at most 5 times per second â smooth and efficient.
Side-by-Side Comparison
| Debounce | Throttle | |
|---|---|---|
| Analogy | Elevator door | News ticker |
| When it fires | After the user STOPS | At regular intervals DURING the action |
| Use case | Search input, form validation | Scroll, resize, drag |
| User types "hello" | Fires once (after typing stops) | Fires ~2â3 times (every N ms during typing) |
Common Mistake: Using throttle when you need debounce and vice versa. Ask yourself: "Do I want to act while they're doing it (throttle) or after they stop (debounce)?"
IntersectionObserver â Detecting Visibility
What Is IntersectionObserver?
Think of it like a security camera at a store entrance â it detects when someone (an element) enters or leaves the visible area (the viewport).
IntersectionObserver is a browser API that tells you when an element becomes visible on screen. No more calculating scroll positions manually.
How Infinite Scroll Uses It
The classic pattern: place an invisible "sentinel" element at the bottom of your list. When the user scrolls it into view, load more data.
import { useState, useEffect, useRef, useCallback } from "react";
function InfiniteList() {
const [items, setItems] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const sentinelRef = useRef(null);
const loadMore = useCallback(() => {
setLoading(true);
fetch(`/api/items?page=${page}`)
.then((res) => res.json())
.then((newItems) => {
setItems((prev) => [...prev, ...newItems]);
setPage((p) => p + 1);
setLoading(false);
});
}, [page]);
useEffect(() => {
const observer = new IntersectionObserver(
(entries) => {
if (entries[0].isIntersecting && !loading) {
loadMore();
}
},
{ threshold: 1.0 }
);
const sentinel = sentinelRef.current;
if (sentinel) observer.observe(sentinel);
return () => {
if (sentinel) observer.unobserve(sentinel);
};
}, [loadMore, loading]);
return (
<div>
{items.map((item) => (
<div key={item.id} className="card">
{item.name}
</div>
))}
<div ref={sentinelRef} style={{ height: "1px" }} />
{loading && <p>Loading more...</p>}
</div>
);
}Other Uses for IntersectionObserver
- Lazy loading images â only load images when they scroll into view
- Animations on scroll â trigger animations when elements become visible
- Analytics â track which sections users actually see
function LazyImage({ src, alt }) {
const [isVisible, setIsVisible] = useState(false);
const imgRef = useRef(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{ rootMargin: "200px" }
);
if (imgRef.current) observer.observe(imgRef.current);
return () => observer.disconnect();
}, []);
return (
<div ref={imgRef}>
{isVisible ? (
<img src={src} alt={alt} />
) : (
<div className="placeholder" style={{ height: "200px", background: "#eee" }} />
)}
</div>
);
}The rootMargin: "200px" starts loading the image 200px before it scrolls into view, so users never see a blank space.
Remember: IntersectionObserver replaces the old pattern of listening to scroll events and calculating positions manually. It's more performant because the browser handles the heavy lifting.
The Event Loop â How JavaScript Handles Async
The Single-Thread Reality
JavaScript runs on a single thread â it can only do one thing at a time. Think of it like a single checkout lane at a grocery store. Customers (tasks) line up and are served one at a time.
So how does JavaScript handle async operations like API calls, timers, and user events without freezing? The event loop.
How the Event Loop Works
1. Call Stack â Where code actually runs (one thing at a time)
2. Web APIs â Browser handles timers, fetch, DOM events separately
3. Task Queue â Callbacks from timers, events wait here
4. Microtask Queue â Promises (.then) wait here (HIGHER priority)
5. Event Loop â Checks: "Is the call stack empty? If so, grab the next task"Here's a classic example:
console.log("1 - Start");
setTimeout(() => {
console.log("2 - Timeout");
}, 0);
Promise.resolve().then(() => {
console.log("3 - Promise");
});
console.log("4 - End");Output:
1 - Start
4 - End
3 - Promise
2 - TimeoutWhy this order?
"1 - Start"runs immediately (call stack)setTimeoutcallback goes to the task queue (even with 0ms delay)Promise.thencallback goes to the microtask queue"4 - End"runs immediately (call stack)- Call stack is empty â microtask queue runs first â
"3 - Promise" - Microtask queue empty â task queue runs â
"2 - Timeout"
Remember: Promises (microtasks) always run before setTimeout (macrotasks). The event loop checks the microtask queue first after every task completes.
requestAnimationFrame â Smooth Animations
Why Not Just Use setTimeout for Animations?
setTimeout doesn't know when the browser is ready to paint the next frame. If you set setTimeout(animate, 16) (aiming for 60fps), it might fire at the wrong time and cause janky, stuttery animations.
requestAnimationFrame tells the browser: "Call this function right before the next screen repaint." The browser decides the perfect timing.
import { useEffect, useRef } from "react";
function SmoothProgressBar() {
const barRef = useRef(null);
const progressRef = useRef(0);
useEffect(() => {
let animationId;
const animate = () => {
progressRef.current += 0.5;
if (barRef.current) {
barRef.current.style.width = `${Math.min(progressRef.current, 100)}%`;
}
if (progressRef.current < 100) {
animationId = requestAnimationFrame(animate);
}
};
animationId = requestAnimationFrame(animate);
return () => cancelAnimationFrame(animationId);
}, []);
return (
<div style={{ width: "100%", height: "20px", background: "#eee", borderRadius: "10px" }}>
<div
ref={barRef}
style={{ height: "100%", background: "#4caf50", borderRadius: "10px", width: "0%" }}
/>
</div>
);
}When to Use requestAnimationFrame
- CSS transitions and animations should use CSS when possible (GPU-accelerated)
requestAnimationFrameis for animations that need JavaScript control â game loops, canvas drawing, physics simulations, custom scroll effects
Common Mistake: Using
setIntervalfor animations. It doesn't sync with the browser's paint cycle, leading to dropped frames and stuttery movement. Always userequestAnimationFramefor visual updates.
Web Workers â Offloading Heavy Computation
The Problem
Remember the single checkout lane? If a customer (task) takes 5 minutes to process, everyone behind them waits. In JavaScript, heavy computation â sorting 100,000 items, processing an image, running complex math â blocks the main thread and freezes the UI.
The Solution: Web Workers
Web Workers are like opening a second checkout lane. They run JavaScript in a separate thread, so heavy work doesn't block the UI.
function HeavyComputationDemo() {
const [result, setResult] = useState(null);
const [calculating, setCalculating] = useState(false);
const runHeavyTask = () => {
setCalculating(true);
const worker = new Worker(
new URL("../workers/heavy-task.js", import.meta.url)
);
worker.postMessage({ numbers: Array.from({ length: 1000000 }, (_, i) => i) });
worker.onmessage = (event) => {
setResult(event.data.total);
setCalculating(false);
worker.terminate();
};
};
return (
<div>
<button onClick={runHeavyTask} disabled={calculating}>
{calculating ? "Calculating..." : "Run Heavy Task"}
</button>
{result !== null && <p>Result: {result}</p>}
<p>Type here to prove the UI isn't frozen:</p>
<input placeholder="Still responsive!" />
</div>
);
}And the worker file (heavy-task.js):
self.onmessage = function (event) {
const { numbers } = event.data;
let total = 0;
for (const num of numbers) {
total += Math.sqrt(num) * Math.sin(num);
}
self.postMessage({ total });
};Key Rules for Web Workers
- Workers cannot access the DOM â they can't touch
documentorwindow - Communication happens through messages (
postMessage/onmessage) - Workers are great for: data processing, image manipulation, sorting large datasets, complex calculations
- Workers add complexity â only use them when computation actually blocks the UI
Remember: Web Workers run in a completely separate thread. They communicate with the main thread through messages, like passing notes between two rooms. The main thread stays responsive while the worker crunches numbers.
Putting It All Together
Here's how all these concepts work together in a real app:
| Concept | Real-World Use |
|---|---|
| Event delegation | React handles this for you â efficient event handling for large lists |
| Debouncing | Search-as-you-type without hammering the API |
| Throttling | Smooth scroll-position tracking for "back to top" button visibility |
| IntersectionObserver | Infinite scroll, lazy-loading images, scroll-triggered animations |
| Event loop | Understanding why your setTimeout(fn, 0) runs after a Promise |
| requestAnimationFrame | Smooth progress bars, custom animations, game loops |
| Web Workers | Processing a large CSV file without freezing the UI |
Interview Corner
What is event delegation and why does React use it? Event delegation attaches one listener to a parent instead of individual listeners to each child. Events bubble up from child to parent. React attaches a single listener to the root element for efficiency.
What's the difference between throttling and debouncing? Debouncing waits until the action stops, then fires once (elevator door). Throttling fires at regular intervals during the action (news ticker). Use debounce for search input, throttle for scroll/resize.
How does IntersectionObserver work? It watches elements and fires a callback when they enter or exit the viewport. It's more efficient than scroll event listeners because the browser handles the visibility calculations internally.
Explain the event loop in simple terms. JavaScript is single-threaded but uses an event loop to handle async work. The call stack runs code, Web APIs handle timers/fetch/events in the background, and finished callbacks wait in queues. The event loop moves callbacks from queues to the call stack when it's empty. Microtasks (Promises) have priority over macrotasks (setTimeout).
When would you use a Web Worker? When you have CPU-intensive work (sorting massive arrays, image processing, complex calculations) that would freeze the UI. Workers run on a separate thread, keeping the main thread responsive.