import { useEffect, useRef } from 'react';
interface LayoutShift extends PerformanceEntry {
value: number;
hadRecentInput: boolean;
}
interface Metric {
name: string;
value: number;
unit: string;
level: string;
}
export const usePerformance = (enable = import.meta.env.DEV) => {
const started = useRef(false);
useEffect(() => {
if (!enable || started.current) return;
started.current = true;
const observers: PerformanceObserver\[\] = \[\];
const metrics = new Map<string, number>();
const addMetric = (name: string, value: number) => {
metrics.set(name, Number(value.toFixed(2)));
};
// ---------------- Navigation ----------------
const nav = performance.getEntriesByType(
'navigation'
)0 as PerformanceNavigationTiming;
if (nav) {
addMetric('TTFB', nav.responseStart);
addMetric('DOM Ready', nav.domContentLoadedEventEnd);
addMetric('Load', nav.loadEventEnd);
}
// ---------------- Paint ----------------
performance
.getEntriesByType('paint')
.forEach((entry: PerformanceEntry) => {
if (entry.name === 'first-paint') {
addMetric('FP', entry.startTime);
addMetric('白屏时间', entry.startTime);
}
if (entry.name === 'first-contentful-paint') {
addMetric('FCP', entry.startTime);
}
});
// ---------------- LCP ----------------
const lcpObserver = new PerformanceObserver((list) => {
const last = list.getEntries().at(-1);
if (last) {
addMetric('LCP', last.startTime);
}
});
lcpObserver.observe({
type: 'largest-contentful-paint',
buffered: true,
});
observers.push(lcpObserver);
// ---------------- CLS ----------------
let cls = 0;
const clsObserver = new PerformanceObserver((list) => {
(list.getEntries() as LayoutShift\[\]).forEach((entry) => {
if (!entry.hadRecentInput) {
cls += entry.value;
}
});
metrics.set('CLS', Number(cls.toFixed(3)));
});
clsObserver.observe({
type: 'layout-shift',
buffered: true,
});
observers.push(clsObserver);
// ---------------- INP ----------------
if (
PerformanceObserver.supportedEntryTypes.includes('event')
) {
const inpObserver = new PerformanceObserver((list) => {
const max = Math.max(
...list.getEntries().map((e) => e.duration)
);
addMetric('INP', max);
});
inpObserver.observe({
type: 'event',
buffered: true,
durationThreshold: 40,
});
observers.push(inpObserver);
}
// ---------------- 输出 ----------------
const timer = window.setTimeout(() => {
const getLevel = (name: string, value: number) => {
switch (name) {
case 'FCP':
return value < 1800 ? '🟢' : value < 3000 ? '🟡' : '🔴';
case 'LCP':
return value < 2500 ? '🟢' : value < 4000 ? '🟡' : '🔴';
case 'CLS':
return value < 0.1 ? '🟢' : value < 0.25 ? '🟡' : '🔴';
case 'INP':
return value < 200 ? '🟢' : value < 500 ? '🟡' : '🔴';
default:
return value < 1000 ? '🟢' : value < 3000 ? '🟡' : '🔴';
}
};
const table: Metric\[\] = \[\];
metrics.forEach((value, key) => {
table.push({
name: key,
value,
unit: key === 'CLS' ? '' : 'ms',
level: getLevel(key, value),
});
});
console.groupCollapsed(
'%c🚀 页面性能',
'background:#1677ff;color:white;padding:4px 10px;border-radius:4px'
);
console.table(table);
console.groupEnd();
}, 2500);
return () => {
window.clearTimeout(timer);
observers.forEach((o) => o.disconnect());
started.current = false;
};
}, enable);
};