Blog
Rendimiento Web9 min de lectura

Cómo pasar de 45 a 98 en PageSpeed con Next.js: el proceso real paso a paso

JL
Javier Lozano
15 de abril de 2026

Por qué un PageSpeed bajo destruye tu negocio en 2026

Google lleva años diciéndolo: la velocidad de carga es un factor de posicionamiento. Pero en 2026 el impacto es más directo que nunca. Las señales de Core Web Vitals —LCP, INP y CLS— forman parte del algoritmo de ranking de forma oficial, y en España los estudios de Semrush y Sistrix muestran que las páginas que puntúan por debajo de 60 en móvil tienen hasta un 35 % menos de visibilidad orgánica que sus competidores directos.

Más allá del SEO, el impacto en conversión es brutal: según datos de Google, cada segundo adicional de tiempo de carga en móvil reduce las conversiones en torno a un 12 %. Para una pyme española que recibe 500 visitas al mes, eso puede significar 60 leads perdidos por culpa de una web lenta.

El stack Next.js está diseñado para ser rápido, pero si no configuras correctamente sus características de optimización, puedes acabar con una SPA que carga lentamente como cualquier otra aplicación React mal optimizada. Esto es exactamente lo que me encontré cuando auditè la web de un cliente: una puntuación de 45 en móvil pese a usar Next.js 14.

El diagnóstico: qué estaba fallando (puntuación inicial: 45)

Antes de tocar una sola línea de código, hay que entender el problema. Usé tres herramientas en paralelo:

  • PageSpeed Insights (datos de campo reales desde Chrome UX Report)
  • WebPageTest con un perfil de dispositivo Moto G4 desde Madrid
  • Chrome DevTools > Performance grabando la carga inicial

Los problemas que encontré, ordenados por impacto:

  1. LCP de 6,2 segundos — la imagen hero se cargaba como una etiqueta <img> normal, sin optimización ninguna
  2. Font render-blocking: se cargaban 3 familias de Google Fonts desde su CDN, bloqueando el renderizado durante 1,8 segundos
  3. JavaScript de terceros no diferido: un script de chat en vivo y Google Analytics se cargaban de forma síncrona
  4. CSS no purgado: el bundle de Tailwind incluía todas las clases (2,3 MB en desarrollo) en lugar del subset real usado
  5. Imágenes en formato JPEG/PNG sin convertir a WebP ni AVIF

Paso 1: Optimizar imágenes con next/image

El componente next/image hace automáticamente la conversión a WebP/AVIF, el lazy loading y el responsive sizing, pero hay que usarlo correctamente. El error más común es no especificar priority en la imagen above-the-fold ni definir bien los tamaños.

Antes (imagen hero sin optimizar):

<img
  src="/hero-banner.jpg"
  alt="Banner principal"
  style={{ width: "100%", height: "auto" }}
/>

Después (con next/image correctamente configurado):

import Image from "next/image";

<Image
  src="/hero-banner.jpg"
  alt="Banner principal de la web"
  width={1200}
  height={600}
  priority          // Carga inmediata: es la imagen LCP
  quality={85}      // 85 es el punto óptimo calidad/tamaño
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px"
  placeholder="blur"
  blurDataURL="data:image/jpeg;base64,/9j/4AAQ..." // blur hash pequeño
/>

Para generar el blurDataURL automáticamente en imágenes locales, puedes usar la librería plaiceholder:

npm install plaiceholder sharp
import { getPlaiceholder } from "plaiceholder";
import fs from "fs";

const file = fs.readFileSync("./public/hero-banner.jpg");
const { base64 } = await getPlaiceholder(file);
// Guarda este base64 como constante en tu componente

Resultado en LCP: de 6,2 s a 1,9 s. Solo con este cambio el LCP ya entró en rango "Bueno" según los umbrales de Google (< 2,5 s).

Paso 2: Eliminar render-blocking con next/font

Cargar Google Fonts desde su CDN externa añade una petición de red bloqueante y expone la IP de tus usuarios a Google. En Next.js 13+ la solución es next/font, que descarga las fuentes en build time y las sirve desde tu propio dominio como CSS con font-display: swap.

Antes (en el <head> del layout):

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
  href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&family=Playfair+Display:wght@700&display=swap"
  rel="stylesheet"
/>

Después (en app/layout.tsx):

import { Inter, Playfair_Display } from "next/font/google";

const inter = Inter({
  subsets: ["latin"],
  weight: ["400", "600", "700"],
  variable: "--font-inter",
  display: "swap",
});

const playfair = Playfair_Display({
  subsets: ["latin"],
  weight: ["700"],
  variable: "--font-playfair",
  display: "swap",
});

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="es" className={`${inter.variable} ${playfair.variable}`}>
      <body className="font-inter">{children}</body>
    </html>
  );
}

En tailwind.config.ts enlaza las variables CSS:

theme: {
  extend: {
    fontFamily: {
      inter: ["var(--font-inter)", "system-ui", "sans-serif"],
      playfair: ["var(--font-playfair)", "Georgia", "serif"],
    },
  },
},

Resultado: eliminados 1,8 s de render-blocking. El FCP (First Contentful Paint) bajó de 3,4 s a 1,1 s.

Paso 3: Diferir JavaScript de terceros

Los scripts de analytics, chat y pixel de redes sociales no son necesarios durante la carga inicial. Next.js tiene el componente Script para controlar exactamente cuándo se cargan.

import Script from "next/script";

// En app/layout.tsx
export default function RootLayout({ children }) {
  return (
    <html lang="es">
      <body>
        {children}

        {/* Google Analytics: carga después de que la página sea interactiva */}
        <Script
          src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXXXXXX"
          strategy="afterInteractive"
        />
        <Script id="gtag-init" strategy="afterInteractive">
          {`
            window.dataLayer = window.dataLayer || [];
            function gtag(){dataLayer.push(arguments);}
            gtag('js', new Date());
            gtag('config', 'G-XXXXXXXXXX', { anonymize_ip: true });
          `}
        </Script>

        {/* Chat en vivo: carga solo cuando el navegador está idle */}
        <Script
          src="https://embed.tawk.to/XXXXXXX/default"
          strategy="lazyOnload"
        />
      </body>
    </html>
  );
}

Las tres estrategias disponibles son:

  • beforeInteractive: se carga antes de que el HTML se hidrate (solo para scripts críticos tipo polyfills)
  • afterInteractive: se carga después de la hidratación (analytics, pixels)
  • lazyOnload: se carga durante el tiempo idle (chats, widgets no críticos)

Paso 4: Tailwind CSS purging correcto en producción

Tailwind ya hace purging automático en producción (Next.js 14 usa Tailwind 3+ con JIT mode por defecto), pero hay errores comunes que lo rompen: usar clases dinámicas construidas con concatenación de strings.

Esto NO funciona (Tailwind no puede detectar la clase en build time):

// MAL: Tailwind no puede analizar clases concatenadas así
const color = "red";
<div className={`text-${color}-500`}>Error</div>

Esto SÍ funciona:

// BIEN: clase completa visible para el analizador de Tailwind
const colorMap = {
  red: "text-red-500",
  blue: "text-blue-500",
  green: "text-green-500",
};
<div className={colorMap[color]}>Correcto</div>

Verifica el tamaño de tu CSS en producción con:

npm run build
# Revisa el output: el CSS debería estar entre 5-20 KB en producción
# Si supera 50 KB, tienes clases no purgadas

Paso 5: Convertir imágenes adicionales y usar formatos modernos

Para las imágenes que se usan como fondo CSS o en contextos donde next/image no aplica, conviértelas manualmente a WebP o AVIF antes de subirlas:

# Instala cwebp (macOS con Homebrew, Linux con apt)
brew install webp

# Convierte un JPEG a WebP manteniendo calidad 85
cwebp -q 85 imagen.jpg -o imagen.webp

# Para AVIF usa squoosh-cli o imagemagick
npx @squoosh/cli --avif '{"quality":60}' imagen.jpg

Para las imágenes de Open Graph y redes sociales (que no pasan por next/image), asegúrate de optimizarlas antes de subirlas. Una imagen OG ideal pesa menos de 100 KB con dimensiones de 1200×630 px.

Resultados finales antes/después

Tras aplicar todos los cambios, estos fueron los números reales medidos con PageSpeed Insights en un perfil móvil (Moto G4 desde Madrid):

Métrica Antes Después Umbral Google
Puntuación total (móvil) 45 98
LCP 6,2 s 1,9 s < 2,5 s
FCP 3,4 s 1,1 s < 1,8 s
INP 340 ms 48 ms < 200 ms
CLS 0,28 0,02 < 0,1
TTFB 1,2 s 0,3 s < 0,8 s

El TTFB mejoró adicionalmente porque habilitamos el caché de Vercel (o puedes usar headers() en Next.js para configurar Cache-Control manualmente si haces self-hosting).

Un apunte sobre el CLS (Cumulative Layout Shift)

El CLS de 0,28 era causado principalmente por dos cosas: imágenes sin width/height definidos (el navegador no reservaba espacio) y fuentes que causaban FOUT (Flash of Unstyled Text). Ambos se resuelven con los pasos anteriores, pero hay un tercer culpable habitual: los banners de cookies y popups que aparecen al cargar la página empujando el contenido hacia abajo.

La solución es posicionar estos elementos como fixed o sticky en lugar de insertarlos en el flujo del documento, o reservar el espacio en el layout desde el servidor.

¿Necesitas que yo optimice tu web?

Si tu web en Next.js tiene una puntuación baja en PageSpeed y no sabes exactamente por dónde empezar, puedo hacer una auditoría técnica completa e implementar todas estas mejoras. Contáctame y cuéntame el estado actual de tu proyecto: en la mayoría de casos las mejoras más grandes se consiguen en pocas horas de trabajo.

Etiquetas

optimización rendimiento nextjsmejorar pagespeed nextjscore web vitals nextjslcp nextjsnext image optimizaciónnext font google fonts

¿Te ha sido útil?

Hablamos sobre tu proyecto

Si necesitas implementar algo de lo que has leído, cuéntame tu caso. Sin compromiso.

Contactar