React JavaScript Performance Optimization
Optimizes JavaScript execution in React by profiling main thread activity, debouncing high-frequency events, offloading CPU-heavy work to Web Workers, and minimizing DOM operations. Keeps the UI responsive under the RAIL model (50ms task budget).
TL;DR Checklist
When to Use
Use this skill when:
- Implementing scroll, resize, or touch event handlers with high fire rates
- Building components that parse large data sets on the main thread
- Identifying janky UI (dropped frames, delayed input response)
- Profiling and optimizing React render performance
- Adding animation-heavy interactions that compete with JS execution
- Debugging main thread tasks exceeding the 50ms RAIL budget
When NOT to Use
Avoid this skill for:
- Network optimization (API call reduction, caching) — use
react-server-performance instead
- Bundle size or code splitting concerns — use
react-bundle-size instead
- React re-render optimization (useMemo, useCallback, React.memo) — use
react-rerender-optimization instead
- CSS-level animations that don't involve JS — CSS handles these without JS intervention
Core Workflow
Profile JS Execution — Open Chrome DevTools Performance tab or instrument with performance.now() to capture task durations.
Checkpoint: Identify all long tasks (>50ms) and high-frequency event handlers.
Identify Optimization Targets — Look for:
- Event handlers firing >30 times/second (scroll, resize, mousemove)
- Synchronous CPU work >50ms (parsing, computation, DOM manipulation)
- Forced synchronous layouts (DOM read after write within same frame)
Apply Debouncing/Throttling — Debounce input and resize handlers; throttle scroll and animation handlers.
Offload to Web Workers — Move parsing, crypto, data transforms, and any task >50ms to a dedicated Worker thread.
Optimize Event Handling — Use passive listeners and event delegation to reduce handler overhead.
Verify — Re-profile to confirm all main thread tasks stay under 50ms and frame rate is stable at 60fps.
Implementation Patterns
Pattern 1: Debouncing High-Frequency Events
// ✅ GOOD: Debounced input handler with configurable delay
function useDebouncedCallback<T extends (...args: unknown[]) => void>(
callback: T,
delay: number = 300
): T {
const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
useEffect(() => {
return () => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
};
}, []);
return useCallback(
((...args: unknown[]) => {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => callback(...args), delay);
}) as T,
[callback, delay]
);
}
// Usage in a search component
function SearchBox() {
const [query, setQuery] = useState('');
const debouncedSearch = useDebouncedCallback(
(value: string) => performSearch(value),
350
);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
debouncedSearch(e.target.value);
};
return <input type="text" value={query} />;
}
Pattern 2: Throttling vs. Debouncing (BAD vs. GOOD)
// ❌ BAD: No throttling — fires hundreds of times during scroll
function ScrollSpy() {
const [scrollY, setScrollY] = useState(0);
useEffect(() => {
const handleScroll = () => {
setScrollY(window.scrollY);
// Expensive layout calculation on every frame
updateActiveSection();
};
window.addEventListener('scroll', handleScroll);
return () => window.removeEventListener('scroll', handleScroll);
}, []);
return <div>{/* ... */}</div>;
}
// ✅ GOOD: Throttled scroll handler with RAF synchronization
function ScrollSpy() {
const [scrollY, setScrollY] = useState(0);
const rafRef = useRef<number | null>(null);
useEffect(() => {
const handleScroll = () => {
if (rafRef.current) return; // Skip if RAF already queued
rafRef.current = requestAnimationFrame(() => {
setScrollY(window.scrollY);
updateActiveSection();
rafRef.current = null;
});
};
window.addEventListener('scroll', handleScroll, { passive: true });
return () => {
window.removeEventListener('scroll', handleScroll);
if (rafRef.current) cancelAnimationFrame(rafRef.current);
};
}, []);
return <div>{/* ... */}</div>;
}
Pattern 3: Web Worker Offloading
// ✅ GOOD: Offload expensive computation to a Web Worker
function useWorker<TInput, TOutput>(
workerFactory: () => Worker,
input: TInput | null
): { result: TOutput | null; error: Error | null; isProcessing: boolean } {
const [result, setResult] = useState<TOutput | null>(null);
const [error, setError] = useState<Error | null>(null);
const [isProcessing, setIsProcessing] = useState(false);
const workerRef = useRef<Worker | null>(null);
useEffect(() => {
workerRef.current = workerFactory();
workerRef.current.onmessage = (e: MessageEvent<TOutput>) => {
setResult(e.data);
setIsProcessing(false);
};
workerRef.current.onerror = (e: ErrorEvent) => {
setError(new Error(e.message));
setIsProcessing(false);
};
return () => {
workerRef.current?.terminate();
};
}, [workerFactory]);
useEffect(() => {
if (input === null) return;
setIsProcessing(true);
workerRef.current?.postMessage(input);
}, [input]);
return { result, error, isProcessing };
}
// worker.ts — runs off the main thread
self.onmessage = (e: MessageEvent<ArrayBuffer>) => {
const parsed = parseComplexData(e.data); // CPU-heavy work
self.postMessage(parsed);
};
Pattern 4: Event Delegation (BAD vs. GOOD)
// ❌ BAD: Attaching listener to every list item — O(n) handlers
function ItemList({ items }: { items: Item[] }) {
return (
<ul>
{items.map((item) => (
<li key={item.id} => handleItemClick(item.id)}>
{item.name}
</li>
))}
</ul>
);
}
// ✅ GOOD: Single delegated listener on the parent — O(1) handler
function ItemList({ items }: { items: Item[] }) {
const handleClick = useCallback(
(e: React.MouseEvent<HTMLUListElement>) => {
const li = (e.target as HTMLElement).closest('li');
if (!li?.dataset?.id) return;
handleItemClick(li.dataset.id);
},
[]
);
return (
<ul
{items.map((item) => (
<li key={item.id} data-id={item.id}>
{item.name}
</li>
))}
</ul>
);
}
Constraints
MUST DO
- Use
{ passive: true } for scroll, touch, and wheel event listeners
- Debounce input handlers with 250-400ms delay
- Keep main thread tasks under 50ms (RAIL model)
- Use Web Workers for CPU-heavy operations (parsing, crypto, data transforms)
- Prefer CSS animations (transform, opacity) over JS-driven animation
- Use
will-change sparingly on elements that actually animate
- Batch DOM reads before writes using requestAnimationFrame
MUST NOT DO
- Block the main thread for more than 50ms at a time
- Use JavaScript for animations that CSS can handle (transforms, opacity)
- Attach individual event listeners to every list item — use event delegation
- Add
will-change to too many elements (consumes GPU memory)
- Create new function or object references in event handler closures
- Read then write DOM properties in the same frame (forces synchronous layout)
Related Skills
| Skill |
Purpose |
react-rerender-optimization |
Reduce unnecessary re-renders with memo, useMemo, useCallback |
react-bundle-size |
Code splitting, tree shaking, and bundle analysis |
react-server-performance |
Server-side rendering and streaming optimization |
react-composition-patterns |
Component composition patterns for efficient rendering |
Live References
Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.
1---2name: react-js-performance3description: Optimizes JavaScript execution performance in React applications through efficient event handling, debouncing/throttling, Web Worker offloading, and DOM operation reduction.4license: MIT5---67# React JavaScript Performance Optimization89Optimizes JavaScript execution in React by profiling main thread activity, debouncing high-frequency events, offloading CPU-heavy work to Web Workers, and minimizing DOM operations. Keeps the UI responsive under the RAIL model (50ms task budget).1011## TL;DR Checklist1213- [ ] Profile with performance.now() and Chrome DevTools Performance tab before optimizing14- [ ] Debounce scroll/resize/input handlers with 250-400ms delay15- [ ] Throttle requestAnimationFrame-bound animations16- [ ] Offload parsing, crypto, and data transforms to Web Workers17- [ ] Use passive event listeners for scroll/touch/wheel events18- [ ] Batch DOM reads before writes using requestAnimationFrame19- [ ] Verify no main thread task exceeds 50ms2021---2223## When to Use2425Use this skill when:2627- Implementing scroll, resize, or touch event handlers with high fire rates28- Building components that parse large data sets on the main thread29- Identifying janky UI (dropped frames, delayed input response)30- Profiling and optimizing React render performance31- Adding animation-heavy interactions that compete with JS execution32- Debugging main thread tasks exceeding the 50ms RAIL budget3334---3536## When NOT to Use3738Avoid this skill for:3940- Network optimization (API call reduction, caching) — use `react-server-performance` instead41- Bundle size or code splitting concerns — use `react-bundle-size` instead42- React re-render optimization (useMemo, useCallback, React.memo) — use `react-rerender-optimization` instead43- CSS-level animations that don't involve JS — CSS handles these without JS intervention4445---4647## Core Workflow48491. **Profile JS Execution** — Open Chrome DevTools Performance tab or instrument with `performance.now()` to capture task durations.50 **Checkpoint:** Identify all long tasks (>50ms) and high-frequency event handlers.51522. **Identify Optimization Targets** — Look for:53 - Event handlers firing >30 times/second (scroll, resize, mousemove)54 - Synchronous CPU work >50ms (parsing, computation, DOM manipulation)55 - Forced synchronous layouts (DOM read after write within same frame)56573. **Apply Debouncing/Throttling** — Debounce input and resize handlers; throttle scroll and animation handlers.58594. **Offload to Web Workers** — Move parsing, crypto, data transforms, and any task >50ms to a dedicated Worker thread.60615. **Optimize Event Handling** — Use passive listeners and event delegation to reduce handler overhead.62636. **Verify** — Re-profile to confirm all main thread tasks stay under 50ms and frame rate is stable at 60fps.6465---6667## Implementation Patterns6869### Pattern 1: Debouncing High-Frequency Events7071```typescript72// ✅ GOOD: Debounced input handler with configurable delay73function useDebouncedCallback<T extends (...args: unknown[]) => void>(74 callback: T,75 delay: number = 30076): T {77 const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);7879 useEffect(() => {80 return () => {81 if (timeoutRef.current) clearTimeout(timeoutRef.current);82 };83 }, []);8485 return useCallback(86 ((...args: unknown[]) => {87 if (timeoutRef.current) clearTimeout(timeoutRef.current);88 timeoutRef.current = setTimeout(() => callback(...args), delay);89 }) as T,90 [callback, delay]91 );92}9394// Usage in a search component95function SearchBox() {96 const [query, setQuery] = useState('');97 const debouncedSearch = useDebouncedCallback(98 (value: string) => performSearch(value),99 350100 );101102 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {103 setQuery(e.target.value);104 debouncedSearch(e.target.value);105 };106107 return <input type="text" value={query} onChange={handleChange} />;108}109```110111### Pattern 2: Throttling vs. Debouncing (BAD vs. GOOD)112113```typescript114// ❌ BAD: No throttling — fires hundreds of times during scroll115function ScrollSpy() {116 const [scrollY, setScrollY] = useState(0);117118 useEffect(() => {119 const handleScroll = () => {120 setScrollY(window.scrollY);121 // Expensive layout calculation on every frame122 updateActiveSection();123 };124 window.addEventListener('scroll', handleScroll);125 return () => window.removeEventListener('scroll', handleScroll);126 }, []);127128 return <div>{/* ... */}</div>;129}130131// ✅ GOOD: Throttled scroll handler with RAF synchronization132function ScrollSpy() {133 const [scrollY, setScrollY] = useState(0);134 const rafRef = useRef<number | null>(null);135136 useEffect(() => {137 const handleScroll = () => {138 if (rafRef.current) return; // Skip if RAF already queued139 rafRef.current = requestAnimationFrame(() => {140 setScrollY(window.scrollY);141 updateActiveSection();142 rafRef.current = null;143 });144 };145146 window.addEventListener('scroll', handleScroll, { passive: true });147 return () => {148 window.removeEventListener('scroll', handleScroll);149 if (rafRef.current) cancelAnimationFrame(rafRef.current);150 };151 }, []);152153 return <div>{/* ... */}</div>;154}155```156157### Pattern 3: Web Worker Offloading158159```typescript160// ✅ GOOD: Offload expensive computation to a Web Worker161function useWorker<TInput, TOutput>(162 workerFactory: () => Worker,163 input: TInput | null164): { result: TOutput | null; error: Error | null; isProcessing: boolean } {165 const [result, setResult] = useState<TOutput | null>(null);166 const [error, setError] = useState<Error | null>(null);167 const [isProcessing, setIsProcessing] = useState(false);168 const workerRef = useRef<Worker | null>(null);169170 useEffect(() => {171 workerRef.current = workerFactory();172173 workerRef.current.onmessage = (e: MessageEvent<TOutput>) => {174 setResult(e.data);175 setIsProcessing(false);176 };177178 workerRef.current.onerror = (e: ErrorEvent) => {179 setError(new Error(e.message));180 setIsProcessing(false);181 };182183 return () => {184 workerRef.current?.terminate();185 };186 }, [workerFactory]);187188 useEffect(() => {189 if (input === null) return;190 setIsProcessing(true);191 workerRef.current?.postMessage(input);192 }, [input]);193194 return { result, error, isProcessing };195}196197// worker.ts — runs off the main thread198self.onmessage = (e: MessageEvent<ArrayBuffer>) => {199 const parsed = parseComplexData(e.data); // CPU-heavy work200 self.postMessage(parsed);201};202```203204### Pattern 4: Event Delegation (BAD vs. GOOD)205206```typescript207// ❌ BAD: Attaching listener to every list item — O(n) handlers208function ItemList({ items }: { items: Item[] }) {209 return (210 <ul>211 {items.map((item) => (212 <li key={item.id} onClick={() => handleItemClick(item.id)}>213 {item.name}214 </li>215 ))}216 </ul>217 );218}219220// ✅ GOOD: Single delegated listener on the parent — O(1) handler221function ItemList({ items }: { items: Item[] }) {222 const handleClick = useCallback(223 (e: React.MouseEvent<HTMLUListElement>) => {224 const li = (e.target as HTMLElement).closest('li');225 if (!li?.dataset?.id) return;226 handleItemClick(li.dataset.id);227 },228 []229 );230231 return (232 <ul onClick={handleClick}>233 {items.map((item) => (234 <li key={item.id} data-id={item.id}>235 {item.name}236 </li>237 ))}238 </ul>239 );240}241```242243---244245## Constraints246247### MUST DO248- Use `{ passive: true }` for scroll, touch, and wheel event listeners249- Debounce input handlers with 250-400ms delay250- Keep main thread tasks under 50ms (RAIL model)251- Use Web Workers for CPU-heavy operations (parsing, crypto, data transforms)252- Prefer CSS animations (transform, opacity) over JS-driven animation253- Use `will-change` sparingly on elements that actually animate254- Batch DOM reads before writes using requestAnimationFrame255256### MUST NOT DO257- Block the main thread for more than 50ms at a time258- Use JavaScript for animations that CSS can handle (transforms, opacity)259- Attach individual event listeners to every list item — use event delegation260- Add `will-change` to too many elements (consumes GPU memory)261- Create new function or object references in event handler closures262- Read then write DOM properties in the same frame (forces synchronous layout)263264---265266## Related Skills267268| Skill | Purpose |269|---|---|270| `react-rerender-optimization` | Reduce unnecessary re-renders with memo, useMemo, useCallback |271| `react-bundle-size` | Code splitting, tree shaking, and bundle analysis |272| `react-server-performance` | Server-side rendering and streaming optimization |273| `react-composition-patterns` | Component composition patterns for efficient rendering |274275---276277## Live References278279> Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.280281- [React Performance Optimization Guide](https://react.dev/learn/render-and-commit)282- [MDN: Event dispatch and passive listeners](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#using_passive_listeners)283- [Web Workers API (MDN)](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API)284- [RAIL Model — Google Web Fundamentals](https://web.dev/articles/rail)285- [MDN: requestAnimationFrame](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame)286- [Chrome DevTools Performance Reference](https://developer.chrome.com/docs/devtools/performance/reference)287- [CSS will-change Property (MDN)](https://developer.mozilla.org/en-US/docs/Web/CSS/will-change)