Saltar al contenido principal
Volver al blog

Core Web Vitals: optimiza el rendimiento en Next.js 15

Ray MartínRay Martín
10 min de lectura
Core Web Vitals: optimiza el rendimiento en Next.js 15

Los Core Web Vitals son las métricas que Google utiliza para evaluar la experiencia de usuario de tu sitio web. En 2026 siguen siendo un factor clave de posicionamiento SEO y, con Next.js 15, tienes todas las herramientas necesarias para dominarlos. Este artículo te muestra cómo optimizar cada métrica con ejemplos prácticos.

¿Qué son los Core Web Vitals?

Los Core Web Vitals son un conjunto de métricas centradas en el usuario que miden la velocidad de carga, la interactividad y la estabilidad visual de una página:

  • LCP (Largest Contentful Paint): Mide cuánto tarda en renderizarse el elemento más grande visible. Objetivo: < 2.5 segundos.
  • INP (Interaction to Next Paint): Mide la latencia de las interacciones del usuario. Sustituyó a FID en 2024. Objetivo: < 200ms.
  • CLS (Cumulative Layout Shift): Mide los cambios inesperados en el layout. Objetivo: < 0.1.

Nota: INP (Interaction to Next Paint) reemplazó a FID (First Input Delay) como Core Web Vital en marzo de 2024. INP mide la latencia de todas las interacciones, no solo la primera.

Optimizar LCP: carga rápida del contenido principal

El LCP suele estar determinado por la imagen hero, un bloque de texto grande o un video. Next.js proporciona herramientas nativas para optimizarlo.

Imágenes optimizadas con next/image

tsx
import Image from "next/image";

export function HeroBanner() {
  return (
    <section className="relative h-[600px]">
      <Image
        src="/assets/hero-banner.webp"
        alt="Desarrollo web profesional"
        fill
        priority
        fetchPriority="high"
        sizes="100vw"
        className="object-cover"
        quality={85}
      />
      <div className="absolute inset-0 flex items-center justify-center">
        <h1 className="text-5xl font-bold text-white">
          Desarrollo Web Profesional
        </h1>
      </div>
    </section>
  );
}

Claves para un buen LCP con imágenes:

  • priority: Añádelo a la imagen hero para precargarla con <link rel="preload">.
  • fetchPriority="high": Indica al navegador que esta imagen tiene máxima prioridad.
  • sizes: Define el tamaño responsive para que el navegador elija el archivo adecuado.
  • Formato WebP/AVIF: Next.js convierte automáticamente las imágenes a formatos modernos.
  • quality: Ajusta entre 75-85 para equilibrar calidad y tamaño de archivo.

Precargar fuentes correctamente

tsx
// app/[locale]/layout.tsx
import { Inter } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  display: "swap",          // Evita flash de texto invisible
  preload: true,            // Precarga el archivo de fuente
  variable: "--font-inter", // Variable CSS para Tailwind
});

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html className={inter.variable}>
      <body className="font-sans">{children}</body>
    </html>
  );
}

Server Components para LCP instantáneo

Los Server Components renderizan HTML en el servidor, eliminando la necesidad de esperar a que JavaScript se ejecute para mostrar contenido:

tsx
// app/[locale]/page.tsx — Server Component
import { getTranslations } from "next-intl/server";

export default async function HomePage({
  params,
}: {
  params: Promise<{ locale: string }>;
}) {
  const { locale } = await params;
  const t = await getTranslations({ locale, namespace: "hero_banner" });

  return (
    <main>
      {/* Este contenido se envía como HTML puro — LCP inmediato */}
      <h1 className="text-5xl font-bold">{t("title")}</h1>
      <p className="mt-4 text-xl text-gray-600">{t("subtitle")}</p>
    </main>
  );
}

Optimizar INP: interacciones fluidas

INP mide cuánto tarda el navegador en responder visualmente a una interacción del usuario (click, tap, tecla). Es la métrica más difícil de optimizar porque depende del JavaScript que se ejecuta en el cliente.

Reducir JavaScript en el cliente

tsx
// ❌ Malo: importar librería pesada en Client Component
"use client";
import { Chart } from "chart.js/auto"; // ~200KB

// ✅ Mejor: carga dinámica solo cuando se necesita
"use client";
import dynamic from "next/dynamic";

const Chart = dynamic(() => import("@/components/Chart"), {
  loading: () => <div className="h-64 animate-pulse bg-gray-200 rounded" />,
  ssr: false,
});

Actualizaciones optimistas con useOptimistic

React 19 introdujo useOptimistic para actualizar la UI inmediatamente mientras la operación real se completa en segundo plano:

tsx
"use client";

import { useOptimistic, useTransition } from "react";

interface Comment {
  id: string;
  text: string;
  pending?: boolean;
}

export function CommentList({
  comments,
  addComment,
}: {
  comments: Comment[];
  addComment: (text: string) => Promise<void>;
}) {
  const [optimisticComments, setOptimistic] = useOptimistic(
    comments,
    (state, newComment: string) => [
      ...state,
      { id: "temp", text: newComment, pending: true },
    ]
  );
  const [, startTransition] = useTransition();

  async function handleSubmit(formData: FormData) {
    const text = formData.get("text") as string;
    startTransition(async () => {
      setOptimistic(text);      // UI se actualiza al instante
      await addComment(text);   // Server Action se ejecuta en background
    });
  }

  return (
    <div>
      {optimisticComments.map((c) => (
        <p key={c.id} className={c.pending ? "opacity-50" : ""}>
          {c.text}
        </p>
      ))}
      <form action={handleSubmit}>
        <input name="text" aria-label="Nuevo comentario" />
        <button type="submit">Comentar</button>
      </form>
    </div>
  );
}

Debounce en event handlers costosos

tsx
"use client";

import { useCallback, useRef } from "react";

export function SearchInput() {
  const timeoutRef = useRef<NodeJS.Timeout>(null);

  const handleSearch = useCallback((value: string) => {
    if (timeoutRef.current) clearTimeout(timeoutRef.current);
    timeoutRef.current = setTimeout(() => {
      // Ejecutar búsqueda después de 300ms de inactividad
      fetch(`/api/search?q=${encodeURIComponent(value)}`);
    }, 300);
  }, []);

  return (
    <input
      type="search"
      onChange={(e) => handleSearch(e.target.value)}
      placeholder="Buscar..."
      aria-label="Buscar en el sitio"
    />
  );
}

Optimizar CLS: estabilidad visual

El CLS mide cuánto se mueve el contenido visible de la página de forma inesperada. Los principales causantes son imágenes sin dimensiones, fuentes web y contenido dinámico.

Reservar espacio para imágenes

tsx
// ✅ Correcto: next/image maneja dimensiones automáticamente
<Image
  src="/assets/project-thumbnail.webp"
  alt="Vista previa del proyecto"
  width={800}
  height={450}
  className="rounded-lg"
/>

// ✅ Para imágenes de fondo con fill: usar aspect-ratio
<div className="relative aspect-video">
  <Image
    src="/assets/hero.webp"
    alt="Hero"
    fill
    className="object-cover"
  />
</div>

Skeleton loaders para contenido dinámico

tsx
import { Suspense } from "react";

function ProjectCardSkeleton() {
  return (
    <div className="animate-pulse">
      <div className="aspect-video bg-gray-200 rounded-lg" />
      <div className="mt-4 h-6 w-3/4 bg-gray-200 rounded" />
      <div className="mt-2 h-4 w-full bg-gray-200 rounded" />
      <div className="mt-2 h-4 w-2/3 bg-gray-200 rounded" />
    </div>
  );
}

export default function ProjectsPage() {
  return (
    <div className="grid gap-6 md:grid-cols-3">
      <Suspense
        fallback={Array.from({ length: 6 }).map((_, i) => (
          <ProjectCardSkeleton key={i} />
        ))}
      >
        <ProjectList />
      </Suspense>
    </div>
  );
}

Evitar CLS con fuentes web

css
/* styles/globals.css */

/* Usar size-adjust para minimizar layout shift de fuentes */
@font-face {
  font-family: "Mali";
  font-display: swap;
  size-adjust: 105%; /* Ajustar al tamaño de la fuente fallback */
}

/* Reservar espacio con min-height en secciones que cargan datos */
.hero-section {
  min-height: 600px;
}

@media (max-width: 768px) {
  .hero-section {
    min-height: 400px;
  }
}

Medir Core Web Vitals en Next.js

Reportar a analytics

tsx
// app/[locale]/layout.tsx
import { SpeedInsights } from "@vercel/speed-insights/next";
import { Analytics } from "@vercel/analytics/next";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html>
      <body>
        {children}
        <SpeedInsights />
        <Analytics />
      </body>
    </html>
  );
}

Métricas personalizadas con web-vitals

typescript
// utils/reportWebVitals.ts
import type { Metric } from "web-vitals";

export function reportWebVitals(metric: Metric) {
  const { name, value, rating } = metric;

  // Enviar a Google Analytics
  if (typeof window.gtag === "function") {
    window.gtag("event", name, {
      event_category: "Web Vitals",
      event_label: rating, // "good", "needs-improvement", "poor"
      value: Math.round(name === "CLS" ? value * 1000 : value),
      non_interaction: true,
    });
  }
}

Configuración de Next.js para rendimiento

typescript
// next.config.ts
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  images: {
    formats: ["image/avif", "image/webp"],
    deviceSizes: [640, 750, 828, 1080, 1200],
    minimumCacheTTL: 60 * 60 * 24 * 30, // 30 días
  },
  experimental: {
    optimizeCss: true,            // Optimizar CSS con critters
    optimizePackageImports: [     // Tree-shaking agresivo
      "@tabler/icons-react",
      "date-fns",
    ],
  },
  headers: async () => [
    {
      source: "/:path*",
      headers: [
        {
          key: "Cache-Control",
          value: "public, max-age=31536000, immutable",
        },
      ],
    },
  ],
};

Checklist de rendimiento

Usa esta checklist antes de cada despliegue para asegurar buenos Core Web Vitals:

  • LCP: Imagen hero con priority y fetchPriority="high".
  • LCP: Fuentes con display: swap y precarga.
  • LCP: Server Components para contenido above-the-fold.
  • INP: Componentes pesados cargados con dynamic().
  • INP: Event handlers con debounce cuando es necesario.
  • INP: Actualizaciones optimistas para acciones del usuario.
  • CLS: Todas las imágenes con width y height o aspect-ratio.
  • CLS: Skeleton loaders para contenido asíncrono.
  • CLS: min-height en secciones que cargan datos.
  • General: Bundle analyzer para detectar dependencias pesadas.

Conclusión

Optimizar Core Web Vitals no es un esfuerzo puntual — es un proceso continuo. Next.js 15 proporciona las primitivas necesarias (Server Components, streaming, optimización de imágenes, font optimization) para alcanzar puntuaciones excelentes. La clave está en medir constantemente, usar las herramientas nativas del framework y mantener el JavaScript del cliente al mínimo.

Con las técnicas de este artículo, tu sitio debería alcanzar puntuaciones verdes en PageSpeed Insights y una experiencia de usuario notablemente más rápida, especialmente en dispositivos móviles.

Compartir:

Artículos relacionados