Merge pull request #169 from Worklenz/imp/task-list-performance-fixes

Imp/task list performance fixes
This commit is contained in:
Chamika J
2025-06-21 18:53:55 +05:30
committed by GitHub
21 changed files with 1735 additions and 452 deletions

View 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;

View 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;