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).
// BAD: imports entire library (70KB)
import _ from 'lodash';
const result = _.debounce(fn, 300);
// GOOD: imports only what you need (2KB)
import debounce from 'lodash/debounce';
const result = debounce(fn, 300);
// BAD: imports all 1000+ icons (200KB)
import { ArrowRight } from 'lucide-react';
// GOOD: tree-shakeable import
import 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).
// Server Component (default) - sends HTML, zero JS
async 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>
{/* Only the button is client-side */}
<LikeButton postId={post.id} />
</article>
))}
</div>
);
}
// Client Component - only the interactive part
'use client';
function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return <button onClick={() => setLiked(!liked)}>Like</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 %.
// BAD: loads full 4MB image
<img src="/hero.jpg" />
// GOOD: Next.js optimizes, lazy loads, serves WebP
import Image from 'next/image';
<Image
src="/hero.jpg"
width={1200}
height={600}
priority // for above-the-fold images
alt="Hero image"
/>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: cache Next.js static files for 1 year
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';
// Only loads when component is rendered
const Chart = dynamic(() => import('@/components/Chart'), {
loading: () => <div className="h-64 animate-pulse bg-gray-800" />,
ssr: false, // chart library needs window
});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', // show fallback font until loaded
});
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 |
