Saltar al contenido principal
60 min de lectura3 de agosto de 2026
RendimientoCore Web VitalsLighthouseOptimización

Requisitos previos

  • react-framework
  • nextjs-framework

Lo que aprenderás

  • Medir y mejorar LCP, INP y CLS
  • Auditar apps con Lighthouse y DevTools
  • Aplicar code splitting, lazy loading y prefetch
  • Optimizar imágenes y gestionar el caché
  • Usar memoización con criterio y evitar CLS
Tutorial60 min

Rendimiento frontend: Core Web Vitals y optimización

La velocidad no es un lujo: es un factor de conversión, de SEO y de experiencia. Este tutorial explica qué miden los Core Web Vitals, cómo medirlos con Lighthouse y DevTools, y las técnicas concretas para optimizar carga, imágenes, caché y renderizado en React y Next.js. Prerequisitos: React y Next.js.


Core Web Vitals

Los Core Web Vitals son tres métricas que Google usa para evaluar la experiencia de usuario. Se miden en dos contextos: lab (entorno controlado, Lighthouse) y field (datos reales de usuarios, CrUX). Cada una tiene un umbral "bueno" y uno "mejorar".

LCP — Largest Contentful Paint

Mide cuándo se pinta el elemento más grande visible (imagen hero, título grande, vídeo). Es una métrica de carga.

  • Bueno: ≤ 2.5s.
  • Mejorar: > 4s.

Para mejorarlo: optimiza el TTFB (servidor/CDN), precarga la imagen hero con priority, sirve imágenes en WebP/AVIF y evita que el CSS bloqueante retrase el primer paint.

INP — Interaction to Next Paint

Mide la latencia de las interacciones: desde que el usuario hace clic o toca hasta que la UI responde. Sustituyó a FID en 2024 y es una métrica de capacidad de respuesta.

  • Bueno: ≤ 200ms.
  • Mejorar: > 500ms.

Para mejorarlo: reduce el trabajo del hilo principal (menos JS, menos re-renders), evita tareas largas que bloqueen la interacción y usa requestIdleCallback para trabajo no urgente.

CLS — Cumulative Layout Shift

Mide cuánto se desplaza el layout de forma inesperada mientras la página carga. Es una métrica de estabilidad visual.

  • Bueno: ≤ 0.1.
  • Mejorar: > 0.25.

Las causas típicas: imágenes sin dimensiones, fuentes que cambian de tamaño al cargar, contenido inyectado por encima del viewport y animaciones que mueven elementos.

Las métricas de lab y field no siempre coinciden. Lighthouse mide en un entorno ideal; los datos de campo (CrUX) reflejan dispositivos reales, redes lentas y usuarios reales. Optimiza para ambos: lab te da diagnósticos, field te dice la verdad.

Lighthouse y herramientas

Antes de optimizar, mide. El flujo habitual:

  • Chrome DevTools → Performance: graba un trace y muestra qué tareas bloquean el hilo principal, cuánto tarda cada recurso y dónde está el cuello de botella.
  • Lighthouse: auditoría automatizada de performance, accesibilidad, SEO y buenas prácticas. Se ejecuta desde DevTools, CLI o CI.
  • Web Vitals extension: muestra LCP, INP y CLS en tiempo real mientras navegas.
  • Dashboard de Next.js: con next dev y la flag experimental, obtienes métricas de rendimiento de cada ruta en desarrollo.
// Ejecutar Lighthouse desde CLI en CI
npx lighthouse https://tu-sitio.com --view --output=json --output-path=./report.json

// Con Next.js, activa el análisis de bundle
ANALYZE=true npm run build
Un único Lighthouse en tu máquina no basta: la red, la CPU y el dispositivo cambian el resultado. Corre varias veces, usa el modo móvil (más exigente) y compara con los datos de campo de PageSpeed Insights.

Optimización de carga

El objetivo es que el usuario vea contenido útil lo antes posible y que el JavaScript que no necesita no se descargue.

Code splitting

El code splitting divide el bundle en trozos que se cargan bajo demanda. En Next.js, cada página ya genera su propio chunk. Para componentes pesados dentro de una página, usa next/dynamic:

// components/Chart.tsx — se carga solo cuando se necesita
import dynamic from "next/dynamic";

const Chart = dynamic(() => import("@/components/Chart"), {
  ssr: false, // solo cliente (usa window, canvas…)
  loading: () => <p>Cargando gráfico…</p>,
});

export function Dashboard() {
  return <Chart data={data} />;
}

En React puro (sin Next), el equivalente es React.lazy con Suspense:

import { lazy, Suspense } from "react";

const HeavyEditor = lazy(() => import("./HeavyEditor"));

export function App() {
  return (
    <Suspense fallback={<Spinner />}>
      <HeavyEditor />
    </Suspense>
  );
}

Loading states

Un loading state honesto evita que el usuario piense que la app está rota. En Next.js, loading.tsx muestra un placeholder mientras el segmento se resuelve, y React.lazy usa fallback:

// app/dashboard/loading.tsx
export default function Loading() {
  return (
    <div className="animate-pulse space-y-4">
      <div className="h-8 w-48 rounded bg-gray-200 dark:bg-gray-800" />
      <div className="h-40 rounded bg-gray-200 dark:bg-gray-800" />
    </div>
  );
}

Prefetch

El prefetch descarga recursos antes de que el usuario los pida. Next.js prefetchea los enlaces Link visibles en el viewport automáticamente. Para datos, React Query permite prefetchar queries:

import { useQueryClient } from "@tanstack/react-query";

export function usePrefetchPost(id: string) {
  const queryClient = useQueryClient();
  return () => {
    queryClient.prefetchQuery({
      queryKey: ["post", id],
      queryFn: () => fetchPost(id),
    });
  };
}
Prefetcha con moderación: prefetchear todo lo que no se va a usar desperdicia ancho de banda y puede empeorar el LCP de la página actual. Prefetch solo lo que tiene alta probabilidad de usarse (el siguiente paso del usuario).

Imágenes

Las imágenes suelen ser el mayor peso de una página. Tres palancas: formato, dimensiones y carga diferida.

Formatos modernos

WebP y AVIF comprimen mucho mejor que JPEG/PNG con calidad visual similar. AVIF suele ganar en peso, WebP en compatibilidad. Sirve siempre el formato más ligero que soporte el navegador.

next/image

next/image optimiza automáticamente: genera múltiples tamaños (srcset), sirve WebP/AVIF, aplica loading="lazy" por defecto y exige dimensiones para evitar CLS:

import Image from "next/image";

export function Hero() {
  return (
    <Image
      src="/portada.jpg"
      alt="Portada del artículo"
      width={1200}
      height={630}
      sizes="(max-width: 768px) 100vw, 50vw"
      priority // la imagen hero se precarga
    />
  );
}
  • width/height: obligatorios, reservan el espacio y eliminan CLS.
  • sizes: le dice al navegador qué ancho usará, para elegir la variante correcta del srcset.
  • priority: precarga la imagen (solo para la hero o LCP).
  • loading="lazy": por defecto en next/image; las imágenes fuera del viewport no se descargan hasta acercarse.
No pongas priority en todas las imágenes: solo la que es el LCP. Precargar varias imágenes compite por el ancho de banda y ralentiza la carga de la principal.

Caching y revalidación

La caché evita repetir trabajo. A nivel HTTP, las respuestas se cachean con Cache-Control:

// Respuesta de una API pública
Cache-Control: public, max-age=3600, stale-while-revalidate=86400

// max-age: 1h fresca en caché
// stale-while-revalidate: 24h más sirviendo la copia vieja
// mientras se revalida en segundo plano

En Next.js, el ISR (Incremental Static Regeneration) combina estático con revalidación: la página se genera en build y se regenera en segundo plano cuando expira revalidate:

// pages/blog/[slug].tsx
export async function getStaticProps({ params }) {
  const post = await fetchPost(params.slug);
  return {
    props: { post },
    revalidate: 3600, // regenera como máximo cada hora
  };
}

export async function getStaticPaths() {
  const posts = await fetchAllPosts();
  return {
    paths: posts.map((p) => ({ params: { slug: p.slug } })),
    fallback: "blocking", // páginas nuevas se generan bajo demanda
  };
}
stale-while-revalidate y el ISR comparten la misma filosofía: el usuario nunca espera por contenido fresco; recibe la copia en caché y la actualización llega después. Es la diferencia entre "rápido pero desactualizado" y "lento pero perfecto".

Memoización en React

La memoización evita trabajo repetido, pero no es gratis: cada useMemo o React.memo añade comparaciones. Úsala con criterio.

React.memo

Evita que un componente se re-renderice si sus props no cambiaron. Útil para componentes pesados que reciben props estables:

import { memo } from "react";

interface RowProps {
  id: number;
  name: string;
  onSelect: (id: number) => void;
}

const Row = memo(function Row({ id, name, onSelect }: RowProps) {
  return <li onClick={() => onSelect(id)}>{name}</li>;
});

useMemo y useCallback

useMemo cachea un valor calculado; useCallback cachea una función. Ambos estabilizan referencias para que React.memo funcione:

import { useCallback, useMemo } from "react";

export function Parent() {
  // la lista se calcula una vez
  const items = useMemo(() => buildItems(), []);

  // la función mantiene la misma referencia entre renders
  const handleSelect = useCallback((id: number) => {
    setSelected(id);
  }, []);

  return <RowList items={items} onSelect={handleSelect} />;
}
No memoices todo. Si el componente es barato o sus props cambian en cada render, React.memo solo añade coste. La regla: memoiza cuando hay un componente pesado, una lista larga, o un valor caro de calcular — y mide antes y después. La memoización prematura es una deuda de complejidad.

Eliminar CLS

El CLS se elimina reservando espacio para todo lo que se carga después. Tres técnicas clave:

  • Dimensiones fijas: imágenes y vídeos con width/height o aspect-ratio.
  • Fuentes: usa font-display: swap para que el texto se muestre con una fuente de respaldo mientras carga la real.
  • Contenedores: reserva altura para contenido que llega tarde (ads, embeds, listas).
/* Reserva espacio con aspect-ratio en vez de dejar que empuje */
.media {
  aspect-ratio: 16 / 9;
  width: 100%;
}

/* Fuente que no desplaza el layout al cargar */
@font-face {
  font-family: "MiFuente";
  src: url("/fonts/mi-fuente.woff2") format("woff2");
  font-display: swap;
}
Un patrón común de CLS: un banner o anuncio que se inserta por encima del contenido. Reserva su espacio con un contenedor de altura fija omin-height desde el primer render.

Bundle analysis

Un bundle grande se traduce en más descarga y más parseo. @next/bundle-analyzer genera un mapa visual de qué pesa en tu bundle:

// next.config.mjs
import withBundleAnalyzer from "@next/bundle-analyzer"({
  enabled: process.env.ANALYZE === "true",
});

export default withBundleAnalyzer({
  // tu config de Next
});

Con el análisis en mano, busca dependencias pesadas y pregúntate:

  • ¿Puedo importar solo la parte que uso? (imports de árbol, import { useMemo } from "react").
  • ¿Puedo cargarla de forma diferida con dynamic?
  • ¿Hay una alternativa más ligera? (una utilidad propia en vez de lodash completa).
El análisis de bundle es un hábito, no un evento: correlo en CI y alerta cuando el tamaño sube por encima de un umbral. Así las dependencias pesadas se detectan en el PR, no en producción.

Ejercicios


El rendimiento no es una fase final: es una disciplina que se aplica mientras escribes. Medir con Lighthouse y datos de campo, optimizar imágenes y carga, cachear con criterio y memoizar solo donde aporta. Cada técnica de este tutorial ataca una métrica concreta — y juntas convierten una página "que funciona" en una que se siente instantánea.

Siguiente lección

Accesibilidad web: WCAG, ARIA y buenas prácticas

Construye webs accesibles para todos: principios WCAG 2.2, HTML semántico, ARIA con criterio, formularios y navegación por teclado accesibles, contraste y testing con axe.