En las aplicaciones web modernas, JavaScript es computacionalmente el recurso más costoso para el dispositivo del cliente. A diferencia de las imágenes ráster de idéntico peso en bytes, cada kilobyte de código JavaScript de cliente requiere no solo transmisión por red, sino también descompresión gzip, análisis sintáctico (AST) y compilación JIT en el motor V8 antes de su ejecución.
Un bundle de cliente sobredimensionado degrada métricas de rendimiento críticas:
Next.js proporciona Code Splitting automático por segmentos de ruta, combinado con Tree Shaking. Sin embargo, el uso descuidado de dependencias npm (como librerías de utilidades monolíticas o colecciones completas de iconos) y la creación de Barrel Files pueden arrastrar accidentalmente cientos de kilobytes de código muerto al bundle de cliente.
/dashboard reside en un chunk aislado y nunca es descargado por un usuario que navega por /blog.First Load JS shared by all, cacheándose agresivamente entre transiciones de página.import y export. Si solo se importa una función de un módulo ESM, el empaquetador descarta las exportaciones restantes del paquete final.Lanzar la compilación de producción con la variable ANALYZE=true para generar mapas visuales interactivos (treemaps) de cliente y servidor.
Inspeccionar los treemaps para identificar módulos desproporcionados, paquetes heredados CJS (moment.js, lodash) o colecciones de iconos duplicadas.
Trasladar paquetes pesados de cálculo a Server Components y configurar optimizePackageImports en next.config.ts.
Envolver gráficos, modales y editores WYSIWYG en next/dynamic con componentes de carga de respaldo.
Verificar el tamaño de First Load JS en los registros de compilación. Bloquear pull requests si superan el presupuesto límite de 100 KB.
@next/bundle-analyzer#Para generar mapas de memoria visuales interactivos (treemaps) de los bundles de cliente y servidor, instala el analizador oficial de Next.js:
pnpm add -D @next/bundle-analyzer cross-envConfigura el analizador dentro de next.config.ts:
import type { NextConfig } from 'next';
import withBundleAnalyzer from '@next/bundle-analyzer';
const nextConfig: NextConfig = {
// Optimizaciones experimentales del compilador
experimental: {
// Tree Shaking profundo automático para paquetes con archivos barrel
optimizePackageImports: ['lucide-react', 'date-fns', 'lodash-es'],
},
};
const analyzer = withBundleAnalyzer({
enabled: process.env.ANALYZE === 'true',
});
export default analyzer(nextConfig);Para ejecutar el análisis interactivo en el navegador, ejecuta:
cross-env ANALYZE=true pnpm run buildnext/dynamic#Para aislar bibliotecas de cliente pesadas (como paquetes de gráficos Recharts o editores TipTap) del bundle inicial, utiliza dynamic():
'use client';
import React, { useState } from 'react';
import dynamic from 'next/dynamic';
// Atención: la opción { ssr: false } está permitida ÚNICAMENTE en componentes de cliente ('use client')
const DynamicAnalyticsChart = dynamic(
() => import('@/components/HeavyChart').then((mod) => mod.HeavyChart),
{
loading: () => (
<div className="w-full h-64 bg-bg-surface animate-pulse rounded-xl border border-border flex items-center justify-center">
<span className="text-xs text-text-muted font-mono">Cargando módulo de visualización...</span>
</div>
),
ssr: false, // Deshabilita el renderizado en servidor (la biblioteca requiere window / canvas)
}
);
export function DashboardMetrics() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="space-y-4">
<button
onClick={() => setIsOpen(!isOpen)}
className="px-4 py-2 rounded-lg bg-accent-violet text-white text-sm font-medium hover:bg-accent-violet/90 transition-colors"
>
{isOpen ? 'Ocultar analítica' : 'Mostrar analítica'}
</button>
{/* El chunk del componente se descarga en el navegador solo en el momento del renderizado */}
{isOpen && <DynamicAnalyticsChart />}
</div>
);
}Un desarrollador escribe en un Server Component app/analytics/page.tsx: const Chart = dynamic(() => import('./Chart'), { ssr: false });. ¿Qué sucede durante next build?
En los React Server Components, el código de las librerías se ejecuta exclusivamente en el servidor. Por diseño, las dependencias importadas en Server Components quedan excluidas del bundle de JavaScript del cliente.
// Server Component (sin directiva 'use client')
import { getPostBySlug } from '@/lib/posts';
// El paquete 'marked' pesa ~40 KB, pero NUNCA se enviará al navegador
import { marked } from 'marked';
import sanitizeHtml from 'sanitize-html';
export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPostBySlug(slug);
// El procesamiento se realiza en el servidor; el cliente recibe HTML limpio y seguro
const rawHtml = await marked.parse(post.content);
const cleanHtml = sanitizeHtml(rawHtml);
return (
<article className="prose dark:prose-invert max-w-3xl mx-auto py-8">
<div dangerouslySetInnerHTML={{ __html: cleanHtml }} />
</article>
);
}moment (~290 KB, CJS sin posibilidad de Tree Shaking) por date-fns o la API nativa Intl.DateTimeFormat.lodash (70 KB CommonJS) por lodash-es o métodos de array nativos de ES2024.Una aplicación importa un icono: import { Check } from 'lucide-react'. ¿Por qué esto puede inflar el bundle de cliente en cientos de kilobytes sin optimización y cómo lo soluciona optimizePackageImports?
Invocar dynamic(..., { ssr: false }) en un archivo sin la directiva 'use client' genera un error fatal de compilación: ssr: false is not allowed with next/dynamic in Server Components.
Solución: En Server Components, dynamic() siempre se ejecuta durante el SSR. Si necesitas deshabilitar el renderizado en servidor por APIs exclusivas de navegador (window), aísla la llamada dynamic(..., { ssr: false }) dentro de un componente de cliente contenedor marcado con 'use client'.
Los paquetes empaquetados en formato CommonJS (moment.js, el lodash tradicional) no se benefician del Tree Shaking. Importar una única función import { debounce } from 'lodash' introduce la biblioteca completa de más de 70 KB.
Solución: Emplea alternativas modernas ESM (lodash-es, date-fns) o configura optimizePackageImports: ['lodash'] en next.config.ts.
Crear un archivo components/index.ts con directivas export * from './HeavyChart' y export * from './Button' provoca que una importación de un botón ligero import { Button } from '@/components' arrastre el módulo de gráficos pesados al bundle.
Solución: Utiliza importaciones directas import { Button } from '@/components/Button' o añade el paquete a experimental.optimizePackageImports en next.config.ts.
Añadir 'use client' al inicio de app/page.tsx convierte todo el subárbol de la página en componentes de cliente. Todas las utilidades, validadores y librerías que podrían haberse ejecutado en el servidor se envían innecesariamente al navegador.
Solución: Conserva páginas y layouts como Server Components. Reserva 'use client' exclusivamente para componentes hoja interactivos (botones, formularios, selectores).
Envolver botones pequeños o tooltips en dynamic() constituye un antipatrón: cada importación dinámica crea una petición HTTP adicional. La sobrecarga de red en cabeceras HTTP supera con creces el ahorro en bytes.
Solución: Aplica next/dynamic estrictamente a módulos de interfaz pesados que superen los 30–50 KB.
Utilizar npm install o yarn add en un repositorio configurado con pnpm corrompe el archivo lockfile e ignora el almacén global unificado de paquetes.
Solución: Utiliza siempre el comando regulado pnpm add -D <package>.