feat(performance): implement extensive performance optimizations across task management components
- Introduced batching and optimized query handling in SQL functions for improved performance during large updates. - Enhanced task sorting functions with batching to reduce load times and improve responsiveness. - Implemented performance monitoring utilities to track render times, memory usage, and long tasks, providing insights for further optimizations. - Added performance analysis component to visualize metrics and identify bottlenecks in task management. - Optimized drag-and-drop functionality with CSS enhancements to ensure smooth interactions and reduce layout thrashing. - Refined task row rendering logic to minimize DOM updates and improve loading behavior for large lists. - Introduced aggressive virtualization and memoization strategies to enhance rendering performance in task lists.
This commit is contained in:
297
worklenz-frontend/src/utils/performance-optimizer.ts
Normal file
297
worklenz-frontend/src/utils/performance-optimizer.ts
Normal file
@@ -0,0 +1,297 @@
|
||||
// Performance optimization utility for reducing long tasks and improving frame rate
|
||||
|
||||
import React from 'react';
|
||||
|
||||
interface PerformanceMetrics {
|
||||
longTaskCount: number;
|
||||
averageLongTaskDuration: number;
|
||||
frameRate: number;
|
||||
memoryUsage: number;
|
||||
layoutThrashingCount: number;
|
||||
}
|
||||
|
||||
class PerformanceOptimizer {
|
||||
private longTaskObserver: PerformanceObserver | null = null;
|
||||
private frameRateObserver: PerformanceObserver | null = null;
|
||||
private layoutThrashingCount = 0;
|
||||
private metrics: PerformanceMetrics = {
|
||||
longTaskCount: 0,
|
||||
averageLongTaskDuration: 0,
|
||||
frameRate: 0,
|
||||
memoryUsage: 0,
|
||||
layoutThrashingCount: 0,
|
||||
};
|
||||
|
||||
constructor() {
|
||||
this.initializeObservers();
|
||||
}
|
||||
|
||||
private initializeObservers() {
|
||||
// Monitor long tasks
|
||||
if ('PerformanceObserver' in window) {
|
||||
this.longTaskObserver = new PerformanceObserver((list) => {
|
||||
for (const entry of list.getEntries()) {
|
||||
if (entry.entryType === 'longtask') {
|
||||
this.metrics.longTaskCount++;
|
||||
this.metrics.averageLongTaskDuration =
|
||||
(this.metrics.averageLongTaskDuration * (this.metrics.longTaskCount - 1) + entry.duration) / this.metrics.longTaskCount;
|
||||
|
||||
console.warn(`🚨 Long task detected: ${entry.duration.toFixed(2)}ms - Consider chunking this operation`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.longTaskObserver.observe({ entryTypes: ['longtask'] });
|
||||
}
|
||||
|
||||
// Monitor frame rate
|
||||
this.startFrameRateMonitoring();
|
||||
}
|
||||
|
||||
private startFrameRateMonitoring() {
|
||||
let frameCount = 0;
|
||||
let lastTime = performance.now();
|
||||
|
||||
const measureFrameRate = () => {
|
||||
frameCount++;
|
||||
const currentTime = performance.now();
|
||||
|
||||
if (currentTime - lastTime >= 1000) {
|
||||
this.metrics.frameRate = frameCount;
|
||||
frameCount = 0;
|
||||
lastTime = currentTime;
|
||||
|
||||
if (this.metrics.frameRate < 30) {
|
||||
console.warn(`⚠️ Low frame rate detected: ${this.metrics.frameRate}fps`);
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(measureFrameRate);
|
||||
};
|
||||
|
||||
requestAnimationFrame(measureFrameRate);
|
||||
}
|
||||
|
||||
// Chunk large operations to prevent long tasks
|
||||
static chunkOperation<T>(
|
||||
items: T[],
|
||||
operation: (item: T, index: number) => void,
|
||||
chunkSize: number = 10,
|
||||
delay: number = 16
|
||||
): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
let index = 0;
|
||||
|
||||
const processChunk = () => {
|
||||
const startTime = performance.now();
|
||||
const endIndex = Math.min(index + chunkSize, items.length);
|
||||
|
||||
// Process items in this chunk
|
||||
for (let i = index; i < endIndex; i++) {
|
||||
operation(items[i], i);
|
||||
}
|
||||
|
||||
index = endIndex;
|
||||
|
||||
// Check if we need to yield to prevent long tasks
|
||||
const processingTime = performance.now() - startTime;
|
||||
if (processingTime > 16) {
|
||||
console.warn(`⚠️ Chunk processing took ${processingTime.toFixed(2)}ms - consider smaller chunks`);
|
||||
}
|
||||
|
||||
if (index < items.length) {
|
||||
// Schedule next chunk with delay to prevent blocking
|
||||
setTimeout(processChunk, delay);
|
||||
} else {
|
||||
resolve();
|
||||
}
|
||||
};
|
||||
|
||||
processChunk();
|
||||
});
|
||||
}
|
||||
|
||||
// Optimize DOM operations to prevent layout thrashing
|
||||
static batchDOMOperations(operations: (() => void)[]): void {
|
||||
// Use requestAnimationFrame to batch DOM updates
|
||||
requestAnimationFrame(() => {
|
||||
// Force layout read first
|
||||
document.body.offsetHeight;
|
||||
|
||||
// Perform all write operations
|
||||
operations.forEach(operation => operation());
|
||||
});
|
||||
}
|
||||
|
||||
// Debounce function for expensive operations
|
||||
static debounce<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
delay: number
|
||||
): T {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
return ((...args: any[]) => {
|
||||
clearTimeout(timeoutId);
|
||||
timeoutId = setTimeout(() => func(...args), delay);
|
||||
}) as T;
|
||||
}
|
||||
|
||||
// Throttle function for frequent operations
|
||||
static throttle<T extends (...args: any[]) => void>(
|
||||
func: T,
|
||||
delay: number
|
||||
): T {
|
||||
let lastExecTime = 0;
|
||||
|
||||
return ((...args: any[]) => {
|
||||
const currentTime = performance.now();
|
||||
|
||||
if (currentTime - lastExecTime > delay) {
|
||||
func(...args);
|
||||
lastExecTime = currentTime;
|
||||
}
|
||||
}) as T;
|
||||
}
|
||||
|
||||
// Optimize list rendering with virtualization hints
|
||||
static optimizeListRendering<T>(
|
||||
items: T[],
|
||||
renderItem: (item: T, index: number) => React.ReactNode,
|
||||
options: {
|
||||
chunkSize?: number;
|
||||
virtualizationThreshold?: number;
|
||||
overscanCount?: number;
|
||||
} = {}
|
||||
): React.ReactNode[] {
|
||||
const {
|
||||
chunkSize = 50,
|
||||
virtualizationThreshold = 100,
|
||||
overscanCount = 5
|
||||
} = options;
|
||||
|
||||
// For small lists, render everything
|
||||
if (items.length <= virtualizationThreshold) {
|
||||
return items.map((item, index) => renderItem(item, index));
|
||||
}
|
||||
|
||||
// For large lists, use chunked rendering
|
||||
const chunks: React.ReactNode[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i += chunkSize) {
|
||||
const chunk = items.slice(i, i + chunkSize);
|
||||
chunks.push(
|
||||
React.createElement('div', { key: `chunk-${i}`, className: 'virtualized-chunk' },
|
||||
chunk.map((item, index) => renderItem(item, i + index))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
|
||||
// Monitor memory usage
|
||||
static getMemoryUsage(): { used: number; total: number; limit: number } {
|
||||
if ('memory' in performance) {
|
||||
const memory = (performance as any).memory;
|
||||
return {
|
||||
used: memory.usedJSHeapSize / 1024 / 1024,
|
||||
total: memory.totalJSHeapSize / 1024 / 1024,
|
||||
limit: memory.jsHeapSizeLimit / 1024 / 1024
|
||||
};
|
||||
}
|
||||
return { used: 0, total: 0, limit: 0 };
|
||||
}
|
||||
|
||||
// Optimize scroll performance
|
||||
static optimizeScroll(container: HTMLElement, handler: (event: Event) => void): () => void {
|
||||
let ticking = false;
|
||||
|
||||
const optimizedHandler = (event: Event) => {
|
||||
if (!ticking) {
|
||||
requestAnimationFrame(() => {
|
||||
handler(event);
|
||||
ticking = false;
|
||||
});
|
||||
ticking = true;
|
||||
}
|
||||
};
|
||||
|
||||
container.addEventListener('scroll', optimizedHandler, { passive: true });
|
||||
|
||||
return () => {
|
||||
container.removeEventListener('scroll', optimizedHandler);
|
||||
};
|
||||
}
|
||||
|
||||
// Optimize hover performance
|
||||
static optimizeHover(
|
||||
element: HTMLElement,
|
||||
onEnter: () => void,
|
||||
onLeave: () => void,
|
||||
delay: number = 50
|
||||
): () => void {
|
||||
let enterTimeout: NodeJS.Timeout;
|
||||
let leaveTimeout: NodeJS.Timeout;
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
clearTimeout(leaveTimeout);
|
||||
enterTimeout = setTimeout(onEnter, delay);
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
clearTimeout(enterTimeout);
|
||||
leaveTimeout = setTimeout(onLeave, delay);
|
||||
};
|
||||
|
||||
element.addEventListener('mouseenter', handleMouseEnter, { passive: true });
|
||||
element.addEventListener('mouseleave', handleMouseLeave, { passive: true });
|
||||
|
||||
return () => {
|
||||
clearTimeout(enterTimeout);
|
||||
clearTimeout(leaveTimeout);
|
||||
element.removeEventListener('mouseenter', handleMouseEnter);
|
||||
element.removeEventListener('mouseleave', handleMouseLeave);
|
||||
};
|
||||
}
|
||||
|
||||
// Get current performance metrics
|
||||
getMetrics(): PerformanceMetrics {
|
||||
const memory = PerformanceOptimizer.getMemoryUsage();
|
||||
this.metrics.memoryUsage = memory.used;
|
||||
this.metrics.layoutThrashingCount = this.layoutThrashingCount;
|
||||
|
||||
return { ...this.metrics };
|
||||
}
|
||||
|
||||
// Reset metrics
|
||||
resetMetrics(): void {
|
||||
this.metrics = {
|
||||
longTaskCount: 0,
|
||||
averageLongTaskDuration: 0,
|
||||
frameRate: 0,
|
||||
memoryUsage: 0,
|
||||
layoutThrashingCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Cleanup observers
|
||||
destroy(): void {
|
||||
if (this.longTaskObserver) {
|
||||
this.longTaskObserver.disconnect();
|
||||
}
|
||||
if (this.frameRateObserver) {
|
||||
this.frameRateObserver.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create global instance
|
||||
const performanceOptimizer = new PerformanceOptimizer();
|
||||
|
||||
// Auto-cleanup on page unload
|
||||
window.addEventListener('beforeunload', () => {
|
||||
performanceOptimizer.destroy();
|
||||
});
|
||||
|
||||
export { PerformanceOptimizer, performanceOptimizer };
|
||||
export default PerformanceOptimizer;
|
||||
Reference in New Issue
Block a user