|
|
export interface OptimizedImage { |
|
|
compressed: string; |
|
|
full: string; |
|
|
width: number; |
|
|
height: number; |
|
|
compressedSize: number; |
|
|
fullSize: number; |
|
|
} |
|
|
|
|
|
export interface PerformanceTiming { |
|
|
stepName: string; |
|
|
startTime: number; |
|
|
endTime: number; |
|
|
duration: number; |
|
|
timestamp: string; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function optimizeImage(base64Image: string, quality: number = 0.7): Promise<OptimizedImage> { |
|
|
return new Promise((resolve, reject) => { |
|
|
try { |
|
|
const img = new Image(); |
|
|
img.onload = () => { |
|
|
|
|
|
const canvas = document.createElement('canvas'); |
|
|
const ctx = canvas.getContext('2d')!; |
|
|
|
|
|
|
|
|
canvas.width = img.width; |
|
|
canvas.height = img.height; |
|
|
|
|
|
|
|
|
ctx.drawImage(img, 0, 0); |
|
|
|
|
|
|
|
|
const compressedBase64 = canvas.toDataURL('image/jpeg', quality); |
|
|
|
|
|
|
|
|
const fullSize = base64Image.length * 0.75; |
|
|
const compressedSize = compressedBase64.split(',')[1].length * 0.75; |
|
|
|
|
|
resolve({ |
|
|
compressed: compressedBase64.split(',')[1], |
|
|
full: base64Image, |
|
|
width: img.width, |
|
|
height: img.height, |
|
|
compressedSize: Math.round(compressedSize), |
|
|
fullSize: Math.round(fullSize) |
|
|
}); |
|
|
}; |
|
|
|
|
|
img.onerror = () => reject(new Error('Failed to load image for optimization')); |
|
|
img.src = `data:image/jpeg;base64,${base64Image}`; |
|
|
} catch (error) { |
|
|
reject(error); |
|
|
} |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export async function optimizeImages(images: string[], quality: number = 0.7): Promise<OptimizedImage[]> { |
|
|
const optimizationPromises = images.map(img => optimizeImage(img, quality)); |
|
|
return Promise.all(optimizationPromises); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export class PerformanceTracker { |
|
|
private timings: PerformanceTiming[] = []; |
|
|
private activeTimers: Map<string, number> = new Map(); |
|
|
|
|
|
startTimer(stepName: string): void { |
|
|
const startTime = performance.now(); |
|
|
this.activeTimers.set(stepName, startTime); |
|
|
} |
|
|
|
|
|
endTimer(stepName: string): PerformanceTiming { |
|
|
const endTime = performance.now(); |
|
|
const startTime = this.activeTimers.get(stepName); |
|
|
|
|
|
if (!startTime) { |
|
|
throw new Error(`Timer for "${stepName}" was not started`); |
|
|
} |
|
|
|
|
|
const timing: PerformanceTiming = { |
|
|
stepName, |
|
|
startTime, |
|
|
endTime, |
|
|
duration: endTime - startTime, |
|
|
timestamp: new Date().toISOString() |
|
|
}; |
|
|
|
|
|
this.timings.push(timing); |
|
|
this.activeTimers.delete(stepName); |
|
|
|
|
|
return timing; |
|
|
} |
|
|
|
|
|
getTimings(): PerformanceTiming[] { |
|
|
return [...this.timings]; |
|
|
} |
|
|
|
|
|
getTotalDuration(): number { |
|
|
return this.timings.reduce((total, timing) => total + timing.duration, 0); |
|
|
} |
|
|
|
|
|
getTimingByStep(stepName: string): PerformanceTiming | undefined { |
|
|
return this.timings.find(timing => timing.stepName === stepName); |
|
|
} |
|
|
|
|
|
clear(): void { |
|
|
this.timings = []; |
|
|
this.activeTimers.clear(); |
|
|
} |
|
|
|
|
|
getFormattedSummary(): string { |
|
|
if (this.timings.length === 0) return 'No performance data available'; |
|
|
|
|
|
const summary = this.timings.map(timing => |
|
|
`${timing.stepName}: ${(timing.duration / 1000).toFixed(2)}s` |
|
|
).join('\n'); |
|
|
|
|
|
const total = (this.getTotalDuration() / 1000).toFixed(2); |
|
|
return `${summary}\n\nTotal: ${total}s`; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export interface LazyLoadOptions { |
|
|
rootMargin?: string; |
|
|
threshold?: number; |
|
|
fallbackDelay?: number; |
|
|
} |
|
|
|
|
|
export function createLazyLoader(options: LazyLoadOptions = {}) { |
|
|
const { rootMargin = '50px', threshold = 0.1, fallbackDelay = 300 } = options; |
|
|
|
|
|
if ('IntersectionObserver' in window) { |
|
|
return new IntersectionObserver((entries) => { |
|
|
entries.forEach(entry => { |
|
|
if (entry.isIntersecting) { |
|
|
const img = entry.target as HTMLImageElement; |
|
|
const src = img.dataset.src; |
|
|
if (src) { |
|
|
img.src = src; |
|
|
img.classList.remove('lazy'); |
|
|
img.classList.add('loaded'); |
|
|
} |
|
|
} |
|
|
}); |
|
|
}, { rootMargin, threshold }); |
|
|
} |
|
|
|
|
|
|
|
|
return { |
|
|
observe: (element: Element) => { |
|
|
setTimeout(() => { |
|
|
const img = element as HTMLImageElement; |
|
|
const src = img.dataset.src; |
|
|
if (src) { |
|
|
img.src = src; |
|
|
img.classList.remove('lazy'); |
|
|
img.classList.add('loaded'); |
|
|
} |
|
|
}, fallbackDelay); |
|
|
}, |
|
|
disconnect: () => {}, |
|
|
unobserve: () => {} |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function getMemoryUsage(): { used: number; total: number; percentage: number } | null { |
|
|
if ('memory' in performance) { |
|
|
const memory = (performance as any).memory; |
|
|
return { |
|
|
used: Math.round(memory.usedJSHeapSize / 1024 / 1024), |
|
|
total: Math.round(memory.totalJSHeapSize / 1024 / 1024), |
|
|
percentage: Math.round((memory.usedJSHeapSize / memory.totalJSHeapSize) * 100) |
|
|
}; |
|
|
} |
|
|
return null; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function debounce<T extends (...args: any[]) => any>( |
|
|
func: T, |
|
|
wait: number |
|
|
): (...args: Parameters<T>) => void { |
|
|
let timeout: NodeJS.Timeout; |
|
|
return (...args: Parameters<T>) => { |
|
|
clearTimeout(timeout); |
|
|
timeout = setTimeout(() => func(...args), wait); |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
export function throttle<T extends (...args: any[]) => any>( |
|
|
func: T, |
|
|
limit: number |
|
|
): (...args: Parameters<T>) => void { |
|
|
let inThrottle: boolean; |
|
|
return (...args: Parameters<T>) => { |
|
|
if (!inThrottle) { |
|
|
func(...args); |
|
|
inThrottle = true; |
|
|
setTimeout(() => inThrottle = false, limit); |
|
|
} |
|
|
}; |
|
|
} |