mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 13:52:16 +01:00
- Implement repository pattern for data access layer - Add comprehensive service layer for business logic - Create scheduler management system with health monitoring - Add bounded buffer utility for memory management - Enhance security audit logging with retention policies
355 lines
11 KiB
TypeScript
355 lines
11 KiB
TypeScript
/**
|
|
* Performance Monitoring and Optimization Utilities
|
|
*
|
|
* This module provides client-side performance monitoring tools to:
|
|
* - Track Core Web Vitals (LCP, FID, CLS)
|
|
* - Monitor bundle loading performance
|
|
* - Provide runtime performance insights
|
|
* - Help identify optimization opportunities
|
|
*/
|
|
|
|
// Core Web Vitals types
|
|
interface PerformanceMetrics {
|
|
lcp?: number; // Largest Contentful Paint
|
|
fid?: number; // First Input Delay
|
|
cls?: number; // Cumulative Layout Shift
|
|
fcp?: number; // First Contentful Paint
|
|
ttfb?: number; // Time to First Byte
|
|
}
|
|
|
|
class PerformanceMonitor {
|
|
private metrics: PerformanceMetrics = {};
|
|
private observers: PerformanceObserver[] = [];
|
|
private isMonitoring = false;
|
|
|
|
constructor() {
|
|
if (typeof window !== "undefined") {
|
|
this.initializeMonitoring();
|
|
}
|
|
}
|
|
|
|
private initializeMonitoring() {
|
|
if (this.isMonitoring) return;
|
|
this.isMonitoring = true;
|
|
|
|
// Monitor LCP (Largest Contentful Paint)
|
|
this.observeMetric("largest-contentful-paint", (entries) => {
|
|
const lastEntry = entries[entries.length - 1] as PerformanceEntry & {
|
|
renderTime: number;
|
|
loadTime: number;
|
|
};
|
|
this.metrics.lcp = lastEntry.renderTime || lastEntry.loadTime;
|
|
this.reportMetric("LCP", this.metrics.lcp);
|
|
});
|
|
|
|
// Monitor FID (First Input Delay)
|
|
this.observeMetric("first-input", (entries) => {
|
|
const firstEntry = entries[0] as PerformanceEntry & {
|
|
processingStart: number;
|
|
startTime: number;
|
|
};
|
|
this.metrics.fid = firstEntry.processingStart - firstEntry.startTime;
|
|
this.reportMetric("FID", this.metrics.fid);
|
|
});
|
|
|
|
// Monitor CLS (Cumulative Layout Shift)
|
|
this.observeMetric("layout-shift", (list) => {
|
|
let clsValue = 0;
|
|
for (const entry of list) {
|
|
const entryWithValue = entry as PerformanceEntry & {
|
|
value: number;
|
|
hadRecentInput: boolean;
|
|
};
|
|
if (!entryWithValue.hadRecentInput) {
|
|
clsValue += entryWithValue.value;
|
|
}
|
|
}
|
|
this.metrics.cls = clsValue;
|
|
this.reportMetric("CLS", this.metrics.cls);
|
|
});
|
|
|
|
// Monitor FCP (First Contentful Paint)
|
|
this.observeMetric("paint", (entries) => {
|
|
const fcpEntry = entries.find(
|
|
(entry) => entry.name === "first-contentful-paint"
|
|
);
|
|
if (fcpEntry) {
|
|
this.metrics.fcp = fcpEntry.startTime;
|
|
this.reportMetric("FCP", this.metrics.fcp);
|
|
}
|
|
});
|
|
|
|
// Monitor TTFB (Time to First Byte)
|
|
this.observeMetric("navigation", (entries) => {
|
|
const navEntry = entries[0] as PerformanceNavigationTiming;
|
|
this.metrics.ttfb = navEntry.responseStart - navEntry.requestStart;
|
|
this.reportMetric("TTFB", this.metrics.ttfb);
|
|
});
|
|
|
|
// Monitor resource loading
|
|
this.observeResourceLoading();
|
|
}
|
|
|
|
private observeMetric(
|
|
entryType: string,
|
|
callback: (entries: PerformanceEntry[]) => void
|
|
) {
|
|
try {
|
|
const observer = new PerformanceObserver((list) => {
|
|
callback(list.getEntries());
|
|
});
|
|
|
|
observer.observe({ entryTypes: [entryType] });
|
|
this.observers.push(observer);
|
|
} catch (error) {
|
|
console.warn(`Failed to observe ${entryType}:`, error);
|
|
}
|
|
}
|
|
|
|
private observeResourceLoading() {
|
|
try {
|
|
const observer = new PerformanceObserver((list) => {
|
|
const entries = list.getEntries();
|
|
for (const entry of entries) {
|
|
if (entry.name.includes(".js") || entry.name.includes(".css")) {
|
|
this.analyzeResourceTiming(entry as PerformanceResourceTiming);
|
|
}
|
|
}
|
|
});
|
|
|
|
observer.observe({ entryTypes: ["resource"] });
|
|
this.observers.push(observer);
|
|
} catch (error) {
|
|
console.warn("Failed to observe resource loading:", error);
|
|
}
|
|
}
|
|
|
|
private analyzeResourceTiming(entry: PerformanceResourceTiming) {
|
|
const isSlowResource = entry.duration > 1000; // Resources taking > 1s
|
|
const isLargeResource = entry.transferSize > 500000; // Resources > 500KB
|
|
|
|
if (isSlowResource || isLargeResource) {
|
|
console.warn("Performance Issue Detected:", {
|
|
resource: entry.name,
|
|
duration: `${entry.duration.toFixed(2)}ms`,
|
|
size: `${(entry.transferSize / 1024).toFixed(2)}KB`,
|
|
type: entry.initiatorType,
|
|
suggestion: isLargeResource
|
|
? "Consider code splitting or dynamic imports"
|
|
: "Resource loading is slow - check network or CDN",
|
|
});
|
|
}
|
|
}
|
|
|
|
private reportMetric(name: string, value: number) {
|
|
if (process.env.NODE_ENV === "development") {
|
|
const rating = this.getRating(name, value);
|
|
console.log(`📊 ${name}: ${value.toFixed(2)}ms (${rating})`);
|
|
|
|
if (rating === "poor") {
|
|
console.warn(`⚠️ ${name} performance is poor. Consider optimization.`);
|
|
}
|
|
}
|
|
|
|
// In production, you might want to send this to an analytics service
|
|
if (process.env.NODE_ENV === "production") {
|
|
this.sendToAnalytics(name, value);
|
|
}
|
|
}
|
|
|
|
private getRating(
|
|
metricName: string,
|
|
value: number
|
|
): "good" | "needs-improvement" | "poor" {
|
|
const thresholds = {
|
|
LCP: { good: 2500, poor: 4000 },
|
|
FID: { good: 100, poor: 300 },
|
|
CLS: { good: 0.1, poor: 0.25 },
|
|
FCP: { good: 1800, poor: 3000 },
|
|
TTFB: { good: 600, poor: 1500 },
|
|
};
|
|
|
|
const threshold = thresholds[metricName as keyof typeof thresholds];
|
|
if (!threshold) return "good";
|
|
|
|
if (value <= threshold.good) return "good";
|
|
if (value <= threshold.poor) return "needs-improvement";
|
|
return "poor";
|
|
}
|
|
|
|
private sendToAnalytics(metricName: string, value: number) {
|
|
// Placeholder for analytics integration
|
|
// You could send this to Google Analytics, Vercel Analytics, etc.
|
|
if (typeof window !== "undefined" && "gtag" in window) {
|
|
(window as any).gtag("event", "core_web_vital", {
|
|
name: metricName,
|
|
value: Math.round(value),
|
|
metric_rating: this.getRating(metricName, value),
|
|
});
|
|
}
|
|
}
|
|
|
|
public getMetrics(): PerformanceMetrics {
|
|
return { ...this.metrics };
|
|
}
|
|
|
|
public generatePerformanceReport(): string {
|
|
const report = Object.entries(this.metrics)
|
|
.map(([key, value]) => {
|
|
const rating = this.getRating(key.toUpperCase(), value);
|
|
return `${key.toUpperCase()}: ${value.toFixed(2)}ms (${rating})`;
|
|
})
|
|
.join("\n");
|
|
|
|
return `Performance Report:\n${report}`;
|
|
}
|
|
|
|
public getBundleAnalysis() {
|
|
if (typeof window === "undefined") return null;
|
|
|
|
const scripts = Array.from(document.querySelectorAll("script[src]"));
|
|
const styles = Array.from(
|
|
document.querySelectorAll('link[rel="stylesheet"]')
|
|
);
|
|
|
|
const bundleInfo = {
|
|
scripts: scripts.length,
|
|
styles: styles.length,
|
|
totalResources: scripts.length + styles.length,
|
|
suggestions: [] as string[],
|
|
};
|
|
|
|
// Analyze bundle composition
|
|
const jsFiles = scripts.map((script) => (script as HTMLScriptElement).src);
|
|
const hasLargeVendorBundle = jsFiles.some(
|
|
(src) => src.includes("vendor") || src.includes("node_modules")
|
|
);
|
|
|
|
if (bundleInfo.scripts > 10) {
|
|
bundleInfo.suggestions.push("Consider consolidating scripts");
|
|
}
|
|
|
|
if (hasLargeVendorBundle) {
|
|
bundleInfo.suggestions.push(
|
|
"Consider code splitting for vendor libraries"
|
|
);
|
|
}
|
|
|
|
return bundleInfo;
|
|
}
|
|
|
|
public cleanup() {
|
|
this.observers.forEach((observer) => observer.disconnect());
|
|
this.observers = [];
|
|
this.isMonitoring = false;
|
|
}
|
|
}
|
|
|
|
// Bundle size analysis utilities
|
|
export const BundleAnalyzer = {
|
|
// Estimate the size of imported modules
|
|
estimateModuleSize: (moduleName: string): Promise<number> => {
|
|
return import(moduleName).then((module) => {
|
|
// This is a rough estimation - in practice you'd use webpack-bundle-analyzer
|
|
return JSON.stringify(module).length;
|
|
});
|
|
},
|
|
|
|
// Check if a module should be dynamically imported based on size
|
|
shouldDynamicImport: (estimatedSize: number, threshold = 50000): boolean => {
|
|
return estimatedSize > threshold; // 50KB threshold
|
|
},
|
|
|
|
// Provide bundle optimization suggestions
|
|
getOptimizationSuggestions: (): string[] => {
|
|
const suggestions: string[] = [];
|
|
|
|
// Check if running in development with potential optimizations
|
|
if (process.env.NODE_ENV === "development") {
|
|
suggestions.push("Run `pnpm build:analyze` to analyze bundle size");
|
|
suggestions.push("Consider using dynamic imports for heavy components");
|
|
suggestions.push("Check if all imported dependencies are actually used");
|
|
}
|
|
|
|
return suggestions;
|
|
},
|
|
};
|
|
|
|
// Web Vitals integration
|
|
export const webVitalsMonitor = new PerformanceMonitor();
|
|
|
|
// Performance hooks for React components
|
|
export const usePerformanceMonitor = () => {
|
|
return {
|
|
getMetrics: () => webVitalsMonitor.getMetrics(),
|
|
generateReport: () => webVitalsMonitor.generatePerformanceReport(),
|
|
getBundleAnalysis: () => webVitalsMonitor.getBundleAnalysis(),
|
|
};
|
|
};
|
|
|
|
// Utility to measure component render time
|
|
export const measureRenderTime = (componentName: string) => {
|
|
const startTime = performance.now();
|
|
|
|
return () => {
|
|
const endTime = performance.now();
|
|
const renderTime = endTime - startTime;
|
|
|
|
if (renderTime > 50) {
|
|
// Flag components taking >50ms to render
|
|
console.warn(
|
|
`🐌 Slow render detected: ${componentName} took ${renderTime.toFixed(2)}ms`
|
|
);
|
|
}
|
|
|
|
return renderTime;
|
|
};
|
|
};
|
|
|
|
// Resource loading utilities
|
|
export const ResourceOptimizer = {
|
|
// Preload critical resources
|
|
preloadResource: (
|
|
url: string,
|
|
type: "script" | "style" | "image" = "script"
|
|
) => {
|
|
if (typeof document === "undefined") return;
|
|
|
|
const link = document.createElement("link");
|
|
link.rel = "preload";
|
|
link.href = url;
|
|
link.as = type;
|
|
document.head.appendChild(link);
|
|
},
|
|
|
|
// Prefetch resources for next navigation
|
|
prefetchResource: (url: string) => {
|
|
if (typeof document === "undefined") return;
|
|
|
|
const link = document.createElement("link");
|
|
link.rel = "prefetch";
|
|
link.href = url;
|
|
document.head.appendChild(link);
|
|
},
|
|
|
|
// Check if resource is already loaded
|
|
isResourceLoaded: (url: string): boolean => {
|
|
if (typeof document === "undefined") return false;
|
|
|
|
const scripts = Array.from(document.querySelectorAll("script[src]"));
|
|
const styles = Array.from(document.querySelectorAll("link[href]"));
|
|
|
|
return [...scripts, ...styles].some((element) => {
|
|
if (element.tagName === "SCRIPT") {
|
|
return (element as HTMLScriptElement).src === url;
|
|
}
|
|
if (element.tagName === "LINK") {
|
|
return (element as HTMLLinkElement).href === url;
|
|
}
|
|
return false;
|
|
});
|
|
},
|
|
};
|
|
|
|
export default webVitalsMonitor;
|