122 lines
3.2 KiB
TypeScript
122 lines
3.2 KiB
TypeScript
interface PerformanceMetrics {
|
|
lcp: number | null;
|
|
fid: number | null;
|
|
cls: number | null;
|
|
fcp: number | null;
|
|
ttfb: number | null;
|
|
pageLoad: number | null;
|
|
}
|
|
|
|
interface ApiTiming {
|
|
name: string;
|
|
duration: number;
|
|
timestamp: number;
|
|
}
|
|
|
|
interface ComponentTiming {
|
|
name: string;
|
|
duration: number;
|
|
timestamp: number;
|
|
}
|
|
|
|
interface LayoutShift extends PerformanceEntry {
|
|
value: number;
|
|
hadRecentInput: boolean;
|
|
}
|
|
|
|
class PerformanceService {
|
|
private metrics: PerformanceMetrics = {
|
|
lcp: null,
|
|
fid: null,
|
|
cls: 0,
|
|
fcp: null,
|
|
ttfb: null,
|
|
pageLoad: null,
|
|
};
|
|
private apiTimings: ApiTiming[] = [];
|
|
private componentTimings: ComponentTiming[] = [];
|
|
|
|
constructor() {
|
|
if (typeof window !== 'undefined' && typeof PerformanceObserver !== 'undefined') {
|
|
this.observeLCP();
|
|
this.observeFID();
|
|
this.observeCLS();
|
|
this.observeFCP();
|
|
}
|
|
}
|
|
|
|
private observeFCP() {
|
|
new PerformanceObserver((entryList) => {
|
|
for (const entry of entryList.getEntries()) {
|
|
if (entry.name === 'first-contentful-paint') {
|
|
this.metrics.fcp = entry.startTime;
|
|
}
|
|
}
|
|
}).observe({ type: 'paint', buffered: true });
|
|
}
|
|
|
|
private observeLCP() {
|
|
new PerformanceObserver((entryList) => {
|
|
const entries = entryList.getEntries();
|
|
if (entries.length > 0) {
|
|
this.metrics.lcp = entries[entries.length - 1]?.startTime ?? null;
|
|
}
|
|
}).observe({ type: 'largest-contentful-paint', buffered: true });
|
|
}
|
|
|
|
private observeFID() {
|
|
new PerformanceObserver((entryList) => {
|
|
for (const entry of entryList.getEntries()) {
|
|
this.metrics.fid = (entry as PerformanceEventTiming).processingStart - entry.startTime;
|
|
}
|
|
}).observe({ type: 'first-input', buffered: true });
|
|
}
|
|
|
|
private observeCLS() {
|
|
new PerformanceObserver((entryList) => {
|
|
for (const entry of entryList.getEntries()) {
|
|
if (!(entry as LayoutShift).hadRecentInput) {
|
|
this.metrics.cls = (this.metrics.cls ?? 0) + (entry as LayoutShift).value;
|
|
}
|
|
}
|
|
}).observe({ type: 'layout-shift', buffered: true });
|
|
}
|
|
|
|
public getMetrics(): PerformanceMetrics {
|
|
if (typeof performance !== 'undefined') {
|
|
const navEntry = performance.getEntriesByType('navigation')[0] as
|
|
| PerformanceNavigationTiming
|
|
| undefined;
|
|
if (navEntry) {
|
|
this.metrics.ttfb = navEntry.responseStart - navEntry.requestStart;
|
|
this.metrics.pageLoad = navEntry.loadEventEnd - navEntry.startTime;
|
|
}
|
|
}
|
|
return this.metrics;
|
|
}
|
|
|
|
public recordApiTiming(name: string, duration: number) {
|
|
this.apiTimings.unshift({ name, duration, timestamp: Date.now() });
|
|
if (this.apiTimings.length > 10) {
|
|
this.apiTimings.pop();
|
|
}
|
|
}
|
|
|
|
public getApiTimings(): ApiTiming[] {
|
|
return this.apiTimings;
|
|
}
|
|
|
|
public recordComponentTiming(name: string, duration: number) {
|
|
this.componentTimings.unshift({ name, duration, timestamp: Date.now() });
|
|
if (this.componentTimings.length > 10) {
|
|
this.componentTimings.pop();
|
|
}
|
|
}
|
|
|
|
public getComponentTimings(): ComponentTiming[] {
|
|
return this.componentTimings;
|
|
}
|
|
}
|
|
|
|
export const performanceService = new PerformanceService();
|