/** * Render profiler hook for measuring React render performance * * Wraps React's Profiler component to track render count, timing, and performance. * * @example * ```tsx * const { Profiler, metrics, reset } = useRenderProfiler('MyList'); * * return ( * * * * ); * * // Later, check metrics * console.log(`Renders: ${metrics.renderCount}, Avg: ${metrics.avgRenderTime}ms`); * ``` */ import React, { useCallback, useRef, useMemo, type PropsWithChildren, type ProfilerOnRenderCallback } from 'react'; import type { RenderMetrics } from '../types'; import { DEFAULT_RENDER_METRICS } from '../types'; /** * Threshold for "slow" renders in milliseconds * A render taking longer than this would cause a frame drop at 60fps */ const SLOW_RENDER_THRESHOLD_MS = 16.67; export interface UseRenderProfilerResult { /** Profiler component to wrap your content */ Profiler: React.FC; /** Current render metrics */ metrics: RenderMetrics; /** Reset all metrics */ reset: () => void; /** Get a snapshot of current metrics */ getSnapshot: () => RenderMetrics; } /** * Hook for profiling React render performance * * Uses React's built-in Profiler component to collect render metrics. * * @param id - Unique identifier for this profiler instance */ export function useRenderProfiler(id: string): UseRenderProfilerResult { // Use ref to store metrics to avoid re-renders const metricsRef = useRef({ ...DEFAULT_RENDER_METRICS }); // Force update ref for triggering re-renders when needed const updateCounterRef = useRef(0); // Profiler callback const onRenderCallback: ProfilerOnRenderCallback = useCallback( ( _id: string, phase: 'mount' | 'update' | 'nested-update', actualDuration: number, _baseDuration: number, _startTime: number, _commitTime: number, ) => { const current = metricsRef.current; // Update render count current.renderCount += 1; // Track mount time if (phase === 'mount') { current.mountTime = actualDuration; } // Update timing metrics current.lastRenderTime = actualDuration; current.totalRenderTime += actualDuration; current.avgRenderTime = current.totalRenderTime / current.renderCount; current.maxRenderTime = Math.max(current.maxRenderTime, actualDuration); // Track slow renders if (actualDuration > SLOW_RENDER_THRESHOLD_MS) { current.slowRenders += 1; } }, [], ); // Reset metrics const reset = useCallback(() => { metricsRef.current = { ...DEFAULT_RENDER_METRICS }; updateCounterRef.current += 1; }, []); // Get snapshot const getSnapshot = useCallback((): RenderMetrics => { return { ...metricsRef.current }; }, []); // Create Profiler component const Profiler = useMemo(() => { const ProfilerComponent: React.FC = ({ children }) => { return React.createElement( React.Profiler, { id, onRender: onRenderCallback, }, children, ); }; ProfilerComponent.displayName = `RenderProfiler(${id})`; return ProfilerComponent; }, [id, onRenderCallback]); return { Profiler, metrics: metricsRef.current, reset, getSnapshot, }; }