DEV Community

sizan mahmud0
sizan mahmud0

Posted on

Advanced Image Optimization in React/Next.js: Device-Based Responsive Images for Peak Performance (Part 2)

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;
Enter fullscreen mode Exit fullscreen mode

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',
  },
};
Enter fullscreen mode Exit fullscreen mode
// 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`;
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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'] });
  }
};
Enter fullscreen mode Exit fullscreen mode

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)