{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "web-vitals", "title": "Web Vitals", "description": "A headless, lazily loaded Core Web Vitals collector with callback and browser-event reporting", "dependencies": ["web-vitals"], "files": [ { "path": "registry/default/ui/web-vitals.tsx", "content": "'use client';\n\nimport { useEffect, useRef } from 'react';\nimport type { MetricType, MetricWithAttribution, ReportOpts } from 'web-vitals';\n\nexport const WEB_VITAL_EVENT = 'wener:web-vital';\n\nexport type WebVitalMetric = MetricType | MetricWithAttribution;\nexport type WebVitalsReporter = (metric: WebVitalMetric) => void;\nexport type WebVitalsLoadStrategy = 'idle' | 'immediate';\n\nexport type UseReportWebVitalsOptions = {\n\tenabled?: boolean;\n\t/** Collector identity options are captured when this hook is first enabled. */\n\tattribution?: boolean;\n\treportAllChanges?: boolean;\n\tloadStrategy?: WebVitalsLoadStrategy;\n\tidleTimeout?: number;\n\t/** Delay before retrying a failed dynamic import. Set false to disable retries. */\n\tretryDelay?: number | false;\n\tonError?: (error: unknown) => void;\n};\n\nexport type WebVitalsProps = UseReportWebVitalsOptions & {\n\tonMetric?: WebVitalsReporter;\n};\n\nexport function WebVitals({ onMetric = dispatchWebVital, onError = reportWebVitalsError, ...options }: WebVitalsProps) {\n\tuseReportWebVitals(onMetric, { ...options, onError });\n\treturn null;\n}\n\nexport function useReportWebVitals(\n\tonMetric: WebVitalsReporter,\n\t{\n\t\tenabled = true,\n\t\tattribution = false,\n\t\treportAllChanges = false,\n\t\tloadStrategy = 'idle',\n\t\tidleTimeout = 2000,\n\t\tretryDelay = 5000,\n\t\tonError,\n\t}: UseReportWebVitalsOptions = {},\n) {\n\tconst reporterRef = useRef(onMetric);\n\tconst errorRef = useRef(onError);\n\tconst collectorOptionsRef = useRef(undefined);\n\treporterRef.current = onMetric;\n\terrorRef.current = onError;\n\tif (enabled && !collectorOptionsRef.current) collectorOptionsRef.current = { attribution, reportAllChanges };\n\n\tuseEffect(() => {\n\t\tconst collectorOptions = collectorOptionsRef.current;\n\t\tif (!enabled || typeof window === 'undefined' || !collectorOptions) return;\n\t\tlet unsubscribe: (() => void) | undefined;\n\t\tconst cancelSchedule = scheduleWebVitalsCollection(\n\t\t\t() => {\n\t\t\t\tunsubscribe = subscribeWebVitalsCollector(collectorOptions, {\n\t\t\t\t\tonMetric: (metric) => reporterRef.current(metric),\n\t\t\t\t\tonError: (error) => errorRef.current?.(error),\n\t\t\t\t\tretryDelay,\n\t\t\t\t});\n\t\t\t},\n\t\t\tloadStrategy,\n\t\t\tidleTimeout,\n\t\t);\n\t\treturn () => {\n\t\t\tcancelSchedule();\n\t\t\tunsubscribe?.();\n\t\t};\n\t}, [enabled, idleTimeout, loadStrategy, retryDelay]);\n}\n\nexport function dispatchWebVital(metric: WebVitalMetric) {\n\tif (typeof window === 'undefined') return;\n\twindow.dispatchEvent(new CustomEvent(WEB_VITAL_EVENT, { detail: metric }));\n}\n\ntype CollectorOptions = {\n\tattribution: boolean;\n\treportAllChanges: boolean;\n};\n\ntype CollectorSubscriber = {\n\tonMetric: WebVitalsReporter;\n\tonError: (error: unknown) => void;\n\tretryDelay: number | false;\n};\n\ntype WebVitalsModule = {\n\tonCLS: (reporter: WebVitalsReporter, options?: ReportOpts) => void;\n\tonFCP: (reporter: WebVitalsReporter, options?: ReportOpts) => void;\n\tonINP: (reporter: WebVitalsReporter, options?: ReportOpts) => void;\n\tonLCP: (reporter: WebVitalsReporter, options?: ReportOpts) => void;\n\tonTTFB: (reporter: WebVitalsReporter, options?: ReportOpts) => void;\n};\n\ntype WebVitalsCollector = {\n\tinitialized: boolean;\n\tloading?: Promise;\n\tretryAttempt?: number;\n\tretryTimer?: ReturnType;\n\tsubscribers: Set;\n};\n\nconst collectors = new Map();\n\nfunction subscribeWebVitalsCollector(options: CollectorOptions, subscriber: CollectorSubscriber) {\n\tconst key = `${options.attribution ? 'attribution' : 'standard'}:${options.reportAllChanges ? 'changes' : 'final'}`;\n\tlet collector = collectors.get(key);\n\tif (!collector) {\n\t\tcollector = { initialized: false, subscribers: new Set() };\n\t\tcollectors.set(key, collector);\n\t}\n\tcollector.subscribers.add(subscriber);\n\tif (collector.retryTimer) {\n\t\tif (subscriber.retryDelay !== false) rescheduleCollectorRetry(collector, options);\n\t} else {\n\t\tstartWebVitalsCollector(collector, options);\n\t}\n\treturn () => {\n\t\tcollector.subscribers.delete(subscriber);\n\t\tif (collector.retryTimer && subscriber.retryDelay !== false) rescheduleCollectorRetry(collector, options);\n\t};\n}\n\nfunction startWebVitalsCollector(collector: WebVitalsCollector, options: CollectorOptions) {\n\tif (collector.initialized || collector.loading || collector.retryTimer) return;\n\tcollector.loading = loadWebVitalsModule(options.attribution)\n\t\t.then((module) => {\n\t\t\tcollector.loading = undefined;\n\t\t\tif (collector.subscribers.size === 0) return;\n\t\t\tcollector.initialized = true;\n\t\t\tcollector.retryAttempt = 0;\n\t\t\tconst report: WebVitalsReporter = (metric) => {\n\t\t\t\tfor (const subscriber of collector.subscribers) {\n\t\t\t\t\ttry {\n\t\t\t\t\t\tsubscriber.onMetric(metric);\n\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\tnotifySubscriberError(subscriber, error);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t};\n\t\t\tconst reportOptions: ReportOpts = { reportAllChanges: options.reportAllChanges };\n\t\t\tmodule.onCLS(report, reportOptions);\n\t\t\tmodule.onFCP(report, reportOptions);\n\t\t\tmodule.onINP(report, reportOptions);\n\t\t\tmodule.onLCP(report, reportOptions);\n\t\t\tmodule.onTTFB(report, reportOptions);\n\t\t})\n\t\t.catch((error) => {\n\t\t\tcollector.loading = undefined;\n\t\t\tfor (const subscriber of collector.subscribers) notifySubscriberError(subscriber, error);\n\t\t\tscheduleCollectorRetry(collector, options);\n\t\t});\n}\n\nfunction scheduleCollectorRetry(collector: WebVitalsCollector, options: CollectorOptions) {\n\tconst delays = Array.from(collector.subscribers)\n\t\t.map((subscriber) => subscriber.retryDelay)\n\t\t.filter((delay): delay is number => delay !== false);\n\tif (delays.length === 0) return;\n\tconst attempt = collector.retryAttempt ?? 0;\n\tconst baseDelay = Math.min(...delays.map((delay) => Math.max(250, nonNegativeOr(delay, 5000))));\n\tconst delay = Math.min(baseDelay * 2 ** attempt, 60_000);\n\tcollector.retryTimer = setTimeout(() => {\n\t\tcollector.retryTimer = undefined;\n\t\tcollector.retryAttempt = attempt + 1;\n\t\tif (hasRetrySubscriber(collector)) startWebVitalsCollector(collector, options);\n\t}, delay);\n}\n\nfunction rescheduleCollectorRetry(collector: WebVitalsCollector, options: CollectorOptions) {\n\tif (collector.retryTimer) clearTimeout(collector.retryTimer);\n\tcollector.retryTimer = undefined;\n\tif (hasRetrySubscriber(collector)) scheduleCollectorRetry(collector, options);\n}\n\nfunction hasRetrySubscriber(collector: WebVitalsCollector) {\n\treturn Array.from(collector.subscribers).some((subscriber) => subscriber.retryDelay !== false);\n}\n\nfunction notifySubscriberError(subscriber: CollectorSubscriber, error: unknown) {\n\ttry {\n\t\tsubscriber.onError(error);\n\t} catch (handlerError) {\n\t\tconsole.error('[WebVitals] Error handler failed.', handlerError);\n\t}\n}\n\nasync function loadWebVitalsModule(attribution: boolean): Promise {\n\tconst module = attribution ? await import('web-vitals/attribution') : await import('web-vitals');\n\treturn module as unknown as WebVitalsModule;\n}\n\nfunction scheduleWebVitalsCollection(callback: () => void, strategy: WebVitalsLoadStrategy, idleTimeout: number) {\n\tconst timeout = nonNegativeOr(idleTimeout, 2000);\n\tif (strategy === 'idle' && typeof window.requestIdleCallback === 'function') {\n\t\tconst handle = window.requestIdleCallback(callback, { timeout });\n\t\treturn () => window.cancelIdleCallback(handle);\n\t}\n\tconst handle = window.setTimeout(callback, strategy === 'immediate' ? 0 : timeout);\n\treturn () => window.clearTimeout(handle);\n}\n\nfunction reportWebVitalsError(error: unknown) {\n\tconsole.error('[WebVitals] Failed to collect metrics.', error);\n}\n\nfunction nonNegativeOr(value: number, fallback: number) {\n\treturn Number.isFinite(value) && value >= 0 ? value : fallback;\n}\n", "type": "registry:component" } ], "type": "registry:component" }