De 76 a 99 en PageSpeed móvil — cómo optimizamos nuestra propia web con Next.js 16
No era la web de un cliente — era la nuestra. nexuscode.es daba 76 en rendimiento móvil. Esto es lo que hicimos para llevarla a 99 en móvil y 100/100/100/100 en desktop. Con datos reales, técnicas reales y un bug de Next.js que no pudimos arreglar.
Esta vez no era la web de un cliente. Era la nuestra.
nexuscode.es — la web de nuestra agencia de desarrollo — llevaba semanas en producción. Diseño cuidado, animaciones suaves, contenido trabajado. Nos sentíamos orgullosos. Hasta que abrimos PageSpeed Insights en modo móvil.
Rendimiento móvil. Nuestra propia web.
La web de la agencia que promete rendimiento.
76. En la web de la agencia que vende optimización. Si eso no es un tirón de orejas cósmico, no sé qué lo es.
Así que hicimos lo único que podíamos hacer: cerrarnos una noche a arreglarlo. Y esto es exactamente lo que hicimos, con datos reales, sin redondear nada.
Los números antes y después
Primero los hechos. Después la historia.
Desktop: 100/100/100/100. Cuatro categorías perfectas. Móvil: 99/100/100/100 — y ese 99 tiene una historia que contamos más abajo.
El diagnóstico: ¿dónde se iban los puntos?
Lighthouse no miente. Simula un Moto G Power con 4G lento y te dice exactamente dónde duele. Esto es lo que encontramos:
Seis problemas. Cada uno sumando penalización. Ninguno visible a simple vista — la web se veía perfecta. Pero Lighthouse no mide cómo se ve. Mide cómo funciona.
Fix 1: matar el "use client" de la home
Este fue el cambio más grande. La página principal de nexuscode.es era un Client Component. Tenía "use client" en la primera línea. ¿Por qué? Porque usaba un hook para el idioma.
El problema: cuando una página es Client Component en Next.js, todo su HTML se genera en el navegador. El servidor envía un bundle de JavaScript, el navegador lo ejecuta, y entonces pinta el contenido. En un Moto G con 4G, eso es una eternidad.
// ❌ Antes — toda la página era client-side
"use client"
import { useLocale } from "@/lib/i18n/nexuscode-locale-provider"
export default function Home() {
const { t } = useLocale() // 🔴 Esto forzaba "use client"
return <main>{t.hero.title}</main>
}
// ✅ Después — Server Component, HTML pre-renderizado
import { getLocale } from "@/lib/i18n/get-locale"
import { translations } from "@/lib/i18n/translations"
export default async function Home() {
const locale = getLocale() // 💚 Lee cookie en el servidor
const t = translations[locale]
return <main>{t.hero.title}</main>
}
Resultado: el servidor envía HTML puro. El navegador lo pinta inmediatamente. Cero JavaScript necesario para el primer render.
De 3.9 segundos a 0.7. Solo con este cambio. El contenido principal pasó de tardar 4 segundos en aparecer a estar ahí antes de que parpadees.
Fix 2: i18n con cookies en vez de localStorage
El sistema de internacionalización (español/inglés) usaba localStorage para guardar el idioma. Suena inocente. No lo es.
localStorage solo existe en el navegador. Si lo usas para decidir qué contenido mostrar, necesitas JavaScript del lado del cliente. Eso significa "use client" en cascada — y eso significa que todo el árbol de componentes pierde SSR.
// 💚 Server utility — lee la cookie sin JavaScript
import { cookies } from "next/headers"
export function getLocale(): "es" | "en" {
const cookieStore = cookies()
return cookieStore.get("nexuscode_lang")?.value === "en"
? "en"
: "es" // 🇪🇸 Español por defecto
}
Cambiar de localStorage a cookies eliminó el flash de idioma, permitió SSR completo y le quitó la dependencia de JavaScript al contenido más importante de la página.
Fix 3: providers condicionales por hostname
Nuestro proyecto tiene cuatro dominios en el mismo código Next.js: la app autenticada, la landing SaaS, la web de la agencia y la landing de un producto. Todos comparten el root layout.
El problema: el root layout cargaba PostHog, ThemeProvider, Service Worker, Toaster y Session Tracker en todas las páginas. Incluyendo las páginas públicas que no necesitan nada de eso.
// ❌ Antes — todos los dominios cargaban todo
export default function RootLayout({ children }) {
return (
<PostHogProvider>
<ThemeProvider>
<Toaster />
<ServiceWorker />
<SessionTracker />
{children}
</ThemeProvider>
</PostHogProvider>
)
}
// ✅ Después — providers solo donde se necesitan
import dynamic from "next/dynamic"
import { headers } from "next/headers"
const AppProviders = dynamic(
() => import("@/components/providers/app-providers")
)
export default async function RootLayout({ children }) {
const host = headers().get("host") ?? ""
const isApp = host.startsWith("app.")
// 💚 Sitios públicos: children directo, cero JS extra
if (!isApp) return <html><body>{children}</body></html>
// 🔒 App autenticada: carga providers con dynamic import
return (
<html><body>
<AppProviders>{children}</AppProviders>
</body></html>
)
}
~130 KB menos de JavaScript en las páginas públicas. PostHog, analytics, temas de color, service worker — nada de eso se descarga en nexuscode.es. Solo se carga en la app autenticada, que es donde realmente se necesita.
Fix 4: eliminar la cadena de redirección DNS
Este es de los que duelen porque es completamente invisible. Antes de que el navegador descargara un solo byte de HTML, ya llevaba 780 milisegundos perdidos rebotando entre servidores.
🔁 La cadena de redirección (antes)
780ms antes de descargar un solo byte
El dominio principal era www.nexuscode.es. El dominio sin www redirigía con un 307 (temporal, no cacheable). Además, Cloudflare tenía el proxy activado (icono naranja), así que el tráfico pasaba por Cloudflare y por Vercel. Doble proxy.
La solución fue triple:
Resultado: de 780ms perdidos a 0ms. El navegador conecta directamente con Vercel. Sin rebotes, sin doble proxy, sin redirecciones.
Fix 5: CSS inline experimental
Next.js con Tailwind genera un archivo CSS externo. El navegador tiene que descargarlo antes de pintar cualquier cosa — es lo que se llama render-blocking CSS.
Next.js 16 tiene una opción experimental que cambia esto:
// 💚 next.config.ts — CSS inyectado directamente en el HTML
const config = {
experimental: {
inlineCss: true, // CSS como <style> en <head>, no <link>
}
}
En vez de generar un archivo .css externo, el CSS de Tailwind se inyecta como un bloque <style> directamente en el <head> del HTML. El navegador puede pintar sin esperar ninguna descarga adicional.
¿Por qué funciona? Porque Tailwind genera CSS atómico — clases pequeñas y reutilizables. El tamaño total del CSS es lo bastante pequeño como para ir inline sin penalizar. Con una librería CSS más pesada (Bootstrap, Material UI), esto no sería viable.
Ojo: esta opción es experimental
inlineCss está marcada como experimental en Next.js 16. Funciona perfectamente para sitios con Tailwind, pero puede dar problemas con CSS Modules o bibliotecas CSS-in-JS más pesadas. Úsala sabiendo que la API puede cambiar.
Fix 6: accesibilidad de 91 a 100
La accesibilidad no es un extra — es una de las cuatro categorías de Lighthouse y afecta directamente al SEO. Nuestro 91 venía de 14 errores de contraste y markup:
text-zinc-300 — sobre fondos oscuros, ratio 8:1+. Antes usábamos text-zinc-500 que no cumplía WCAG AA.underline — un link debe ser distinguible del texto sin depender solo del color.aria-label coincide con texto visible — si un botón dice "Contacto", su aria-label no puede decir "Ir a la página de contacto".aria-hidden en iconos decorativos — los lectores de pantalla no necesitan describir un icono de flecha junto a texto que ya dice "Ver servicios".Ninguno de estos cambios afecta al diseño visual. Son invisibles para el usuario, pero cada uno suma puntos en Lighthouse. Y cada punto de accesibilidad es un punto de SEO que tu competencia no está consiguiendo.
¿Por qué 99 y no 100 en móvil?
Porque somos honestos.
El punto que falta lo causa un bug confirmado de Next.js (#86785). Next.js inyecta ~23 KB de polyfills del navegador que no se pueden desactivar. Ni con browserslist, ni con configuración, ni con workarounds. El equipo de Vercel lo tiene documentado como bug.
🐛 Next.js issue #86785
Podríamos haber redondeado a 100. Podríamos haber ejecutado PageSpeed diez veces y quedarnos con la mejor. Pero preferimos contarte la verdad: 99 en móvil es nuestro techo técnico con Next.js 16 hoy. Cuando Vercel arregle el bug, será 100.
En desktop, donde el hardware es más potente, esos 23 KB no marcan diferencia. Por eso desktop sí da 100.
¿Y la variabilidad?
Algo que casi nadie menciona: PageSpeed móvil fluctúa. La misma web puede dar 96 en una ejecución y 99 en la siguiente. Lighthouse simula condiciones de red y CPU, y esa simulación tiene varianza inherente.
Por eso nunca prometemos "100 en cada ejecución". Lo que prometemos es que la web esté arquitectónicamente optimizada para que el rango sea 95-100, no 70-85. La diferencia está en los fundamentos, no en la suerte del momento.
El impacto real
Los números bonitos están bien para Twitter. Pero ¿qué significan en la práctica?
📊 Lo que dicen los datos
Nuestro LCP pasó de 3.9 a 0.7 segundos. Nuestro TBT de 140ms a 0ms. En una web comercial, esa diferencia es tráfico, es posicionamiento, es dinero.
Lo que la mayoría de agencias hace mal
Después de años construyendo webs de alto rendimiento, los tres errores más comunes que vemos son:
1. Medir solo en desktop
Google usa la puntuación móvil para el ranking. Tu web puede dar 98 en desktop y 60 en móvil. Adivina cuál usa Google.
2. Confundir rápido con optimizado
Que una web "vaya rápida" en tu MacBook Pro no significa que esté optimizada. Lighthouse simula un Moto G Power con 4G lento. ¿Tu web sobrevive a eso?
3. Ignorar la accesibilidad
La accesibilidad no es un bonus — es una de las cuatro puntuaciones de Lighthouse. Cada contraste insuficiente, cada link sin subrayar, cada aria-label ausente resta puntos.
El checklist que seguimos
Si tienes una web Next.js y quieres subir en PageSpeed, este es el orden que recomendamos:
inlineCss — si usas Tailwind, el CSS inline elimina una descarga blocking.
TL;DR
Cogimos nuestra propia web Next.js en producción — nexuscode.es. Daba 76 en rendimiento móvil. Convertimos la home a Server Component, migramos i18n a cookies, eliminamos 130 KB de JS innecesario con providers condicionales, matamos una cadena de redirección DNS de 780ms, activamos CSS inline experimental y arreglamos 14 errores de accesibilidad.
Resultado: 99/100/100/100 en móvil, 100/100/100/100 en desktop. El punto que falta es un bug de Next.js (#86785) que no depende de nosotros.
Todo en una noche. Sin cambiar el diseño. Sin quitar funcionalidad. Solo con decisiones de ingeniería.
💡 Lo que aprendimos
"use client" en una página completa es el asesino silencioso del LCP.
🛠️ Stack utilizado en este proyecto
¿Tu web no llega a 90 en PageSpeed? En Nexus Code hacemos auditorías de rendimiento gratuitas. Te decimos exactamente qué frena tu web y cuánto cuesta arreglarlo. Pide la tuya aquí — sin compromiso, resultado en 48h.