/**
* 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,
};
}