PatternsObservability
Render Profiling (React Profiler)
Optimize React component renders with profiling
React Profiler helps identify unnecessary re-renders and optimize component performance.
React DevTools Profiler
1. Install React DevTools extension
2. Open DevTools → Profiler tab
3. Click Record
4. Interact with app
5. Stop recording
6. Analyze flame chart
Colors:
- Yellow: Fast render
- Orange: Moderate
- Red: Slow renderProgrammatic Profiling
import { Profiler, ProfilerOnRenderCallback } from 'react';
const onRenderCallback: ProfilerOnRenderCallback = (
id, // Component ID
phase, // "mount" or "update"
actualDuration, // Time spent rendering
baseDuration, // Estimated time without memoization
startTime, // When render started
commitTime // When changes committed
) => {
// Log slow renders
if (actualDuration > 16) {
console.warn(
`Slow render: ${id} (${phase}) - ${actualDuration.toFixed(2)}ms`
);
}
// Track metrics
logMetric({
component: id,
phase,
actualDuration,
baseDuration,
});
};
export function App() {
return (
<Profiler id="App" onRender={onRenderCallback}>
<Header />
<Main />
<Footer />
</Profiler>
);
}Detecting Unnecessary Renders
// Hook to detect renders
export function useRenderCount(componentName: string) {
const renders = useRef(0);
useEffect(() => {
renders.current++;
console.log(`${componentName} rendered ${renders.current} times`);
});
}
// Hook to detect why component rendered
export function useWhyDidYouUpdate(name: string, props: any) {
const previousProps = useRef<any>();
useEffect(() => {
if (previousProps.current) {
const allKeys = Object.keys({ ...previousProps.current, ...props });
const changedProps: any = {};
allKeys.forEach((key) => {
if (previousProps.current[key] !== props[key]) {
changedProps[key] = {
from: previousProps.current[key],
to: props[key],
};
}
});
if (Object.keys(changedProps).length > 0) {
console.log('[why-did-you-update]', name, changedProps);
}
}
previousProps.current = props;
});
}
// Usage
function Component(props: Props) {
useWhyDidYouUpdate('MyComponent', props);
return <div>{/* ... */}</div>;
}Optimization Techniques
React.memo
// ❌ Re-renders on every parent render
function ExpensiveComponent({ data }: Props) {
return <div>{processData(data)}</div>;
}
// ✅ Only re-renders when props change
const ExpensiveComponent = React.memo(
function ExpensiveComponent({ data }: Props) {
return <div>{processData(data)}</div>;
},
(prevProps, nextProps) => {
// Return true if equal (skip render)
return prevProps.data.id === nextProps.data.id;
}
);useMemo
// ❌ Recalculates on every render
function Component({ items }: Props) {
const filtered = items.filter(item => item.active);
const sorted = filtered.sort((a, b) => a.name.localeCompare(b.name));
return <List items={sorted} />;
}
// ✅ Only recalculates when items change
function Component({ items }: Props) {
const sorted = useMemo(() => {
const filtered = items.filter(item => item.active);
return filtered.sort((a, b) => a.name.localeCompare(b.name));
}, [items]);
return <List items={sorted} />;
}useCallback
// ❌ Creates new function on every render
function Component() {
const handleClick = () => {
console.log('clicked');
};
return <ExpensiveChild onClick={handleClick} />;
}
// ✅ Stable function reference
function Component() {
const handleClick = useCallback(() => {
console.log('clicked');
}, []);
return <ExpensiveChild onClick={handleClick} />;
}Performance Monitoring Hook
export function usePerformanceMonitor(componentName: string) {
const renderCount = useRef(0);
const renderTimes = useRef<number[]>([]);
const renderStart = useRef(0);
renderCount.current++;
renderStart.current = performance.now();
useEffect(() => {
const renderTime = performance.now() - renderStart.current;
renderTimes.current.push(renderTime);
// Log stats every 10 renders
if (renderCount.current % 10 === 0) {
const avg = renderTimes.current.reduce((a, b) => a + b, 0) / renderTimes.current.length;
const max = Math.max(...renderTimes.current);
console.log(`${componentName} Performance:`, {
renders: renderCount.current,
avgTime: avg.toFixed(2),
maxTime: max.toFixed(2),
});
// Reset after reporting
renderTimes.current = [];
}
});
}Best Practices
- Profile before optimizing - Measure actual impact
- Use React.memo wisely - Not for everything
- Memoize expensive computations - useMemo for heavy calc
- Stable callbacks - useCallback for child props
- Split large components - Smaller = easier to optimize
- Virtual scrolling - For long lists
- Lazy loading - Code split components
- Key stability - Use stable keys in lists
Render profiling reveals performance issues—optimize strategically, not prematurely!