In Part 1, we covered device pixel ratio detection and React image components. Now let's explore Next.js Image optimization, advanced lazy loading, and performance monitoring techniques.
Next.js Image Component: Built-in Optimization
Next.js provides a powerful next/image component with automatic optimization, but we can enhance it with device-aware strategies.
Enhanced Next.js Image Component
// components/SmartImage.jsx
'use client';
import Image from 'next/image';
import { useState } from 'react';
import { useDeviceInfo } from '../hooks/useDeviceInfo';
const SmartImage = ({
src,
alt,
width,
height,
priority = false,
className = ''
}) => {
const { dpr, isMobile, isTablet, connection } = useDeviceInfo();
const [isLoading, setIsLoading] = useState(true);
// Adjust quality based on connection and device
const getQuality = () => {
if (connection === '2g' || connection === 'slow-2g') return 40;
if (connection === '3g') return 60;
if (isMobile) return 75;
return 85;
};
// Calculate sizes attribute for responsive images
const getSizes = () => {
if (isMobile) return '100vw';
if (isTablet) return '50vw';
return '33vw';
};
return (
<div className={`relative ${className}`}>
{isLoading && (
<div className="absolute inset-0 bg-gray-200 animate-pulse" />
)}
<Image
src={src}
alt={alt}
width={width}
height={height}
quality={getQuality()}
sizes={getSizes()}
priority={priority}
placeholder="blur"
blurDataURL={generateBlurDataURL(width, height)}
onLoad={() => setIsLoading(false)}
className={isLoading ? 'opacity-0' : 'opacity-100 transition-opacity'}
style={{ objectFit: 'cover' }}
/>
</div>
);
};
const generateBlurDataURL = (width, height) => {
// Generate a tiny base64 placeholder
return `data:image/svg+xml;base64,${Buffer.from(
`<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">
<rect width="${width}" height="${height}" fill="#e5e7eb"/>
</svg>`
).toString('base64')}`;
};
export default SmartImage;
Custom Image Loader for CDN Integration
// next.config.js
module.exports = {
images: {
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
formats: ['image/webp', 'image/avif'],
loader: 'custom',
loaderFile: './lib/imageLoader.js',
},
};
// lib/imageLoader.js
export default function imageLoader({ src, width, quality }) {
const dpr = typeof window !== 'undefined'
? Math.min(window.devicePixelRatio || 1, 3)
: 1;
// Example with Cloudinary
return `https://res.cloudinary.com/your-cloud/image/upload/w_${width},q_${quality || 75},dpr_${dpr},f_auto/${src}`;
// Example with Imgix
// return `https://your-domain.imgix.net/${src}?w=${width}&q=${quality}&dpr=${dpr}&auto=format`;
}
Advanced Lazy Loading with Intersection Observer
// components/LazyImage.jsx
import { useEffect, useRef, useState } from 'react';
import { useDeviceInfo } from '../hooks/useDeviceInfo';
const LazyImage = ({ src, alt, className = '', threshold = 0.1 }) => {
const [isVisible, setIsVisible] = useState(false);
const [imageSrc, setImageSrc] = useState('');
const imgRef = useRef(null);
const { dpr, width } = useDeviceInfo();
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true);
observer.disconnect();
}
},
{
threshold,
rootMargin: '50px' // Start loading 50px before visible
}
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, [threshold]);
useEffect(() => {
if (isVisible) {
const optimizedSrc = getOptimizedImageUrl(src, width, dpr);
setImageSrc(optimizedSrc);
}
}, [isVisible, src, width, dpr]);
const getOptimizedImageUrl = (url, viewportWidth, deviceDpr) => {
const imageWidth = Math.ceil(viewportWidth * deviceDpr);
return `${url}?w=${imageWidth}&dpr=${deviceDpr}&q=75&auto=format`;
};
return (
<img
ref={imgRef}
src={imageSrc || 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'}
alt={alt}
className={className}
loading="lazy"
/>
);
};
export default LazyImage;
Progressive Image Loading
Load low-quality placeholder first, then full image:
// components/ProgressiveImage.jsx
import { useState, useEffect } from 'react';
import { useDeviceInfo } from '../hooks/useDeviceInfo';
const ProgressiveImage = ({ src, alt, className = '' }) => {
const [currentSrc, setCurrentSrc] = useState('');
const [isLoading, setIsLoading] = useState(true);
const { dpr, width } = useDeviceInfo();
useEffect(() => {
// Load tiny placeholder first (LQIP - Low Quality Image Placeholder)
const lqip = `${src}?w=20&q=10&blur=50`;
const fullImage = `${src}?w=${width * dpr}&q=80&dpr=${dpr}`;
// Load LQIP immediately
const lqipImg = new Image();
lqipImg.src = lqip;
lqipImg.onload = () => {
setCurrentSrc(lqip);
// Load full image in background
const fullImg = new Image();
fullImg.src = fullImage;
fullImg.onload = () => {
setCurrentSrc(fullImage);
setIsLoading(false);
};
};
}, [src, width, dpr]);
return (
<img
src={currentSrc}
alt={alt}
className={`${className} ${isLoading ? 'blur-sm' : 'blur-0'} transition-all duration-500`}
style={{ filter: isLoading ? 'blur(10px)' : 'none' }}
/>
);
};
export default ProgressiveImage;
Performance Monitoring
Track image loading performance to optimize further:
// utils/imagePerformance.js
export const monitorImagePerformance = (imageSrc) => {
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.name.includes(imageSrc)) {
console.log('Image Performance Metrics:', {
url: entry.name,
duration: entry.duration,
size: entry.transferSize,
startTime: entry.startTime,
renderTime: entry.responseEnd - entry.fetchStart
});
// Send to analytics
if (typeof gtag !== 'undefined') {
gtag('event', 'image_performance', {
image_url: entry.name,
load_time: entry.duration,
image_size: entry.transferSize
});
}
}
});
});
observer.observe({ entryTypes: ['resource'] });
}
};
Best Practices Checklist
✅ Use WebP/AVIF with fallbacks
✅ Implement device pixel ratio detection
✅ Serve appropriately sized images per device
✅ Use lazy loading for below-fold images
✅ Implement progressive loading (LQIP)
✅ Add blur placeholders during load
✅ Cache aggressively with CDN
✅ Monitor Core Web Vitals (LCP, CLS)
✅ Consider network speed in quality decisions
✅ Use priority prop for above-fold images
✅ Optimize with CDN transforms
✅ Set proper sizes attribute
Real-World Performance Impact
Implementing these techniques typically yields:
- 50-70% reduction in image payload
- 2-3x faster Largest Contentful Paint (LCP)
- 40-60% improvement in mobile performance scores
- Better SEO rankings due to Core Web Vitals
- Reduced bounce rates from faster loading
Conclusion
Device-aware image optimization is non-negotiable for modern web applications. By detecting device capabilities and serving appropriately sized images, you dramatically improve performance, user experience, and SEO rankings. Start with Next.js Image component, add device detection, and progressively enhance with lazy loading and monitoring.
Keywords: Next.js image optimization, lazy loading, progressive images, Core Web Vitals, LCP optimization, responsive images, WebP, AVIF
Top comments (0)