Por qué tu aplicación Next.js va lenta: 7 soluciones de rendimiento que realmente funcionan
Tu aplicación Next.js tarda más de 4 segundos en cargarse. La puntuación de Lighthouse es de 60. Los usuarios abandonan la página. Aquà tienes 7 soluciones concretas con mediciones del antes y el después: nada de consejos vagos, solo código.
1. Estás enviando 500 KB de JavaScript al navegador
Ejecuta npx @next/bundle-analyzer y comprueba qué contiene el paquete de tu cliente. Los elementos que suelen ocupar más espacio son: moment.js (300 KB → sustitúyelo por date-fns), lodash (70 KB → importa solo las funciones especÃficas) y las bibliotecas de iconos (importar todos los iconos en lugar de solo los que necesitas).
// MALO: importa toda la biblioteca (70 KB)
import _ from 'lodash';
const result = _.debounce(fn, 300);
// BIEN: importa solo lo que necesitas (2 KB)
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// MAL: importa los más de 1000 iconos (200 KB)
import { ArrowRight } from 'lucide-react';
// BIEN: importación
optimizableimport ArrowRight from 'lucide-react/dist/esm/icons/arrow-right';2. Todos los componentes son «use client»
Si añades «use client» a todos los componentes, estarás enviando todo ese JavaScript al navegador. Los componentes de servidor se renderizan en el servidor y envÃan HTML, sin nada de JavaScript. Utiliza «use client» solo para los componentes que necesiten interactividad (onClick, useState, useEffect).
// Componente de servidor (predeterminado): envÃa HTML, sin JavaScript
asÃncrono function BlogList() {
const posts = await db.posts.findMany();
return (
<div>
{posts.map(post => (
<article key={post.id}>
<h2>{post.title}</h2>
<p>{post.excerpt}</p>
{/* Solo el botón está en el lado del cliente */}
<LikeButton postId={post.id} />
</article>
))}
</div>
);
}
// Componente del cliente: solo la parte
interactiva'use client';
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>Me gusta</button>;
}3. Imágenes sin «siguiente/imagen»
Las etiquetas <img> sin procesar cargan la imagen a tamaño completo. next/image redimensiona automáticamente la imagen, la convierte a WebP/AVIF y la carga de forma diferida. Esto por sà solo puede reducir el LCP entre un 40 % y un 60 %.
// MAL: carga una imagen
completa de 4 MB<img src="/hero.jpg" />
// BIEN: Next.js optimiza, carga diferida y sirve en formato
WebPimport Image from 'next/image';
<Image
src="/hero.jpg"
width={1200}
height={600}
priority // para imágenes que se ven al cargar
la página alt="Imagen principal"
/>4. Recuperación de datos en el cliente cuando se podrÃa utilizar SSR
Si utilizas useEffect + fetch para cargar datos cada vez que se visita una página, estás haciendo que el usuario tenga que esperar a que: se descargue el HTML → se analice el JS → se hidrate React → se realice la solicitud fetch → se renderice. Con los componentes de servidor, la obtención de datos se realiza en el servidor y el usuario recibe el HTML completo de inmediato.
5. Faltan los encabezados de almacenamiento en caché en los recursos estáticos
Los archivos estáticos de Next.js ubicados en /_next/static/ están protegidos mediante hash de contenido y son inmutables. Tu servidor Nginx deberÃa almacenarlos en caché de forma intensiva:
# Nginx: almacenar en caché los archivos estáticos de Next.js durante 1 año
location /_next/static {
proxy_pass http://frontend:3000;
add_header Cache-Control "public, max-age=31536000, immutable";
}6. No utilizar React.lazy para componentes pesados
Los componentes como los editores de texto enriquecido, los gráficos y los mapas ocupan mucho espacio. AplÃcales la carga diferida para que no bloqueen la renderización inicial:
import dynamic from 'next/dynamic';
// Solo se carga cuando se renderiza el componente
const Chart = dynamic(() => import('@/components/Chart'), {
loading: () => <div className="h-64 animate-pulse bg-gray-800" />,
ssr: false, // La biblioteca de gráficos necesita la ventana
});7. Sin optimización de fuentes
Las fuentes personalizadas provocan cambios en el diseño (CLS) cuando se cargan tarde. Utiliza next/font para alojar y precargar las fuentes:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap', // muestra la fuente alternativa hasta que se cargue
});
export default function Layout({ children }) {
return <body className={inter.className}>{children}</body>;
}Resultados previstos
| Sistema métrico | Antes | Una vez realizadas todas las correcciones |
|---|---|---|
| Rendimiento de Lighthouse | 58 | 94 |
| Primer contenido visible | 2,8 s | 0,8 s |
| Primer elemento visible | 4,2 s | 1,4 s |
| Paquete JS completo | 520 KB | 180 KB |
| Desplazamiento acumulativo de la disposición | 0,25 | 0,02 |
