My Code Chronicles #1: Optimizing Images in Next.js with a Custom Component

First Contentful Paint (FCP) is a critical performance metric that measures the time it takes for the first piece of content to appear on the screen after a user navigates to a page. A fast FCP is essential for providing a good user experience, as it gives users visual feedback that the page is loading. However, using large, high-quality images can significantly delay the FCP, which impacts perceived performance. At the same time, high-quality images are vital for creating a visually appealing and professional website. Balancing fast load times and maintaining good image quality is often challenging. To address this, I created a custom Next.js client component for images. This component initially renders a placeholder block when the page is built. After hydration, it uses JavaScript to load the image and only displays it once it has been fully loaded. Let’s dive into how it works. Key Features Initial Placeholder Rendering: During the page’s build and hydration phases, the component renders a placeholder (e.g., a skeleton or loader) instead of the actual image. JavaScript Image Preloading: After hydration, the browser preloads the image using JavaScript. The image is only displayed once it has been fully loaded. Responsive Sizing: The component supports responsive sizing via the setSizesprop, which allows developers to define image sizes for different screen breakpoints. Loader Feedback: While the image is loading, a custom loader (e.g., a spinner or skeleton) is displayed to provide a better user experience. Leverages Next.js Optimization: The component integrates seamlessly with Next.js’s for optimized image handling. Code Overview Here’s the complete implementation of the MyImagecomponent: "use client"; import React, { ComponentProps, useEffect, useState } from "react"; import NextImage, { StaticImageData } from "next/image"; import ImageLoader from "./loader"; import { cn } from "@/lib/utils"; type NextImageProps = ComponentProps; type ImageProps = NextImageProps & { showLoaderIcon?: boolean; setSizes?: { default: number; sm?: number; md?: number; lg?: number; xl?: number; }; src: string | StaticImageData; }; const generateSize = (sizes: ImageProps["setSizes"]): string | undefined => { let size: undefined | string = undefined; if (sizes) { size = `${sizes.default}vw`; if (sizes.sm) size = `(min-width: 640px) ${sizes.sm}vw, ${size}`; if (sizes.md) size = `(min-width: 768px) ${sizes.md}vw, ${size}`; if (sizes.lg) size = `(min-width: 1024px) ${sizes.lg}vw, ${size}`; if (sizes.xl) size = `(min-width: 1280px) ${sizes.xl}vw, ${size}`; } return size; }; export default function MyImage(props: ImageProps) { const { className, src } = props; const [isLoading, setIsLoading] = useState(true); useEffect(() => { if (document) { const image = new Image(); image.onerror = () => { // Handle error if needed }; image.src = typeof src === "string" ? src : src.src; image.onload = () => { setIsLoading(false); }; } }, []); const newProps = { ...props, className: cn(className, "relative animation-fade-in"), fill: props.width !== undefined || props.height !== undefined ? false : true, sizes: props.width !== undefined || props.height !== undefined ? undefined : props.sizes ?? generateSize(props.setSizes), }; if (isLoading) return ( ); if (newProps.showLoaderIcon !== undefined) delete newProps.showLoaderIcon; if (newProps.setSizes !== undefined) delete newProps.setSizes; if (newProps.fill) return ( ); return ( ); } How It Works Initial Placeholder Rendering: When the component is mounted, a placeholder is shown using the ImageLoader component. import React from "react"; import { FaImage } from "react-icons/fa"; // Types interface ImageLoaderProps { className?: string; showIcon?: boolean; } export default function ImageLoader({ className, showIcon }: ImageLoaderProps) { className = className || "w-full h-full"; showIcon = showIcon ?? true; return ( {showIcon && ( )} ); } Preloading and State Management: The useEffecthook uses a native Image object to preload the image in the background. The isLoading state determines whether the placeholder or the actual image is displayed. Responsive Sizes: The generateSize function dynamically calculates the sizes attribute based on the screen’s viewport. Final Image Rendering: Once preloading is complete, the image is displayed using Next.js’s , with all the necessary props and optimizations. Conclusion This custom image component provides a robust solution for optimizing image performance in Next.js. By displaying a placeholder initially and loading images only after hydration, it ensures a faster FCP without sacrificing quality. Ho

Jan 17, 2025 - 21:23
My Code Chronicles #1: Optimizing Images in Next.js with a Custom Component

First Contentful Paint (FCP) is a critical performance metric that measures the time it takes for the first piece of content to appear on the screen after a user navigates to a page. A fast FCP is essential for providing a good user experience, as it gives users visual feedback that the page is loading.

However, using large, high-quality images can significantly delay the FCP, which impacts perceived performance. At the same time, high-quality images are vital for creating a visually appealing and professional website. Balancing fast load times and maintaining good image quality is often challenging.

To address this, I created a custom Next.js client component for images. This component initially renders a placeholder block when the page is built. After hydration, it uses JavaScript to load the image and only displays it once it has been fully loaded. Let’s dive into how it works.

Key Features

  • Initial Placeholder Rendering: During the page’s build and hydration phases, the component renders a placeholder (e.g., a skeleton or loader) instead of the actual image.

  • JavaScript Image Preloading: After hydration, the browser preloads the image using JavaScript. The image is only displayed once it has been fully loaded.

  • Responsive Sizing: The component supports responsive sizing via the setSizesprop, which allows developers to define image sizes for different screen breakpoints.

  • Loader Feedback: While the image is loading, a custom loader (e.g., a spinner or skeleton) is displayed to provide a better user experience.

  • Leverages Next.js Optimization: The component integrates seamlessly with Next.js’s for optimized image handling.

Code Overview

Here’s the complete implementation of the MyImagecomponent:

"use client";

import React, { ComponentProps, useEffect, useState } from "react";
import NextImage, { StaticImageData } from "next/image";

import ImageLoader from "./loader";
import { cn } from "@/lib/utils";

type NextImageProps = ComponentProps;
type ImageProps = NextImageProps & {
  showLoaderIcon?: boolean;
  setSizes?: {
    default: number;
    sm?: number;
    md?: number;
    lg?: number;
    xl?: number;
  };
  src: string | StaticImageData;
};

const generateSize = (sizes: ImageProps["setSizes"]): string | undefined => {
  let size: undefined | string = undefined;
  if (sizes) {
    size = `${sizes.default}vw`;
    if (sizes.sm) size = `(min-width: 640px) ${sizes.sm}vw, ${size}`;
    if (sizes.md) size = `(min-width: 768px) ${sizes.md}vw, ${size}`;
    if (sizes.lg) size = `(min-width: 1024px) ${sizes.lg}vw, ${size}`;
    if (sizes.xl) size = `(min-width: 1280px) ${sizes.xl}vw, ${size}`;
  }

  return size;
};

export default function MyImage(props: ImageProps) {
  const { className, src } = props;

  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    if (document) {
      const image = new Image();
      image.onerror = () => {
        // Handle error if needed
      };
      image.src = typeof src === "string" ? src : src.src;
      image.onload = () => {
        setIsLoading(false);
      };
    }
  }, []);

  const newProps = {
    ...props,
    className: cn(className, "relative animation-fade-in"),
    fill:
      props.width !== undefined || props.height !== undefined ? false : true,
    sizes:
      props.width !== undefined || props.height !== undefined
        ? undefined
        : props.sizes ?? generateSize(props.setSizes),
  };

  if (isLoading)
    return (
      
    );

  if (newProps.showLoaderIcon !== undefined) delete newProps.showLoaderIcon;
  if (newProps.setSizes !== undefined) delete newProps.setSizes;
  if (newProps.fill)
    return (
      
); return ( ); }

How It Works

  • Initial Placeholder Rendering: When the component is mounted, a placeholder is shown using the ImageLoader component.
import React from "react";
import { FaImage } from "react-icons/fa";

// Types
interface ImageLoaderProps {
  className?: string;
  showIcon?: boolean;
}

export default function ImageLoader({ className, showIcon }: ImageLoaderProps) {
  className = className || "w-full h-full";
  showIcon = showIcon ?? true;
  return (
    
{showIcon && ( )}
); }
  • Preloading and State Management: The useEffecthook uses a native Image object to preload the image in the background. The isLoading state determines whether the placeholder or the actual image is displayed.

  • Responsive Sizes: The generateSize function dynamically calculates the sizes attribute based on the screen’s viewport.

  • Final Image Rendering: Once preloading is complete, the image is displayed using Next.js’s , with all the necessary props and optimizations.

Conclusion

This custom image component provides a robust solution for optimizing image performance in Next.js. By displaying a placeholder initially and loading images only after hydration, it ensures a faster FCP without sacrificing quality.

However, there’s a small challenge I’m still working on: when the image appears after being loaded in the background, there’s a short delay. I suspect this delay is related to the rendering process of the image. If anyone has insights or solutions to address this, I would greatly appreciate your input.

Stay tuned for more posts in My Code Chronicles, where I share technical tips, challenges, and solutions from my development journey!