Skip to content
SP StackPractices
beginner Por Mathias Paulenko

Implementar Lazy Loading para Imágenes, Componentes y Datos

Cómo diferir la carga de recursos no críticos hasta que sean necesarios, mejorando el tiempo de carga inicial de página, reduciendo el ancho de banda y optimizando Core Web Vitals.

Visión general

El lazy loading es una estrategia de optimización de rendimiento que difiere la carga de recursos no críticos hasta que son realmente necesarios. En lugar de descargar cada imagen, componente y chunk de datos en la carga inicial de página, la aplicación solo obtiene lo que el usuario puede ver o con el que puede interactuar inmediatamente. Los recursos bajo el fold, tabs ocultas o carruseles fuera de pantalla se cargan bajo demanda — típicamente cuando el usuario hace scroll, clic o hover.

Esta técnica mejora directamente tres métricas clave: Largest Contentful Paint (LCP) al priorizar contenido above-the-fold, Time to Interactive (TTI) al reducir el parsing de JavaScript en startup, y uso de ancho de banda acumulado al evitar descargas innecesarias. Los navegadores modernos proveen lazy loading nativo para imágenes vía el atributo loading="lazy", mientras que frameworks como React y Vue ofrecen code splitting a nivel de componente. Esta receta cubre imágenes, componentes UI y datos de API.

Cuándo usarlo

Usa esta receta cuando:

  • Una página contiene muchas imágenes o archivos multimedia bajo el viewport inicial
  • Tu bundle de JavaScript es grande y ralentiza el render inicial
  • Dashboards o paneles de admin tienen tabs, modales o secciones raramente accedidas
  • Listas o tablas cargan cientos de filas donde solo las primeras diez son visibles
  • Usuarios móviles en conexiones lentas experimentan tiempos de carga inicial largos

Solución

Lazy Loading Nativo de Imágenes (HTML)

<img src="hero.jpg" alt="Hero" loading="eager" width="1200" height="600">

<img src="gallery-1.jpg" alt="Gallery" loading="lazy" width="800" height="600">
<img src="gallery-2.jpg" alt="Gallery" loading="lazy" width="800" height="600">
<img src="gallery-3.jpg" alt="Gallery" loading="lazy" width="800" height="600">

Intersection Observer (JavaScript Vanilla)

const imageObserver = new IntersectionObserver((entries, observer) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target;
      img.src = img.dataset.src;
      img.removeAttribute('data-src');
      observer.unobserve(img);
    }
  });
}, {
  rootMargin: '50px 0px',
  threshold: 0.01
});

document.querySelectorAll('img[data-src]').forEach(img => {
  imageObserver.observe(img);
});

Lazy Loading en React (Componentes)

import { lazy, Suspense } from 'react';

const HeavyChart = lazy(() => import('./HeavyChart'));
const VideoPlayer = lazy(() => import('./VideoPlayer'));

function Dashboard() {
  return (
    <div>
      <SummaryCards />
      <Suspense fallback={<SkeletonChart />}>
        <HeavyChart />
      </Suspense>
      <Suspense fallback={<SkeletonPlayer />}>
        <VideoPlayer />
      </Suspense>
    </div>
  );
}

Lazy Loading de Datos (React Query / TanStack Query)

import { useInfiniteQuery } from '@tanstack/react-query';

function ProductList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
    useInfiniteQuery({
      queryKey: ['products'],
      queryFn: ({ pageParam = 1 }) =>
        fetch(`/api/products?page=${pageParam}`).then(r => r.json()),
      getNextPageParam: (lastPage) => lastPage.nextPage ?? undefined,
    });

  return (
    <>
      {data?.pages.map(page =>
        page.products.map(p => <ProductCard key={p.id} product={p} />)
      )}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? 'Cargando...' : 'Cargar más'}
        </button>
      )}
    </>
  );
}

Explicación

  • loading="lazy" nativo: el enfoque más simple. El navegador decide cuándo obtener la imagen basándose en la distancia al viewport. Soportado en todos los navegadores modernos. Siempre incluye width y height para prevenir layout shift (CLS).
  • Intersection Observer: una API performante que observa cuando los elementos entran al viewport. A diferencia de listeners de scroll, no corre continuamente en el main thread. Úsala para comportamientos de lazy loading personalizados, imágenes de fondo o iframes.
  • Code splitting de componentes: bundlers como Webpack, Vite y Rollup dividen automáticamente las llamadas a import() dinámicas en chunks separados. El lazy() de React envuelve estos chunks en un boundary de Suspense, mostrando un fallback mientras el chunk carga.
  • Scroll infinito / paginación: en lugar de cargar todos los datos upfront, obtén páginas a medida que el usuario hace scroll o clickea “cargar más”. Esto reduce el payload inicial de API y el costo de query de base de datos.

Variantes

TécnicaTipo de recursoSoporte de navegadorFrameworkMejor para
loading="lazy"ImágenesNavegadores modernosCualquieraGalerías de imágenes simples
Intersection ObserverImágenes, iframesNavegadores modernosCualquieraTriggers de scroll personalizados
import() dinámicoComponentes JSUniversalReact, Vue, SvelteChunks de UI grandes
Lazy loading por rutaRutasUniversalReact Router, Vue RouterNavegación SPA
Query infinitaDatosUniversalReact Query, SWRListas, feeds

Mejores prácticas

  • Establece dimensiones en imágenes lazy: sin width y height explícitos, el navegador no puede reservar espacio antes de que la imagen cargue. Esto causa Cumulative Layout Shift (CLS), una penalización de Core Web Vitals.
  • Usa eager para imágenes above-the-fold: la imagen hero, logo y CTA principal deberían cargar inmediatamente con loading="eager". Solo difiere contenido que el usuario no puede ver en el primer paint.
  • Preload recursos críticos: para contenido que probablemente se necesite pronto (por ejemplo, la siguiente ruta en una SPA), usa <link rel="preload"> o prefetch para que cargue en tiempo idle.
  • Muestra skeleton placeholders: mientras un componente o imagen lazy carga, muestra una UI skeleton ligera que coincida con el layout final. Evita espacios en blanco o contenido que salte.
  • Respeta prefers-reduced-data: algunos usuarios habilitan modo de ahorro de datos. Honra esto reduciendo o deshabilitando contenido heavy lazy-loaded como videos de autoplay.

Errores comunes

  • Lazy loading la imagen LCP: el elemento de largest contentful paint nunca debería ser lazy loaded. Si la imagen hero tiene loading="lazy", LCP se retrasará hasta que el usuario haga scroll — derrotando el propósito.
  • No manejar errores: si una imagen lazy falla al cargar (error de red, 404), el usuario ve un icono roto o spinner infinito. Agrega handlers onerror e imágenes de fallback.
  • Over-splitting de componentes: dividir cada componente en su propio chunk crea excesivas peticiones HTTP. Agrupa componentes relacionados y divide solo chunks mayores a 20-30KB.
  • Olvidar server-side rendering: si un componente lazy es necesario para SSR o paint inicial, bloqueará el renderizado. Usa flags específicos de framework como ssr: true o carga eager para contenido above-the-fold.

Preguntas frecuentes

P: ¿El lazy loading perjudica el SEO? R: No. Googlebot renderiza imágenes y contenido lazy-loaded. Mientras las imágenes estén en el HTML inicial o cargadas vía JavaScript estándar (no interacción de usuario), los motores de búsqueda las indexarán. Usa fallbacks <noscript> para seguridad absoluta.

P: ¿Cuál es la diferencia entre lazy loading y prefetching? R: El lazy loading difiere hasta que se necesita. El prefetching carga por adelantado durante tiempo idle. Usa lazy loading para contenido below-the-fold y prefetching para objetivos de navegación probables.

P: ¿Puedo hacer lazy load de CSS? R: Sí. Usa rel="preload" para CSS crítico y carga hojas de estilo no críticas asíncronamente con el truco media="print" o loadCSS. Sin embargo, el flashing de contenido sin estilo (FOUC) es un riesgo — prueba cuidadosamente.

P: ¿Cómo pruebo el rendimiento de lazy loading? R: Usa el panel Network de Chrome DevTools, limita a “Slow 3G,” y haz scroll por la página. Revisa el waterfall chart — las imágenes y chunks deberían cargar solo al entrar al viewport, no al inicio de página.