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