Images account for 50-70% of a typical web page's total size. Serving optimized images based on device capabilities and screen density is crucial for performance, user experience, and SEO. In this comprehensive guide, we'll explore device pixel ratio (DPR) optimization and responsive image techniques using React and Next.js.
Understanding Device Pixel Ratio (DPR)
Device Pixel Ratio represents the relationship between physical pixels and CSS pixels. Modern devices have varying DPRs:
- Standard displays: DPR 1 (1920x1080 monitors)
- Retina/HD displays: DPR 2 (MacBook Pro, iPhone)
- Ultra HD displays: DPR 3+ (iPhone 15 Pro, Samsung Galaxy)
Serving a 1x image to a 3x device results in blurry visuals, while serving a 3x image to a 1x device wastes bandwidth.
Detecting Device Pixel Ratio in JavaScript
// Get current device pixel ratio
const dpr = window.devicePixelRatio || 1;
// Listen for DPR changes (when moving between monitors)
const dprQuery = window.matchMedia(`(resolution: ${dpr}dppx)`);
dprQuery.addEventListener('change', (e) => {
if (!e.matches) {
console.log('DPR changed, reload optimized images');
}
});
React Hook for Device Detection
Create a custom hook to detect device capabilities:
// hooks/useDeviceInfo.js
import { useState, useEffect } from 'react';
export const useDeviceInfo = () => {
const [deviceInfo, setDeviceInfo] = useState({
dpr: 1,
width: 0,
isMobile: false,
isTablet: false,
isDesktop: false,
connection: 'unknown'
});
useEffect(() => {
const updateDeviceInfo = () => {
const width = window.innerWidth;
const dpr = window.devicePixelRatio || 1;
// Get connection speed (if supported)
const connection = navigator.connection?.effectiveType || 'unknown';
setDeviceInfo({
dpr: Math.min(dpr, 3), // Cap at 3x for practicality
width,
isMobile: width < 768,
isTablet: width >= 768 && width < 1024,
isDesktop: width >= 1024,
connection
});
};
updateDeviceInfo();
window.addEventListener('resize', updateDeviceInfo);
return () => window.removeEventListener('resize', updateDeviceInfo);
}, []);
return deviceInfo;
};
Building a Responsive Image Component (React)
// components/OptimizedImage.jsx
import React, { useState, useEffect } from 'react';
import { useDeviceInfo } from '../hooks/useDeviceInfo';
const OptimizedImage = ({
src,
alt,
sizes = '100vw',
quality = 75,
className = ''
}) => {
const { dpr, width, connection } = useDeviceInfo();
const [imageSrc, setImageSrc] = useState('');
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
// Reduce quality on slow connections
const adjustedQuality = connection === '2g' ? 50 : quality;
// Calculate optimal image width
const imageWidth = calculateImageWidth(width, dpr);
// Build optimized URL (example with Cloudinary)
const optimizedSrc = buildOptimizedUrl(src, {
width: imageWidth,
quality: adjustedQuality,
dpr: dpr,
format: 'auto' // WebP when supported
});
setImageSrc(optimizedSrc);
}, [src, dpr, width, connection, quality]);
const calculateImageWidth = (viewportWidth, deviceDpr) => {
// Round to nearest standard size for better caching
const targetWidth = viewportWidth * deviceDpr;
const standardSizes = [320, 640, 750, 828, 1080, 1200, 1920, 2048, 3840];
return standardSizes.find(size => size >= targetWidth) || 3840;
};
const buildOptimizedUrl = (originalSrc, options) => {
// Example for Cloudinary
// Adapt this for your CDN (Imgix, Cloudflare Images, etc.)
const { width, quality, dpr, format } = options;
return `https://res.cloudinary.com/your-cloud/image/upload/w_${width},q_${quality},dpr_${dpr},f_${format}/${originalSrc}`;
};
return (
<div className={`image-container ${className}`}>
{isLoading && (
<div className="skeleton-loader" aria-label="Loading image" />
)}
<img
src={imageSrc}
alt={alt}
loading="lazy"
onLoad={() => setIsLoading(false)}
style={{ display: isLoading ? 'none' : 'block' }}
/>
</div>
);
};
export default OptimizedImage;
Using Picture Element for Art Direction
Different devices might need different image crops or compositions:
const ResponsiveArtDirectedImage = ({ images, alt }) => {
const { dpr } = useDeviceInfo();
return (
<picture>
{/* Mobile - portrait crop */}
<source
media="(max-width: 767px)"
srcSet={`
${images.mobile}?w=400&dpr=${dpr} 1x,
${images.mobile}?w=800&dpr=${dpr} 2x
`}
/>
{/* Tablet - landscape crop */}
<source
media="(min-width: 768px) and (max-width: 1023px)"
srcSet={`
${images.tablet}?w=768&dpr=${dpr} 1x,
${images.tablet}?w=1536&dpr=${dpr} 2x
`}
/>
{/* Desktop - full width */}
<source
media="(min-width: 1024px)"
srcSet={`
${images.desktop}?w=1200&dpr=${dpr} 1x,
${images.desktop}?w=2400&dpr=${dpr} 2x
`}
/>
<img src={images.desktop} alt={alt} loading="lazy" />
</picture>
);
};
Continue to Part 2 for Next.js Image optimization, lazy loading strategies, and performance monitoring techniques.
Keywords: React image optimization, device pixel ratio, responsive images, Next.js performance, DPR optimization, lazy loading, web vitals
Top comments (0)