feat(performance): implement comprehensive performance improvements for Worklenz frontend
- Introduced a new document summarizing performance optimizations across the application. - Applied React.memo(), useMemo(), and useCallback() to key components to minimize unnecessary re-renders and optimize rendering performance. - Implemented a route preloading system to enhance navigation speed and user experience. - Added performance monitoring utilities for development to track component render times and function execution. - Enhanced lazy loading and suspense boundaries for better loading states. - Conducted production optimizations, including TypeScript error fixes and memory management improvements. - Memoized style and configuration objects to reduce garbage collection pressure and improve memory usage.
This commit is contained in:
182
worklenz-frontend/src/utils/performance.ts
Normal file
182
worklenz-frontend/src/utils/performance.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Performance monitoring utilities for development
|
||||
*/
|
||||
|
||||
const isProduction = import.meta.env.PROD;
|
||||
const isDevelopment = !isProduction;
|
||||
|
||||
interface PerformanceEntry {
|
||||
name: string;
|
||||
startTime: number;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
class PerformanceMonitor {
|
||||
private timers: Map<string, number> = new Map();
|
||||
private entries: PerformanceEntry[] = [];
|
||||
|
||||
/**
|
||||
* Start timing a performance measurement
|
||||
*/
|
||||
public startTimer(name: string): void {
|
||||
if (isProduction) return;
|
||||
|
||||
this.timers.set(name, performance.now());
|
||||
}
|
||||
|
||||
/**
|
||||
* End timing and log the result
|
||||
*/
|
||||
public endTimer(name: string): number | null {
|
||||
if (isProduction) return null;
|
||||
|
||||
const startTime = this.timers.get(name);
|
||||
if (!startTime) {
|
||||
console.warn(`Performance timer "${name}" was not started`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime;
|
||||
|
||||
this.timers.delete(name);
|
||||
this.entries.push({ name, startTime, duration });
|
||||
|
||||
if (isDevelopment) {
|
||||
const color = duration > 100 ? '#ff4d4f' : duration > 50 ? '#faad14' : '#52c41a';
|
||||
console.log(
|
||||
`%c⏱️ ${name}: ${duration.toFixed(2)}ms`,
|
||||
`color: ${color}; font-weight: bold;`
|
||||
);
|
||||
}
|
||||
|
||||
return duration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the performance of a function
|
||||
*/
|
||||
public measure<T>(name: string, fn: () => T): T {
|
||||
if (isProduction) return fn();
|
||||
|
||||
this.startTimer(name);
|
||||
const result = fn();
|
||||
this.endTimer(name);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Measure the performance of an async function
|
||||
*/
|
||||
public async measureAsync<T>(name: string, fn: () => Promise<T>): Promise<T> {
|
||||
if (isProduction) return fn();
|
||||
|
||||
this.startTimer(name);
|
||||
const result = await fn();
|
||||
this.endTimer(name);
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all performance entries
|
||||
*/
|
||||
public getEntries(): PerformanceEntry[] {
|
||||
return [...this.entries];
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all entries
|
||||
*/
|
||||
public clearEntries(): void {
|
||||
this.entries = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Log a summary of all performance entries
|
||||
*/
|
||||
public logSummary(): void {
|
||||
if (isProduction || this.entries.length === 0) return;
|
||||
|
||||
console.group('📊 Performance Summary');
|
||||
|
||||
const sortedEntries = this.entries
|
||||
.filter(entry => entry.duration !== undefined)
|
||||
.sort((a, b) => (b.duration || 0) - (a.duration || 0));
|
||||
|
||||
console.table(
|
||||
sortedEntries.map(entry => ({
|
||||
Name: entry.name,
|
||||
Duration: `${(entry.duration || 0).toFixed(2)}ms`,
|
||||
'Start Time': `${entry.startTime.toFixed(2)}ms`
|
||||
}))
|
||||
);
|
||||
|
||||
const totalTime = sortedEntries.reduce((sum, entry) => sum + (entry.duration || 0), 0);
|
||||
console.log(`%cTotal measured time: ${totalTime.toFixed(2)}ms`, 'font-weight: bold;');
|
||||
|
||||
console.groupEnd();
|
||||
}
|
||||
}
|
||||
|
||||
// Create default instance
|
||||
export const performanceMonitor = new PerformanceMonitor();
|
||||
|
||||
/**
|
||||
* Higher-order component to measure component render performance
|
||||
*/
|
||||
export function withPerformanceMonitoring<P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
componentName?: string
|
||||
): React.ComponentType<P> {
|
||||
if (isProduction) return Component;
|
||||
|
||||
const name = componentName || Component.displayName || Component.name || 'Unknown';
|
||||
|
||||
const WrappedComponent = (props: P) => {
|
||||
React.useEffect(() => {
|
||||
performanceMonitor.startTimer(`${name} mount`);
|
||||
return () => {
|
||||
performanceMonitor.endTimer(`${name} mount`);
|
||||
};
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
performanceMonitor.endTimer(`${name} render`);
|
||||
});
|
||||
|
||||
performanceMonitor.startTimer(`${name} render`);
|
||||
return React.createElement(Component, props);
|
||||
};
|
||||
|
||||
WrappedComponent.displayName = `withPerformanceMonitoring(${name})`;
|
||||
return WrappedComponent;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook to measure render performance
|
||||
*/
|
||||
export function useRenderPerformance(componentName: string): void {
|
||||
if (isProduction) return;
|
||||
|
||||
const renderCount = React.useRef(0);
|
||||
const startTime = React.useRef<number>(0);
|
||||
|
||||
React.useEffect(() => {
|
||||
renderCount.current += 1;
|
||||
const endTime = performance.now();
|
||||
const duration = endTime - startTime.current;
|
||||
|
||||
if (renderCount.current > 1) {
|
||||
console.log(
|
||||
`%c🔄 ${componentName} render #${renderCount.current}: ${duration.toFixed(2)}ms`,
|
||||
'color: #1890ff; font-size: 11px;'
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
startTime.current = performance.now();
|
||||
}
|
||||
|
||||
export default performanceMonitor;
|
||||
181
worklenz-frontend/src/utils/routePreloader.ts
Normal file
181
worklenz-frontend/src/utils/routePreloader.ts
Normal file
@@ -0,0 +1,181 @@
|
||||
import React from 'react';
|
||||
|
||||
/**
|
||||
* Route preloader utility to prefetch components and improve navigation performance
|
||||
*/
|
||||
|
||||
interface PreloadableRoute {
|
||||
path: string;
|
||||
loader: () => Promise<any>;
|
||||
priority: 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
class RoutePreloader {
|
||||
private preloadedRoutes = new Set<string>();
|
||||
private preloadQueue: PreloadableRoute[] = [];
|
||||
private isPreloading = false;
|
||||
|
||||
/**
|
||||
* Register a route for preloading
|
||||
*/
|
||||
public registerRoute(path: string, loader: () => Promise<any>, priority: 'high' | 'medium' | 'low' = 'medium'): void {
|
||||
if (this.preloadedRoutes.has(path)) return;
|
||||
|
||||
this.preloadQueue.push({ path, loader, priority });
|
||||
this.sortQueue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload a specific route immediately
|
||||
*/
|
||||
public async preloadRoute(path: string, loader: () => Promise<any>): Promise<void> {
|
||||
if (this.preloadedRoutes.has(path)) return;
|
||||
|
||||
try {
|
||||
await loader();
|
||||
this.preloadedRoutes.add(path);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to preload route: ${path}`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start preloading routes in the queue
|
||||
*/
|
||||
public async startPreloading(): Promise<void> {
|
||||
if (this.isPreloading || this.preloadQueue.length === 0) return;
|
||||
|
||||
this.isPreloading = true;
|
||||
|
||||
// Use requestIdleCallback if available, otherwise setTimeout
|
||||
const scheduleWork = (callback: () => void) => {
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(callback, { timeout: 1000 });
|
||||
} else {
|
||||
setTimeout(callback, 0);
|
||||
}
|
||||
};
|
||||
|
||||
const processQueue = async () => {
|
||||
while (this.preloadQueue.length > 0) {
|
||||
const route = this.preloadQueue.shift();
|
||||
if (!route) break;
|
||||
|
||||
if (this.preloadedRoutes.has(route.path)) continue;
|
||||
|
||||
try {
|
||||
await route.loader();
|
||||
this.preloadedRoutes.add(route.path);
|
||||
} catch (error) {
|
||||
console.warn(`Failed to preload route: ${route.path}`, error);
|
||||
}
|
||||
|
||||
// Yield control back to the browser
|
||||
await new Promise<void>(resolve => scheduleWork(() => resolve()));
|
||||
}
|
||||
|
||||
this.isPreloading = false;
|
||||
};
|
||||
|
||||
scheduleWork(processQueue);
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload routes on user interaction (hover, focus)
|
||||
*/
|
||||
public preloadOnInteraction(element: HTMLElement, path: string, loader: () => Promise<any>): void {
|
||||
if (this.preloadedRoutes.has(path)) return;
|
||||
|
||||
let preloadTriggered = false;
|
||||
|
||||
const handleInteraction = () => {
|
||||
if (preloadTriggered) return;
|
||||
preloadTriggered = true;
|
||||
|
||||
this.preloadRoute(path, loader);
|
||||
|
||||
// Clean up listeners
|
||||
element.removeEventListener('mouseenter', handleInteraction);
|
||||
element.removeEventListener('focus', handleInteraction);
|
||||
element.removeEventListener('touchstart', handleInteraction);
|
||||
};
|
||||
|
||||
element.addEventListener('mouseenter', handleInteraction, { passive: true });
|
||||
element.addEventListener('focus', handleInteraction, { passive: true });
|
||||
element.addEventListener('touchstart', handleInteraction, { passive: true });
|
||||
}
|
||||
|
||||
/**
|
||||
* Preload routes when the browser is idle
|
||||
*/
|
||||
public preloadOnIdle(): void {
|
||||
if ('requestIdleCallback' in window) {
|
||||
requestIdleCallback(() => {
|
||||
this.startPreloading();
|
||||
}, { timeout: 2000 });
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
this.startPreloading();
|
||||
}, 1000);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a route is already preloaded
|
||||
*/
|
||||
public isRoutePreloaded(path: string): boolean {
|
||||
return this.preloadedRoutes.has(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all preloaded routes
|
||||
*/
|
||||
public clearPreloaded(): void {
|
||||
this.preloadedRoutes.clear();
|
||||
this.preloadQueue = [];
|
||||
}
|
||||
|
||||
private sortQueue(): void {
|
||||
const priorityOrder = { high: 0, medium: 1, low: 2 };
|
||||
this.preloadQueue.sort((a, b) => priorityOrder[a.priority] - priorityOrder[b.priority]);
|
||||
}
|
||||
}
|
||||
|
||||
// Create default instance
|
||||
export const routePreloader = new RoutePreloader();
|
||||
|
||||
/**
|
||||
* React hook to preload routes on component mount
|
||||
*/
|
||||
export function useRoutePreloader(routes: Array<{ path: string; loader: () => Promise<any>; priority?: 'high' | 'medium' | 'low' }>): void {
|
||||
React.useEffect(() => {
|
||||
routes.forEach(route => {
|
||||
routePreloader.registerRoute(route.path, route.loader, route.priority);
|
||||
});
|
||||
|
||||
// Start preloading after a short delay to not interfere with initial render
|
||||
const timer = setTimeout(() => {
|
||||
routePreloader.preloadOnIdle();
|
||||
}, 100);
|
||||
|
||||
return () => clearTimeout(timer);
|
||||
}, [routes]);
|
||||
}
|
||||
|
||||
/**
|
||||
* React hook to preload a route on element interaction
|
||||
*/
|
||||
export function usePreloadOnHover(path: string, loader: () => Promise<any>) {
|
||||
const elementRef = React.useRef<HTMLElement>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const element = elementRef.current;
|
||||
if (!element) return;
|
||||
|
||||
routePreloader.preloadOnInteraction(element, path, loader);
|
||||
}, [path, loader]);
|
||||
|
||||
return elementRef;
|
||||
}
|
||||
|
||||
export default routePreloader;
|
||||
Reference in New Issue
Block a user