Optimizing Images with NextJS Image

Introduction

The NextJS Image component is a powerful solution that helps automatically optimize images in web applications. The Image component has been implemented by NextJS with many superior functions that support displaying images more effectively than a standard HTML image tag, such as

  • Automatically resizing images to fit the device
  • Supporting modern image formats (like WebP and AVIF)
  • Preventing Cumulative Layout Shift (CLS) by holding space for images
  • Integrating a built-in lazy loading mechanism to speed up initial page load speeds.

Detail

In this article, I will provide an example of loading a product list in ecommerce pages so you can see the advantages of NextJS Image in automatic image optimization.

Create file app/image/types.ts

export interface Product {
  id: number
  title: string
  description: string
  price: number
  thumbnail: string
  category: string
}

export interface ProductResponse {
  products: Product[]
  total: number
  skip: number
  limit: number
}

export interface Category {
  id: number
  name: string
  img: string
}

Create file app/image/api.ts

import type {ProductResponse} from './types'

export async function getProducts(): Promise<ProductResponse> {
  const res = await fetch('https://dummyjson.com/products?limit=8')
  if (!res.ok) throw new Error('Failed to fetch')
  return res.json()
}

export const CATEGORIES = [
  {id: 1, name: 'Electronics', img: 'https://picsum.photos/200/200?random=1'},
  {id: 2, name: 'Fashion', img: 'https://picsum.photos/200/200?random=2'},
  {id: 3, name: 'Home', img: 'https://picsum.photos/200/200?random=3'},
  {id: 4, name: 'Beauty', img: 'https://picsum.photos/200/200?random=4'},
]

Create file app/image/ProductList.tsx

import Image from 'next/image'
import type {Product} from './types'

interface ProductListProps {
  products: Product[]
}

export default function ProductList({products}: ProductListProps) {
  return (
    <div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-6">
      {products.map(product => (
        <div key={product.id} className="group border p-4">
          <div className="relative aspect-square">
            <Image
              src={product.thumbnail}
              alt={product.title}
              fill
              sizes="(max-width: 768px) 100vw, 25vw"
              loading="lazy"
            />
          </div>
          <h3>{product.title}</h3>
          <p>${product.price}</p>
        </div>
      ))}
    </div>
  )
}

Create file app/image/page.tsx

import Image from 'next/image'
import {CATEGORIES, getProducts} from './api'
import ProductList from './ProductList'

export default async function HomePage() {
  const data = await getProducts()
  return (
    <main className="max-w-7xl mx-auto px-4 py-8">
      <section className="relative w-full h-[400px] mb-12 rounded-xl overflow-hidden">
        <Image
          src="https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe"
          alt="E-commerce Banner"
          fill
          priority
          sizes="100vw"
          className="object-cover"
        />
        <div className="absolute inset-0 bg-black/20 flex items-center justify-center">
          <h1 className="text-white text-5xl font-bold">New Collection</h1>
        </div>
      </section>

      <section className="mb-12">
        <h2 className="text-2xl font-bold mb-6">Categories</h2>
        <div className="flex gap-6 overflow-x-auto pb-4 scrollbar-hide">
          {CATEGORIES.map(cat => (
            <div key={cat.id} className="flex-shrink-0 text-center">
              <div className="relative w-24 h-24 mb-2">
                <Image
                  src={cat.img}
                  alt={cat.name}
                  fill
                  loading="eager"
                  sizes="96px"
                  className="rounded-full object-cover border-2 border-gray-100"
                />
              </div>
              <span className="text-sm font-medium">{cat.name}</span>
            </div>
          ))}
        </div>
      </section>
      <section>
        <h2 className="text-2xl font-bold mb-6">Featured Products</h2>
        <ProductList products={data.products} />
      </section>
    </main>
  )
}

The image loading priority order includes

  • priority: Prioritize immediate loading, only applied to a few of the most important above-the-fold images that will be shown directly to the user as soon as they access the site, this is the most important main factor that helps improve LCP (Largest Contentful Paint) metrics
  • loading=eager: to ensure these images are always ready when the user first enters the page.
  • loading=lazy: the default value of Image, helping the browser save bandwidth, it can be used for most images on the page to only load images when the user scrolls to them.

Distinguishing width, height and sizes

  • width, height: the size of the placeholder space the browser will reserve to load the image into
  • sizes: this is the factor to identify conditions and load images corresponding to device size

As in the code above, you see I used

  • sizes="(max-width: 768px) 100vw, 25vw": NextJS will create corresponding resized images

    • if the screen size is 768px or less like mobile then take an image almost as large as the mobile screen
    • if the screen size is larger than 768px for desktop, at this point it will divide into 4 columns so only take an image as small as 1/4 of the desktop screen
  • sizes="96px": NextJS will create small resized images (like 128px, 256px) instead of large images for Desktop, the browser will choose whichever image is closest to 96px to load.

You also need to configure the remote pattern for images in next.config.ts.
import type {NextConfig} from 'next'

const nextConfig: NextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: '**.dummyjson.com',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
},
{
protocol: 'https',
hostname: 'picsum.photos',
},
],
},
}

export default nextConfig

Note that for most image types (such as PNG, WebP and JPG), resizing is supported, but it is not supported for SVG images because they are already inherently optimized and work well across various sizes, so NextJS will not perform resizing for this image type.

To make image resizing work, you need to rebuild the project and start in production mode, after viewing the pages as desktop and mobile you should access the .next/cache/images folder where automatically resized image files corresponding to different sizes will appear.




Happy coding!

See more articles here.

Comments

Popular posts from this blog

All Practice Series

Kubernetes Deployment for Zero Downtime

Deploying a NodeJS Server on Google Kubernetes Engine

Setting up Kubernetes Dashboard with Kind

Sitemap

React Practice Series

Monitoring with cAdvisor, Prometheus and Grafana on Docker

DevOps Practice Series

A Handy Guide to Using Dynamic Import in JavaScript

Using Kafka with Docker and NodeJS