|
1
|
// Performance optimization utilities |
|
2
|
|
|
3
|
// Preload critical routes and data |
|
4
|
export function preloadCriticalRoutes() { |
|
5
|
// Preload the most common pages users visit |
|
6
|
const criticalImports = [ |
|
7
|
() => import("@/pages/device-finder"), |
|
8
|
() => import("@/pages/troubleshooting"), |
|
9
|
() => import("@/pages/repair-guide"), |
|
10
|
]; |
|
11
|
|
|
12
|
// Use requestIdleCallback to preload during idle time |
|
13
|
if ('requestIdleCallback' in window) { |
|
14
|
criticalImports.forEach((importFn, index) => { |
|
15
|
window.requestIdleCallback(() => { |
|
16
|
importFn().catch(() => {}); // Silent fail for preloading |
|
17
|
}, { timeout: 1000 + (index * 500) }); |
|
18
|
}); |
|
19
|
} else { |
|
20
|
// Fallback for browsers without requestIdleCallback |
|
21
|
setTimeout(() => { |
|
22
|
criticalImports.forEach((importFn) => { |
|
23
|
importFn().catch(() => {}); |
|
24
|
}); |
|
25
|
}, 2000); |
|
26
|
} |
|
27
|
} |
|
28
|
|
|
29
|
// Optimize images with lazy loading and proper sizing |
|
30
|
export function setupImageOptimization() { |
|
31
|
// Add loading="lazy" to all images that don't have it |
|
32
|
const images = document.querySelectorAll('img:not([loading])'); |
|
33
|
images.forEach((img) => { |
|
34
|
img.setAttribute('loading', 'lazy'); |
|
35
|
}); |
|
36
|
} |
|
37
|
|
|
38
|
// Preconnect to external domains for faster resource loading |
|
39
|
export function setupPreconnects() { |
|
40
|
const preconnects = [ |
|
41
|
'https://fonts.googleapis.com', |
|
42
|
'https://fonts.gstatic.com', |
|
43
|
]; |
|
44
|
|
|
45
|
preconnects.forEach((url) => { |
|
46
|
const link = document.createElement('link'); |
|
47
|
link.rel = 'preconnect'; |
|
48
|
link.href = url; |
|
49
|
link.crossOrigin = 'anonymous'; |
|
50
|
document.head.appendChild(link); |
|
51
|
}); |
|
52
|
} |
|
53
|
|
|
54
|
// Setup performance monitoring |
|
55
|
export function setupPerformanceMonitoring() { |
|
56
|
// Monitor Core Web Vitals |
|
57
|
if ('web-vital' in window || typeof window !== 'undefined') { |
|
58
|
// Track First Contentful Paint |
|
59
|
const observer = new PerformanceObserver((list) => { |
|
60
|
for (const entry of list.getEntries()) { |
|
61
|
if (entry.name === 'first-contentful-paint') { |
|
62
|
console.log('FCP:', entry.startTime); |
|
63
|
} |
|
64
|
} |
|
65
|
}); |
|
66
|
|
|
67
|
try { |
|
68
|
observer.observe({ entryTypes: ['paint'] }); |
|
69
|
} catch (e) { |
|
70
|
// Browser doesn't support this API |
|
71
|
} |
|
72
|
} |
|
73
|
} |
|
74
|
|
|
75
|
// Debounce function for performance |
|
76
|
export function debounce<T extends (...args: any[]) => any>( |
|
77
|
func: T, |
|
78
|
wait: number |
|
79
|
): (...args: Parameters<T>) => void { |
|
80
|
let timeout: NodeJS.Timeout; |
|
81
|
return (...args: Parameters<T>) => { |
|
82
|
clearTimeout(timeout); |
|
83
|
timeout = setTimeout(() => func(...args), wait); |
|
84
|
}; |
|
85
|
} |
|
86
|
|
|
87
|
// Throttle function for performance |
|
88
|
export function throttle<T extends (...args: any[]) => any>( |
|
89
|
func: T, |
|
90
|
limit: number |
|
91
|
): (...args: Parameters<T>) => void { |
|
92
|
let inThrottle = false; |
|
93
|
return (...args: Parameters<T>) => { |
|
94
|
if (!inThrottle) { |
|
95
|
func(...args); |
|
96
|
inThrottle = true; |
|
97
|
setTimeout(() => inThrottle = false, limit); |
|
98
|
} |
|
99
|
}; |
|
100
|
} |
|
101
|
|
|
102
|
// Enhanced image optimization with memory management |
|
103
|
export function optimizeImageLoading() { |
|
104
|
// Lazy load all images |
|
105
|
const images = document.querySelectorAll('img'); |
|
106
|
|
|
107
|
const imageObserver = new IntersectionObserver((entries) => { |
|
108
|
entries.forEach(entry => { |
|
109
|
if (entry.isIntersecting) { |
|
110
|
const img = entry.target as HTMLImageElement; |
|
111
|
|
|
112
|
// Set loading attributes |
|
113
|
if (!img.getAttribute('loading')) { |
|
114
|
img.setAttribute('loading', 'lazy'); |
|
115
|
} |
|
116
|
|
|
117
|
// Optimize image sizing |
|
118
|
if (!img.style.maxWidth) { |
|
119
|
img.style.maxWidth = '100%'; |
|
120
|
img.style.height = 'auto'; |
|
121
|
} |
|
122
|
|
|
123
|
// Add error handling |
|
124
|
img.onerror = () => { |
|
125
|
img.style.display = 'none'; |
|
126
|
console.warn(`Failed to load image: ${img.src}`); |
|
127
|
}; |
|
128
|
|
|
129
|
imageObserver.unobserve(img); |
|
130
|
} |
|
131
|
}); |
|
132
|
}, { |
|
133
|
rootMargin: '50px', |
|
134
|
threshold: 0.1 |
|
135
|
}); |
|
136
|
|
|
137
|
images.forEach(img => imageObserver.observe(img)); |
|
138
|
|
|
139
|
return () => imageObserver.disconnect(); |
|
140
|
} |
|
141
|
|
|
142
|
// Memory-efficient list rendering with virtual scrolling |
|
143
|
export function setupVirtualScrolling(containerRef: HTMLElement, itemHeight: number, totalItems: number) { |
|
144
|
let startIndex = 0; |
|
145
|
let endIndex = Math.min(Math.ceil(containerRef.clientHeight / itemHeight) + 5, totalItems); |
|
146
|
|
|
147
|
const updateVisibleRange = () => { |
|
148
|
const scrollTop = containerRef.scrollTop; |
|
149
|
const containerHeight = containerRef.clientHeight; |
|
150
|
|
|
151
|
startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - 2); |
|
152
|
endIndex = Math.min(totalItems, startIndex + Math.ceil(containerHeight / itemHeight) + 5); |
|
153
|
|
|
154
|
return { startIndex, endIndex }; |
|
155
|
}; |
|
156
|
|
|
157
|
const handleScroll = throttle(() => { |
|
158
|
updateVisibleRange(); |
|
159
|
// Trigger re-render callback if needed |
|
160
|
}, 16); // 60fps |
|
161
|
|
|
162
|
containerRef.addEventListener('scroll', handleScroll, { passive: true }); |
|
163
|
|
|
164
|
return { |
|
165
|
getVisibleRange: updateVisibleRange, |
|
166
|
cleanup: () => containerRef.removeEventListener('scroll', handleScroll) |
|
167
|
}; |
|
168
|
} |
|
169
|
|
|
170
|
// Memory leak detection and cleanup |
|
171
|
export function detectMemoryLeaks() { |
|
172
|
const initialMemory = (performance as any).memory?.usedJSHeapSize; |
|
173
|
let checkCount = 0; |
|
174
|
|
|
175
|
const memoryCheckInterval = setInterval(() => { |
|
176
|
checkCount++; |
|
177
|
|
|
178
|
if ('memory' in performance) { |
|
179
|
const currentMemory = (performance as any).memory.usedJSHeapSize; |
|
180
|
const memoryGrowth = currentMemory - (initialMemory || 0); |
|
181
|
|
|
182
|
if (memoryGrowth > 50 * 1024 * 1024 && checkCount > 10) { // 50MB growth |
|
183
|
console.warn('Potential memory leak detected:', { |
|
184
|
growth: `${Math.round(memoryGrowth / 1024 / 1024)}MB`, |
|
185
|
current: `${Math.round(currentMemory / 1024 / 1024)}MB` |
|
186
|
}); |
|
187
|
|
|
188
|
// Suggest cleanup actions |
|
189
|
console.log('Suggesting cleanup actions:'); |
|
190
|
console.log('- Clear unused query cache'); |
|
191
|
console.log('- Remove old localStorage data'); |
|
192
|
console.log('- Check for unremoved event listeners'); |
|
193
|
} |
|
194
|
} |
|
195
|
|
|
196
|
// Stop checking after 30 minutes |
|
197
|
if (checkCount > 180) { |
|
198
|
clearInterval(memoryCheckInterval); |
|
199
|
} |
|
200
|
}, 10000); // Check every 10 seconds |
|
201
|
|
|
202
|
return () => clearInterval(memoryCheckInterval); |
|
203
|
} |
|
204
|
|
|
205
|
// Optimize bundle loading with intelligent preloading |
|
206
|
export function optimizeBundleLoading() { |
|
207
|
// Preload critical chunks during idle time |
|
208
|
const criticalChunks = [ |
|
209
|
'/assets/repair-guides', |
|
210
|
'/assets/device-finder', |
|
211
|
'/assets/troubleshooting' |
|
212
|
]; |
|
213
|
|
|
214
|
if ('requestIdleCallback' in window) { |
|
215
|
window.requestIdleCallback(() => { |
|
216
|
criticalChunks.forEach((chunk, index) => { |
|
217
|
setTimeout(() => { |
|
218
|
const link = document.createElement('link'); |
|
219
|
link.rel = 'preload'; |
|
220
|
link.as = 'script'; |
|
221
|
link.href = chunk; |
|
222
|
document.head.appendChild(link); |
|
223
|
}, index * 200); |
|
224
|
}); |
|
225
|
}); |
|
226
|
} |
|
227
|
} |
|
228
|
|
|
229
|
// Resource cleanup on route changes |
|
230
|
export function cleanupOnRouteChange() { |
|
231
|
// Cancel pending requests |
|
232
|
const abortController = new AbortController(); |
|
233
|
|
|
234
|
// Clean up timers |
|
235
|
const timers: NodeJS.Timeout[] = []; |
|
236
|
|
|
237
|
const cleanup = () => { |
|
238
|
abortController.abort(); |
|
239
|
timers.forEach(timer => clearTimeout(timer)); |
|
240
|
timers.length = 0; |
|
241
|
}; |
|
242
|
|
|
243
|
// Listen for route changes |
|
244
|
if (typeof window !== 'undefined') { |
|
245
|
window.addEventListener('beforeunload', cleanup); |
|
246
|
window.addEventListener('popstate', cleanup); |
|
247
|
} |
|
248
|
|
|
249
|
return { |
|
250
|
addTimer: (timer: NodeJS.Timeout) => timers.push(timer), |
|
251
|
signal: abortController.signal, |
|
252
|
cleanup |
|
253
|
}; |
|
254
|
} |