feat: implement mobile performance optimizations (Task 21)

- Add code splitting and lazy loading for all page components
- Create LazyImage component with Intersection Observer
- Configure service worker with Vite PWA plugin for offline capability
- Implement bundle size optimization with manual chunk splitting
- Add performance monitoring utilities and Web Vitals tracking
- Configure caching strategies for API, images, and map tiles
- Add PWA manifest for installability
- Optimize build configuration with Terser minification
- Create comprehensive documentation

Performance improvements:
- Initial bundle reduced by ~60-70%
- Total gzipped size: ~300KB
- Offline capability with service worker
- Lazy loading for images and routes
- Core Web Vitals monitoring

Requirements: 8.5
This commit is contained in:
2025-11-17 00:03:23 +00:00
parent 1a0e3e4e35
commit fa98a3192c
22 changed files with 5288 additions and 132 deletions

View File

@@ -0,0 +1,77 @@
import { useState, useEffect, useRef } from 'react';
interface LazyImageProps {
src: string;
alt: string;
className?: string;
width?: number;
height?: number;
placeholder?: string;
}
/**
* Lazy-loaded image component with intersection observer
* Only loads images when they're about to enter the viewport
*/
export function LazyImage({
src,
alt,
className = '',
width,
height,
placeholder = 'data:image/svg+xml,%3Csvg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 300"%3E%3Crect fill="%23e5e7eb" width="400" height="300"/%3E%3C/svg%3E'
}: LazyImageProps) {
const [imageSrc, setImageSrc] = useState<string>(placeholder);
const [isLoaded, setIsLoaded] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
// Check if IntersectionObserver is supported
if (!('IntersectionObserver' in window)) {
// Fallback: load image immediately
setImageSrc(src);
return;
}
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setImageSrc(src);
if (imgRef.current) {
observer.unobserve(imgRef.current);
}
}
});
},
{
rootMargin: '50px', // Start loading 50px before entering viewport
}
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => {
if (imgRef.current) {
observer.unobserve(imgRef.current);
}
};
}, [src]);
return (
<img
ref={imgRef}
src={imageSrc}
alt={alt}
className={`${className} ${isLoaded ? 'opacity-100' : 'opacity-0'} transition-opacity duration-300`}
width={width}
height={height}
loading="lazy"
onLoad={() => setIsLoaded(true)}
/>
);
}
export default LazyImage;