import { throttle } from '@codeleap/utils' import { useEffect } from 'react' import { PerformanceError } from './errors' import { InspectRenderOptions } from './types' import { LoggerConfig } from '../../types' export * from './types' /** * Dev-only performance utilities. All methods are no-ops in production * (`config.Environment.IsDev === false`) or when * `config.Logger.performanceInspector.enabled` is `false`. */ export class PerformanceService { renderCounter: Record = {} constructor(private config: LoggerConfig) { } /** * Tracks render frequency for a component and warns when it exceeds the configured threshold. * * Call this at the top of a function component body (not inside a hook). By default it also * registers mount/unmount effects via `useEffect` — pass `noHooks: true` to skip those when * the component cannot accept additional hooks. * * When the accumulated render count within a `throttleInterval` window exceeds `maxRenders`, * the counter resets and a {@link PerformanceError} is thrown (surfacing as a React error * boundary hit rather than a silent warning). * * Components whose names start with any entry in * `config.Logger.performanceInspector.blacklist` are silently skipped. * * Has no effect outside a dev environment or when the inspector is disabled in config. */ inspectRender = ( name: string, options: InspectRenderOptions = { noHooks: false, logMode: 'summarized', throttleInterval: 1000, }, ) => { const config = this.config.Logger.performanceInspector const blacklist = config.blacklist || [] if (blacklist.some((item) => name.startsWith(item))) return const { noHooks, logMode, throttleInterval, maxRenders = config.maxRenders } = options if (!config.enabled || !this.config.Environment.IsDev) { return } if (!noHooks) { useEffect(() => { console.log(`[PerformanceInspector] Mounted -> ${name}`) return () => { console.log(`[PerformanceInspector] Unmounted -> ${name}`) } }) } this.renderCounter[name] = this.renderCounter[name] ? this.renderCounter[name] + 1 : 1 const renders = this.renderCounter[name] if (renders > maxRenders) { this.renderCounter[name] = 0 throw new PerformanceError('maxRenders', { name, throttleInterval, maxRenders, }) } if (logMode === 'raw') { console.log(`[PerformanceInspector] Rendered -> ${name}: ${renders}`) return } function logSummary(this: PerformanceService) { if (renders <= 0) return console.log(`[PerformanceInspector] Render summary -> ${name}: ${renders}`) this.renderCounter[name] = 0 } // @ts-ignore throttle(logSummary, name, throttleInterval) } }