React DevTools Profiler
The React Profiler (available in the React DevTools browser extension) records renders and shows you exactly which components rendered, how long they took, and why.
Recording a Profile
- Open React DevTools and switch to the Profiler tab.
- Click Record.
- Interact with your app (click buttons, type, navigate).
- Click Stop.
The flamegraph shows each component's render time. Gray bars mean the component did not render. Colored bars show render duration — yellow/red indicates slow renders.
Programmatic Profiler
React also provides a <Profiler> component for measuring renders in code:
import { Profiler, ProfilerOnRenderCallback } from "react";
const onRender: ProfilerOnRenderCallback = (
id, // the "id" prop of the Profiler tree
phase, // "mount" | "update" | "nested-update"
actualDuration, // time spent rendering the committed update
baseDuration, // estimated time to render the entire subtree without memoization
startTime,
commitTime
) => {
console.log(`${id} [${phase}]: ${actualDuration.toFixed(2)}ms`);
};
function App() {
return (
<Profiler id="Dashboard" onRender={onRender}>
<Dashboard />
</Profiler>
);
}Use baseDuration vs actualDuration to see how much memoization is saving you. If they're close, your memoization isn't doing much.
Sending Profiling Data to Analytics
In production, you can send profiling data to your analytics service:
const onRender: ProfilerOnRenderCallback = (
id,
phase,
actualDuration
) => {
// Only report slow renders
if (actualDuration > 16) {
analytics.track("slow_render", {
component: id,
phase,
duration: Math.round(actualDuration),
});
}
};Chrome DevTools Performance Tab
For lower-level analysis, Chrome's Performance tab shows the full picture — JavaScript execution, layout, paint, and compositing.
Key Things to Look For
- Long Tasks (red triangles) — any task over 50ms blocks the main thread.
- Layout Thrashing — repeated forced reflows from reading layout properties after writes.
- Excessive Paints — large areas being repainted on each frame.
// BAD: Layout thrashing — reads and writes interleaved
function resizeAll(elements: HTMLElement[]) {
elements.forEach((el) => {
const height = el.offsetHeight; // READ (forces layout)
el.style.height = `${height * 2}px`; // WRITE (invalidates layout)
});
}
// GOOD: Batch reads, then batch writes
function resizeAll(elements: HTMLElement[]) {
const heights = elements.map((el) => el.offsetHeight); // all READs
elements.forEach((el, i) => {
el.style.height = `${heights[i] * 2}px`; // all WRITEs
});
}Lighthouse Performance Audit
Lighthouse measures real-world performance metrics. Run it from Chrome DevTools > Lighthouse tab, or via CLI:
// Key metrics to focus on:
// - LCP (Largest Contentful Paint): < 2.5s
// - INP (Interaction to Next Paint): < 200ms
// - CLS (Cumulative Layout Shift): < 0.1
// Common React-specific issues Lighthouse catches:
// 1. Large JavaScript bundles → use code splitting
// 2. Unused JavaScript → tree-shake or lazy load
// 3. Render-blocking resources → defer non-critical scripts
// 4. Layout shifts → set explicit dimensions on images/embeds
Fixing Common Issues
// Fix CLS: Always set width and height on images
function ProductImage({ src, alt }: { src: string; alt: string }) {
return (
<img
src={src}
alt={alt}
width={400}
height={300}
className="object-cover rounded"
loading="lazy" // Fix LCP for below-fold images
/>
);
}
// Fix LCP: Preload critical images
// In Next.js, use priority prop on above-fold images
import Image from "next/image";
function Hero() {
return (
<Image
src="/hero.jpg"
alt="Hero image"
width={1200}
height={600}
priority // Preloads the image, no lazy loading
/>
);
}Identifying Common Bottlenecks
1. Unnecessary Re-renders
Use React DevTools' "Highlight updates when components render" setting to visually spot components that re-render too often.
// Symptom: Component flashes on every parent render
// Fix: Check if React.memo or state colocation helps
// Before
function ParentWithTimer() {
const [time, setTime] = useState(Date.now());
useEffect(() => {
const id = setInterval(() => setTime(Date.now()), 1000);
return () => clearInterval(id);
}, []);
return (
<div>
<Clock time={time} />
<ExpensiveChild /> {/* re-renders every second! */}
</div>
);
}
// After: Isolate the timer
function ParentWithTimer() {
return (
<div>
<ClockSection /> {/* timer state lives here */}
<ExpensiveChild /> {/* no longer affected */}
</div>
);
}
function ClockSection() {
const [time, setTime] = useState(Date.now());
useEffect(() => {
const id = setInterval(() => setTime(Date.now()), 1000);
return () => clearInterval(id);
}, []);
return <Clock time={time} />;
}
2. Expensive List Rendering
// Symptom: Scrolling or filtering a large list is janky
// Fix: Virtualize with react-window or tanstack-virtual
import { FixedSizeList } from "react-window";
function VirtualizedList({ items }: { items: Item[] }) {
return (
<FixedSizeList
height={600}
width="100%"
itemCount={items.length}
itemSize={60}
>
{({ index, style }) => (
<div style={style} className="flex items-center px-4">
{items[index].name}
</div>
)}
</FixedSizeList>
);
}3. Debouncing Rapid Updates
// Symptom: Search input filtering is slow
// Fix: Debounce the filter, or use useTransition
import { useTransition } from "react";
function SearchableList({ items }: { items: Item[] }) {
const [query, setQuery] = useState("");
const [filteredItems, setFilteredItems] = useState(items);
const [isPending, startTransition] = useTransition();
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
const value = e.target.value;
setQuery(value); // urgent: update input immediately
startTransition(() => {
// non-urgent: filter can be deferred
setFilteredItems(
items.filter((item) =>
item.name.toLowerCase().includes(value.toLowerCase())
)
);
});
}
return (
<div>
<input value={query} onChange={handleChange} />
{isPending && <span className="text-gray-400">Filtering...</span>}
<ul>
{filteredItems.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
</div>
);
}Performance Checklist
Before shipping, verify:
- No components re-render unnecessarily (check with React Profiler)
- Large lists are virtualized
- Heavy components are code-split
- Images have explicit dimensions and use lazy loading
- LCP is under 2.5s, INP under 200ms, CLS under 0.1
- No layout thrashing in scroll or resize handlers
- State is colocated — not lifted higher than needed