Bundle analysis y dependencias en Next.js | Nicolás Garzón
Optimizar el bundle no significa perseguir el archivo JavaScript más pequeño posible. Significa reducir el código, CSS y terceros que una interacción real debe descargar, parsear y ejecutar. La evidencia proviene del build, el analyzer y las herramientas del navegador, no del número de dependencias en package.json.
Texto
Copiar source imports
→ dependency graph
→ server/client boundaries
→ chunks per route
→ network transfer
→ parse/compile/execute
→ hydration and interactionUn paquete pesado importado solo por un Server Component puede no llegar al navegador. Un paquete pequeño importado desde root use client puede entrar en todas las rutas.
JavaScript inicial por ruta.
Chunks cargados bajo interacción.
CSS por ruta.
Duplicación de versiones.
Código no usado.
Coste de ejecución, no solo gzip.
Requests inducidos por terceros.
RSC payload.
Hydration duration.
Una librería de 30 KB que ejecuta un parser costoso puede impactar más que 60 KB de datos simples.
El modo desarrollo incluye tooling, warnings y Fast Refresh. Analiza producción.
Tamaños reportados por ruta según la versión/tooling.
Warnings de módulos grandes.
Dynamic imports.
Server/client chunks.
Errores de tree shaking.
No compares números de builds con configuraciones distintas sin contexto.
Integra una herramienta compatible como @next/bundle-analyzer:
TypeScript
Copiar import bundleAnalyzer from "@next/bundle-analyzer" ;
const withBundleAnalyzer = bundleAnalyzer ( {
enabled: process. env. ANALYZE === "true" ,
} ) ;
export default withBundleAnalyzer ( { } ) ; Bash
Copiar ANALYZE = true pnpm buildEl treemap muestra qué módulos forman cada chunk. Pregunta:
¿Esta dependencia aparece en client bundle?
¿Por qué está en la ruta inicial?
¿Existe más de una versión?
¿Se importa un entrypoint completo?
¿Puede permanecer servidor o cargar bajo demanda?
TypeScript
Copiar
"use client" ;
import HeavyProvider from "heavy-provider" ; El root layout y sus imports cruzan al grafo cliente. Mejor:
TypeScript
Copiar
import { Providers } from "./providers" ;
export default function Layout ( { children } : Props) {
return < Providers> { children} < / Providers> ;
} TypeScript
Copiar
"use client" ; Aun así, Providers se carga en todas las rutas. Mueve providers de feature a nested layouts.
Funciona mejor con ES modules y exports estáticos:
TypeScript
Copiar import { format } from "date-fns" ; Pero un paquete puede marcar side effects o publicar CommonJS, limitando eliminación.
Evita barrel imports de librerías que reexportan todo cuando el tooling no puede optimizar:
TypeScript
Copiar import Button from "library/button" ; No cambies imports a paths internos no documentados; pueden romper upgrades.
Next.js puede optimizar imports de paquetes con muchos exports bajo configuración soportada:
TypeScript
Copiar const nextConfig = {
experimental: {
optimizePackageImports: [ "some-library" ] ,
} ,
} ; La lista de paquetes optimizados por defecto y el estado de la opción cambian por versión. Mide y consulta docs. No añadas todo node_modules.
Algunas librerías ofrecen plugins o entrypoints específicos. Prefiere API oficial. Un codemod puede migrar imports, pero verifica funcionalidad y tipos.
Dos versiones de React o una librería aumentan bundle y pueden romper Context/Hooks.
Bash
Copiar pnpm why react
pnpm why zod
Peer dependencies incompatibles.
Lockfile antiguo.
Paquetes que incluyen dependencia en vez de peer.
Monorepo con rangos distintos.
Deduplica sin forzar una versión incompatible.
TypeScript
Copiar const Editor = dynamic ( ( ) => import ( "./editor" ) , {
loading : ( ) => < EditorSkeleton / > ,
} ) ; Aporta cuando la feature es pesada y condicional. No divide un paquete si otro import estático ya lo arrastra al chunk inicial.
El analyzer debe confirmar que realmente se separó.
SDKs admin, ORM y parsers pueden permanecer server-only:
TypeScript
Copiar import "server-only" ; No importes un módulo común que reexporta server y client juntos:
TypeScript
Copiar
export * from "./db" ;
export * from "./button" ; Texto
Copiar feature/server
feature/client
feature/shared“Shared” solo debe contener código ejecutable en ambos entornos:
Funciones puras.
Schemas sin secretos.
Tipos.
No debe importar Node APIs, DOM o env privadas.
Un schema Zod grande usado solo en servidor no necesita enviarse al formulario cliente; crea un schema ligero para UX si es necesario y valida completo en servidor.
Importar un paquete completo puede ser costoso según publicación:
TypeScript
Copiar import { Trash, Edit } from "icon-library" ; Muchas librerías modernas tree-shakean; confirma con analyzer. No reemplaces iconos accesibles por SVG copiado sin gestionar aria-hidden, tamaño y mantenimiento.
Moment.js con locales completos es un ejemplo histórico de bundle grande. Intl o una librería modular puede ser suficiente.
Si necesitas locale data, carga únicamente el idioma de la route o mantenlo servidor.
Suelen dominar el bundle. Estrategias:
Dynamic import.
Cargar al abrir.
Ejecutar transformación servidor.
Importar plugins seleccionados.
Usar iframe cuando el aislamiento aporta.
Render estático de gráfico para lectores y carga interactiva opcional.
No reemplaces una librería probada por código propio inseguro solo para ahorrar KB.
Framework global completo.
CSS de librerías en root.
Duplicación entre chunks.
Classes no usadas.
Fuentes e imágenes inline.
Tailwind elimina classes no detectadas, pero safelists amplias vuelven a incluir mucho CSS.
Next.js transpila según targets, pero no toda API runtime recibe polyfill. Un paquete puede traer polyfills globales grandes.
Define navegadores soportados y carga polyfills solo donde se necesiten. No optimices para navegadores fuera del producto sin requisito.
El script inicial puede ser pequeño y cargar mucho después. Usa Performance/Network, no solo analyzer.
Texto
Copiar first-party bundle
+ third-party script
+ requests initiated by third partyElimina proveedores redundantes antes de cambiar estrategias de carga.
Server Components reducen JS, pero pueden enviar props grandes:
TypeScript
Copiar < ClientTable rows= { thousandsOfRows} / > Esto aumenta RSC payload, parseo y memoria. Pasa una ventana/página, mantén render servidor o virtualiza con datos bajo demanda.
No midas solo chunks .js.
Source maps ayudan debugging y pueden subir a un servicio sin publicarse. También aumentan artefactos y pueden revelar source si se exponen.
Configura producción según riesgo y plataforma. No los elimines sin una alternativa de observabilidad.
API necesaria.
Bundle real con tree shaking.
Ejecución.
Mantenimiento.
Seguridad.
Accesibilidad.
Types.
Una alternativa 5 KB menor pero abandonada es un mal trade-off.
Texto
Copiar marketing JS initial < target
checkout third-party < target
admin editor loaded on demandIntegra reportes en CI y alerta regresiones. Un límite rígido sin contexto puede bloquear una feature valiosa; requiere owner y excepción documentada.
Home/portfolio casi todo Server Components.
Framer Motion solo en islands necesarias.
Videos cargados al entrar en viewport/interacción.
Sanity client server-only.
Icon imports específicos.
Analytics después de consentimiento.
Source maps subidos al monitor.
Reproduce una interacción lenta.
Build/analyzer.
Identifica chunk/coste dominante.
Comprueba boundary/import.
Elimina, mueve servidor o lazy-load.
Mide red y main thread.
Verifica UX y funcionalidad.
Añade budget/regression test.
Dependencia instalada no implica bundle cliente.
Otro import la mantiene inicial.
Datos grandes siguen costando.
Analiza por ruta e interacción.
Server/client boundary determina qué llega al navegador.
Tree shaking depende de publicación/imports.
Dynamic import debe verificarse.
Third parties y RSC payload también cuentan.
Deduplica versiones compatibles.
Optimiza con budgets y evidencia.
Mantenimiento y seguridad son parte del coste.
¿Por qué una dependencia grande puede no afectar al cliente?
¿Cómo confirmas que dynamic import creó otro chunk?
¿Qué problema causa un barrel mixto?
¿Por qué gzip no basta?
¿Qué es un bundle budget útil?
¿Cómo puede un Server Component seguir enviando demasiado?
Ver respuestas
Puede ejecutarse únicamente en servidor y no cruzar use client.
Con build/analyzer y Network durante la interacción.
Puede arrastrar código servidor al cliente o viceversa.
Parseo, ejecución y requests secundarios también consumen recursos.
Un límite por ruta/interacción con owner y contexto.
Pasando props/RSC payload enormes a Client Components.
Performance en Next.js amplía el diagnóstico desde bundles hacia red, servidor, React, navegador y experiencia de usuario.