← back to Wine Finder Next
lib/utils/performance.ts
293 lines
/**
* Performance Optimization Utilities for Mobile Wine Finder
*/
// Debounce function for search input
export function debounce<T extends (...args: any[]) => any>(
func: T,
wait: number
): (...args: Parameters<T>) => void {
let timeout: NodeJS.Timeout | null = null;
return function executedFunction(...args: Parameters<T>) {
const later = () => {
timeout = null;
func(...args);
};
if (timeout) {
clearTimeout(timeout);
}
timeout = setTimeout(later, wait);
};
}
// Throttle function for scroll events
export function throttle<T extends (...args: any[]) => any>(
func: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle: boolean;
return function executedFunction(...args: Parameters<T>) {
if (!inThrottle) {
func(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Lazy load images with Intersection Observer
export function lazyLoadImage(
img: HTMLImageElement,
callback?: () => void
): IntersectionObserver {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const target = entry.target as HTMLImageElement;
const src = target.dataset.src;
if (src) {
target.src = src;
target.removeAttribute('data-src');
observer.unobserve(target);
if (callback) {
callback();
}
}
}
});
},
{
rootMargin: '50px', // Start loading 50px before visible
threshold: 0.01,
}
);
observer.observe(img);
return observer;
}
// Prefetch data for faster navigation
export async function prefetchData(url: string): Promise<void> {
try {
const response = await fetch(url, {
priority: 'low',
} as any);
await response.json();
} catch (error) {
console.error('Prefetch failed:', error);
}
}
// Detect network speed
export function getNetworkSpeed(): 'slow' | 'medium' | 'fast' {
if (typeof navigator === 'undefined' || !('connection' in navigator)) {
return 'medium';
}
const connection = (navigator as any).connection;
const effectiveType = connection?.effectiveType;
switch (effectiveType) {
case 'slow-2g':
case '2g':
return 'slow';
case '3g':
return 'medium';
case '4g':
default:
return 'fast';
}
}
// Check if device prefers reduced motion
export function prefersReducedMotion(): boolean {
if (typeof window === 'undefined') return false;
return window.matchMedia('(prefers-reduced-motion: reduce)').matches;
}
// Get device type
export function getDeviceType(): 'mobile' | 'tablet' | 'desktop' {
if (typeof window === 'undefined') return 'desktop';
const width = window.innerWidth;
if (width < 768) return 'mobile';
if (width < 1024) return 'tablet';
return 'desktop';
}
// Cache management
export class CacheManager {
private cache: Map<string, { data: any; timestamp: number }>;
private maxAge: number;
constructor(maxAge: number = 5 * 60 * 1000) {
// 5 minutes default
this.cache = new Map();
this.maxAge = maxAge;
}
set(key: string, data: any): void {
this.cache.set(key, {
data,
timestamp: Date.now(),
});
}
get(key: string): any | null {
const cached = this.cache.get(key);
if (!cached) return null;
const age = Date.now() - cached.timestamp;
if (age > this.maxAge) {
this.cache.delete(key);
return null;
}
return cached.data;
}
clear(): void {
this.cache.clear();
}
has(key: string): boolean {
return this.cache.has(key) && this.get(key) !== null;
}
}
// Image compression for uploads
export async function compressImage(
file: File,
maxWidth: number = 1200,
quality: number = 0.8
): Promise<Blob> {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
const canvas = document.createElement('canvas');
let { width, height } = img;
if (width > maxWidth) {
height = (height * maxWidth) / width;
width = maxWidth;
}
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');
if (!ctx) {
reject(new Error('Canvas context not available'));
return;
}
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
if (blob) {
resolve(blob);
} else {
reject(new Error('Failed to compress image'));
}
},
'image/jpeg',
quality
);
};
img.onerror = reject;
img.src = e.target?.result as string;
};
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
// Service Worker registration
export async function registerServiceWorker(): Promise<ServiceWorkerRegistration | null> {
if (typeof window === 'undefined' || !('serviceWorker' in navigator)) {
return null;
}
try {
const registration = await navigator.serviceWorker.register('/sw.js');
console.log('Service Worker registered:', registration);
return registration;
} catch (error) {
console.error('Service Worker registration failed:', error);
return null;
}
}
// Measure performance metrics
export function measurePerformance(): {
fcp?: number;
lcp?: number;
fid?: number;
cls?: number;
ttfb?: number;
} {
if (typeof window === 'undefined') return {};
const metrics: any = {};
// First Contentful Paint
const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0];
if (fcpEntry) {
metrics.fcp = fcpEntry.startTime;
}
// Time to First Byte
const navigationTiming = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (navigationTiming) {
metrics.ttfb = navigationTiming.responseStart - navigationTiming.requestStart;
}
return metrics;
}
// Check if running on iOS
export function isIOS(): boolean {
if (typeof window === 'undefined') return false;
return (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)
);
}
// Check if running as PWA
export function isPWA(): boolean {
if (typeof window === 'undefined') return false;
return (
window.matchMedia('(display-mode: standalone)').matches ||
(window.navigator as any).standalone === true
);
}
// Optimal image format detection
export function supportsWebP(): boolean {
if (typeof window === 'undefined') return false;
const canvas = document.createElement('canvas');
canvas.width = 1;
canvas.height = 1;
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}