Optimización de Imágenes y Lazy Loading en Next.js 15: Guía Completa

InicionextjsOptimización en Next.js 15
Optimización de Imágenes y Lazy Loading en Next.js 15: Guía Completa
Optimización de Imágenes y Lazy Loading en Next.js 15: Guía Completa

by Juan Carlos García

26-My-2025

(1)

Suscribirme al canal:

En este clase del  Curso de Next.js desde cero, descubrirás cómo implementar técnicas avanzadas de optimización de imágenes y lazy loading en Next.js 15. Me he dado cuenta que el mayor peso de una página web proviene de imágenes, y una carga optimizada puede mejorar el LCP considerablemente.

Aprenderás a usar el componente Image de Next.js con todas sus capacidades, incluyendo formatos modernos como WebP y AVIF, tamaños responsivos y carga diferida inteligente. También dominarás técnicas de lazy loading para componentes no críticos, reduciendo el bundle inicial y mejorando los Core Web Vitals.

Este conocimiento es esencial para desarrollar aplicaciones competitivas con Next.js 15 que ofrezcan una experiencia de usuario excepcional.

Image de Next.js

Maximiza el rendimiento con imágenes optimizadas y carga diferida en Next.js

lazy loading en Next.js 15

Diseño de páginas web EWebik

🧐 Autoevaluación: Optimización en Next.js 15

¿Cuál es la principal ventaja de usar el componente Image de Next.js 15 para optimización de imágenes?

¿Qué parámetro del componente dynamic de Next.js 15 controla si un componente debe renderizarse en el servidor?

¿Cuál de estas estrategias NO es recomendable para optimizar el lazy loading en Next.js 15?

¡No te puedes perder las nuevas clases 🧐!

Optimización de Imágenes en Next.js 15: Técnicas Profesionales

En Next.js 15, la optimización de imágenes es clave para mejorar el rendimiento web y los Core Web Vitals. El componente Image nativo ofrece optimizaciones automáticas que pueden reducir el peso de las imágenes hasta un 70% sin perder calidad.

Uso Básico del Componente Image

El componente Image de Next.js 15 incluye optimizaciones automáticas, para nuestro curso utilizamos Image para crear la imagen principal de nuestro post del blog que hemos desarrollado en clases pasadas.

//src/components/MainImage.tsx
"use client";
import Image from "next/image";

export default function MainImage({ src, alt }: { src: string; alt: string }) {
  return (
    <Image
      src={src}
      alt={alt}
      width={800}
      height={420}
      priority={true} // Prioriza carga para imágenes
      quality={85} // Calidad óptima para WebP
      className="w-full h-auto"
    />
  );
}

Configuración Avanzada en next.config.js

Si tu imagen no se muestra, recuerda configurar tu archivo next.config, en nuestro caso agregamos un dominio cdn.ewebik.com para cargar imágenes remotas, tú debes configurarlo según tu caso:


// next.config.js
module.exports = {
  images: {
    // Dominios externos permitidos (¡Necesario para <Image> con src externo!)
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.ewebik.com",
        port: "",
        pathname: "/**", // Permite cualquier path dentro de ese hostname
      },
      {
        protocol: "https",
        hostname: "cdn.ewebik.com",
      },
    ],
    // domains: ['images.unsplash.com', 'cdn.example.com'], // Forma anterior, ahora se prefiere remotePatterns
    // Tamaños de imagen a generar
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840],
    formats: ["image/avif", "image/webp","image/jpeg"], // Formatos a intentar servir (además del original)
  }
}

Ejercicio Práctico: Galería Optimizada

Crea una galería con lazy loading y tamaños responsivos, ahora para que podamos ver como es que Next.js retrasa la carga de las imágenes, vamos a crear una galería de imágenes, donde, explícitamente asignamos loading="lazy", con lo que indicamos que la carga sea diferida, por default Next.js cargara las imágenes de forma diferida, es mejor utilizar priority para indicar que la carga debe ser lo antes posible.

//src/components/Gallery.tsx
"use client";
import Image from "next/image";
interface Img {
  src: string;
  alt: string;
}
interface GalleryProps {
  images: Img[];
}

export default function Gallery({ images }: GalleryProps) {
  return (
    <div className="grid grid-cols-3 gap-4">
      {images.map((img, index) => (
        <div key={"img-" + index} className="aspect-3/2">
          <Image
            src={img.src}
            alt={img.alt}
            width={800}
            height={420}
            loading="lazy" // Carga diferida
            quality={75}
            sizes="(max-width: 768px) 100vw, 33vw" // Tamaños responsivos
            className="object-fill w-full h-auto"
          />
        </div>
      ))}
    </div>
  );
}


Cambios en nuestra api y page del blog

Te dejo el código de la API y page del blog, para que puedas replicar el ejemplo:

// app/api/posts/[post]/route.ts
import { NextResponse } from "next/server";

const images = [
  {
    src: "/ewebik/img1.jpg",
    alt: "Imagen 1",
  },
  {
    src: "/ewebik/img2.jpg",
    alt: "Imagen 2",
  },
  
];
export async function GET(
  request: Request,
  { params }: { params: Promise<{ post: string }> }
) {
  try {
    const posts = [
      {
        id: "post-1",
        title: "Post 1",
        slug: "/blog/post-1",
        createdAt: "2025-04-01",
        image: {
          src: "/ewebik/img11.webp",
          alt: "Imagen 1",
        },
        images,
      },
      {
        id: "post-2",
        title: "Post 2",
        slug: "/blog/post-2",
        createdAt: "2025-04-02",
        image: {
          src: "/ewebik/img2.jpg",
          alt: "Imagen 2",
        },
        images,
      },
      {
        id: "post-3",
        title: "Post 3",
        slug: "/blog/post-3",
        createdAt: "2025-04-03",
        image: {
          src: "/ewebik/img3.jpg",
          alt: "Imagen 3",
        },
        images,
      },
    ];

    const { post } = await params;
    if (!post || post === "") {
      return NextResponse.json({ error: "Not found" }, { status: 404 });
    }
    const dataPost = posts.find((p) => p.id === post);

    if (dataPost) {
      return NextResponse.json({ data: dataPost, error: "" });
    } else {
      return NextResponse.json({ error: "Not found" }, { status: 404 });
    }
  } catch (error) {
    return NextResponse.json(
      { error: "Failed to fetch posts: " + error },
      { status: 500 }
    );
  }
}
// app/blog/[slug]/page.tsx

import Gallery from "@/components/Gallery";
import MainImage from "@/components/MainImage";
import { notFound } from "next/navigation";

interface Img {
  src: string;
  alt: string;
}

interface Post {
  id: string;
  title: string;
  slug: string;
  createdAt: string;
  image: Img;
  images: Img[];
}

/**
 * Definimos las urls de nuestro sitio
 */
export async function generateStaticParams() {
  const posts: Post[] = await fetch(
    `${process.env.NEXT_PUBLIC_API_URL}/api/posts`,
    {
      next: { tags: ["posts"], revalidate: 3600 },
    }
  ).then((res) => res.json());
  return posts.map((post) => ({ slug: post.slug }));
}

export const revalidate = 3600; // Revalida cada hora

async function getPost(slug: string): Promise<Post | undefined> {
  try {
    const response = await fetch(
      `${process.env.NEXT_PUBLIC_API_URL}/api/posts/${slug}`,
      {
        next: { tags: ["posts"], revalidate: 3600 },
      }
    );
    if (response.status !== 200) {
      return undefined;
    }
    return (await response.json())?.data as Post;
  } catch (error) {
    console.log("Error getPost -- " + error);
    return undefined;
  }
}

export default async function PostPage({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const page = await params;
  const post = await getPost(page.slug);

  if (!post) {
    notFound();
  }

  return (
    <article>
      <h1>{post?.title}</h1>
      <div className="w-full flex justify-center items-center">
        <div className="w-full md:w-1/2">
          <MainImage src={post?.image?.src} alt={post?.image?.alt} />
        </div>
      </div>
      <p>{post?.createdAt}</p>
      <p>{`Actualización: ${Date.now()}`}</p>
      <h2 className="text-4xl text-center my-6 font-bold">{"Imágenes"}</h2>
      <Gallery images={post?.images} />
    </article>
  );
}

Errores Comunes a Evitar

  • ⚠️ No definir width y height
  • ⚠️ Usar imágenes locales sin optimizar
  • ⚠️ Sobrecargar con imágenes de alta resolución
  • ⚠️ Olvidar el atributo alt en imágenes
  • ⚠️ Ignorar la compresión de SVG

Dominar la optimización de imágenes en Next.js 15 te permitirá crear aplicaciones hasta un 50% más rápidas, mejorando significativamente la experiencia de usuario y el posicionamiento SEO.

Diseño de páginas web EWebik

Lazy Loading de Componentes en Next.js

El lazy loading de componentes en Next.js 15 es una técnica esencial para mejorar el rendimiento de tus aplicaciones, al cargar componentes solo cuando son necesarios, reduciendo el tiempo de carga inicial y mejorando la experiencia del usuario.

¿Qué es el Lazy Loading en Next.js 15?

El lazy loading, o carga diferida, permite cargar componentes, imágenes o módulos de JavaScript solo cuando el usuario los necesita, en lugar de cargarlos todos al inicio. Next.js 15 ofrece varias formas de implementar esta técnica, especialmente útil para aplicaciones con muchos componentes o recursos pesados.

Entre las ventajas del lazy loading en Next.js 15 se encuentran:

  • Reducción del tamaño del bundle inicial
  • Mejora en los tiempos de carga
  • Optimización del rendimiento en dispositivos móviles
  • Mejor experiencia de usuario

Implementación Básica de Lazy Loading en Next.js 15

Next.js 15 soporta el lazy loading de componentes mediante la función dynamic de next/dynamic. Veamos un ejemplo básico:

import dynamic from 'next/dynamic';

const LazyComponent= dynamic(() => import('../components/LazyComponent'), {
  loading: () => <p>Cargando...</p>,
  ssr: false
});

function HomePage() {
  return (
    <div>
      <h1>Página Principal</h1>
      <LazyComponent />
    </div>
  );
}

export default HomePage;

En este ejemplo, LazyComponent solo se cargará cuando sea renderizado en la página, no durante la carga inicial, la opción ssr: false indica que este componente no debe ser renderizado en el servidor, un componente no debe renderizarse en el servidor si:

  1. Componentes que dependen del entorno del navegador:
    1. APIs del navegador como window, document, localStorage
    2. Librerías que acceden a APIs específicas del cliente
  2. Componentes pesados que no son críticos para el SEO:
    1. Widgets secundarios
    2. Elementos interactivos complejos
    3. Contenido detrás de autenticación
  3. Librerías que no son compatibles con SSR:
    1. Algunas librerías de gráficos (D3.js, Three.js)
    2. Ciertos plugins de mapas
    3. Editores WYSIWYG
  4. Componentes que requieren autenticación previa:
    1. Dashboards de usuario
    2. Carritos de compra
    3. Paneles de administración
  5. Componentes con dependencias pesadas:
    1. Editores de código
    2. PDF viewers

Lazy Loading de Imágenes en Next.js 15

Cómo vimos anteriormente, Next.js 15 incluye un componente Image optimizado que soporta lazy loading por defecto. Podemos combinar lazy loading de Image con lazy loading de componentes, entonces, vamos a cargar de forma diferida la imagen que al mismo tiempo hará la carga diferida de nuestra imagen.

//src/components/ProductImage.tsx
"use client";
import Image from "next/image";
interface Img {
  src: string;
  alt: string;
}

export function ProductImage({ src, alt }: Img) {
  return (
    <div>
      <h1>Nuestro Producto</h1>
      <Image
        src={src}
        alt={alt}
        width={800}
        height={600}
        priority={false} // Esto habilita el lazy loading
      />
    </div>
  );
}

export default ProductImage;

El componente Image en Next.js 15 carga las imágenes de forma diferida por defecto cuando están fuera del viewport inicial, optimizando así el rendimiento.

Ejemplo Avanzado

Para proyectos que usan TypeScript, podemos implementar lazy loading con tipado seguro, cargaremos la imagen del componente ProductImage. únicamente cuando sea necesario, es decir, hasta que el cliente lo solicite, esto quiere decir que no formara parte del budle inicial del proyecto.

Creando Product.tsx

Creamos un componente de cliente Product , en cual cargaremos de forma diferida ProductImage, al hacer la prueba notaremos que cuando mostramos la imagen, el componente diferido aparecerá como un archivo separado.

//src\components\Product.tsx

"use client";

import dynamic from "next/dynamic";
import { Suspense, useState } from "react";

interface Img {
  src: string;
  alt: string;
}

// Componente lazy con tipos TypeScript

const ProductImage = dynamic<Img>(
  () =>
    new Promise((resolve) =>
      setTimeout(() => {
        resolve(import("@/components/ProductImage"));
      }, 2000)
    ),
  {
    loading: () => <div>Cargando imagen...</div>,
    ssr: false,
  }
);

//import ProductImage from "@/components/ProductImage";

export const Product = ({ productId }: { productId: string }) => {
  const [showImage, setShowImage] = useState(false);
  return (
    <>
      <button
        className="bg-red-500 text-white my-6 p-2 cursor-pointer"
        onClick={() => {
          setShowImage(!showImage);
        }}
      >
        {`Mostrar imagen del producto: ${productId}`}
      </button>
      {showImage && (
        <Suspense fallback={<div>Cargando imagen del producto...</div>}>
          <ProductImage src="/ewebik/img1.jpg" alt="Producto" />
        </Suspense>
      )}
    </>
  );
};

export default Product;

Modificando la página del producto ProductPage

Recuerda en clases anteriores creamos ProductPage, ahora, lo modificamos para importar nuestro nuevo componente Product y realizar nuestras pruebas.

// app/products/[id]/page.tsx

import Product from "@/components/Product";

interface ProductPageProps {
  params: Promise<{ id: string }>;
}

export default async function ProductPage({ params }: ProductPageProps) {
  const page = await params;
  return (
    <div className="product-page">
      <h1>Detalles del Producto {page.id}</h1>
      {/* Contenido dinámico aquí */}
      <Product productId={page.id} />
    </div>
  );
}

Este ejemplo muestra cómo implementar lazy loading en Next.js 15 con TypeScript, incluyendo:

  • Tipado de props para el componente lazy
  • Manejo de estados de carga
  • Integración con React Suspense

En resumen, al hacer la carga diferida de un componente, el componente diferido aparecerá como un archivo separado.

Optimización de Rendimiento en Next.js 15 con Lazy Loading

Para maximizar los beneficios del lazy loading, considera estas buenas prácticas:

  1. Agrupa componentes relacionados: Usa dynamic imports para secciones completas de tu aplicación
  2. Precarga estratégica: Carga componentes que probablemente se necesitarán pronto
  3. Usa skeletons: Muestra placeholders durante la carga para mejor UX

El lazy loading de componentes en Next.js 15 es una técnica poderosa para optimizar el rendimiento de tus aplicaciones, al implementar carga diferida de componentes, imágenes y módulos, puedes reducir significativamente el tiempo de carga inicial y mejorar la experiencia del usuario, especialmente en dispositivos móviles o conexiones lentas.

Diseño de páginas web EWebik

🧐 Autoevaluación: Optimización en Next.js 15

¿Cuál es la principal ventaja de usar el componente Image de Next.js 15 para optimización de imágenes?

¿Qué parámetro del componente dynamic de Next.js 15 controla si un componente debe renderizarse en el servidor?

¿Cuál de estas estrategias NO es recomendable para optimizar el lazy loading en Next.js 15?

Juan Carlos

Juan Carlos García

Desarrollador de software / SEO / Ing. eléctrico - electrónico UNAM

Durante años he desarrollado plataformas dedicadas al rastreo satelital y varios sitios web que se encuentran en la primera página de Google, y hoy quiero compartir contigo lo que se en tecnologías como: Node JS, PHP, C# y Bases de datos.

Si quieres apoyarme sígueme en mis redes sociales y suscríbete a mi canal de YouTube.

© 2025 EWebik