Front-end Engineering Lab

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 render

Programmatic 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

  1. Profile before optimizing - Measure actual impact
  2. Use React.memo wisely - Not for everything
  3. Memoize expensive computations - useMemo for heavy calc
  4. Stable callbacks - useCallback for child props
  5. Split large components - Smaller = easier to optimize
  6. Virtual scrolling - For long lists
  7. Lazy loading - Code split components
  8. Key stability - Use stable keys in lists

Render profiling reveals performance issues—optimize strategically, not prematurely!

On this page