Puedes reducir los tiempos de build y el tamaño del bundle en Next.js midiendo primero con bundle analyzer y luego aplicando correcciones específicas: tree-shaking de dependencias pesadas (lodash, moment.js), configurar next/image correctamente, y aprovechar Turbopack — ahora el bundler por defecto en Next.js 16.
El build de 32blog.com empezó a ralentizarse a medida que crecía la cantidad de artículos. Los despliegues en Vercel superaban los 2 minutos. Ejecutar next build localmente antes de verificar un cambio se sentía como hacer cola.
El tamaño del bundle también se estaba convirtiendo en un problema. Las puntuaciones de Lighthouse bajaban de maneras que se rastreaban hasta JavaScript que no necesitaba estar ahí.
Esta guía cubre las técnicas prácticas que usé para reducir los tiempos de build de Next.js y el tamaño del bundle. Incluiré detalles específicos de configuración y números reales donde los tengo.
Empieza con Bundle Analyzer para encontrar lo pesado
Optimizar sin medir es adivinar. Primero, visualiza qué está haciendo grande tu bundle.
npm install --save-dev @next/bundle-analyzer
// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";
const nextConfig = {
// your config here
};
export default withBundleAnalyzer({
enabled: process.env.ANALYZE === "true",
})(nextConfig);
# Launch the analyzer
ANALYZE=true npm run build
Esto abre un treemap interactivo en tu navegador. Los bloques más grandes significan mayores contribuciones al bundle. Concéntrate en los mayores responsables primero.
Dependencias pesadas comunes que encontrarás:
moment.js(3MB+. Reemplázalo condate-fnsodayjs)lodash(cuando el tree-shaking no funciona)@mui/material(importa solo lo que uses)- Bibliotecas de iconos (cuando importas el set completo)
Verificaciones rápidas de tamaño sin un build completo
Para verificar tamaños de paquetes individuales antes de instalarlos, bundlephobia.com es invaluable — pega el nombre del paquete y muestra el tamaño de instalación, tamaño gzip y si es tree-shakeable.
Tree-Shaking: deja de empaquetar código que no usas
La forma más efectiva de reducir el tamaño del bundle es dejar de incluir código que nunca se ejecuta.
Cambia lodash a imports con nombre
// Mal: empaqueta todo lodash (70KB+)
import _ from "lodash";
const result = _.groupBy(items, "category");
// Bien: solo empaqueta groupBy (unos pocos KB)
import groupBy from "lodash/groupBy";
const result = groupBy(items, "category");
O usa lodash-es que se distribuye como módulos ES y hace tree-shake naturalmente.
npm install lodash-es
npm install --save-dev @types/lodash-es
// lodash-es hace tree-shake automáticamente
import { groupBy, sortBy } from "lodash-es";
Migra de moment.js a date-fns
Si todavía usas moment.js, migrar a date-fns es uno de los cambios con mayor retorno de inversión que puedes hacer.
npm uninstall moment
npm install date-fns
// moment.js — ~70KB después de empaquetar
import moment from "moment";
const formatted = moment().format("YYYY/MM/DD");
// date-fns — solo empaqueta lo que importas (unos pocos KB)
import { format } from "date-fns";
import { enUS } from "date-fns/locale";
const formatted = format(new Date(), "yyyy/MM/dd", { locale: enUS });
Imports de bibliotecas de iconos
// Mal: importa todos los iconos vía wildcard (varios MB)
import * as FaIcons from "react-icons/fa";
// Bien: imports con nombre — tree-shaking elimina iconos sin usar
import { FaGithub, FaTwitter } from "react-icons/fa";
lucide-react soporta tree-shaking nativamente, así que los imports con nombre funcionan bien:
// lucide-react: los imports con nombre están bien
import { Github, Twitter, ExternalLink } from "lucide-react";
Optimización de imágenes con next/image
next/image maneja mucha optimización automáticamente, pero la configuración correcta multiplica el impacto.
Configuración básica de imágenes
// next.config.ts
const nextConfig = {
images: {
remotePatterns: [
{
protocol: "https",
hostname: "images.unsplash.com",
},
],
// Preferir AVIF, recurrir a WebP
formats: ["image/avif", "image/webp"],
// Breakpoints de imagen responsive
deviceSizes: [640, 750, 828, 1080, 1200, 1920],
},
};
Precarga de imágenes LCP
Las imágenes en el viewport al cargar la página deben precargarse. En Next.js 16, la prop priority está obsoleta — usa preload en su lugar.
// app/page.tsx
import Image from "next/image";
export default function HeroSection() {
return (
<section>
{/* Next.js 16+: usa preload para insertar un <link> preload en <head> */}
<Image
src="/hero.webp"
alt="Hero image"
width={1200}
height={600}
preload
/>
{/* Next.js 15 y anteriores usaban `priority` */}
{/* <Image src="/hero.webp" alt="Hero" width={1200} height={600} priority /> */}
</section>
);
}
Para carga inmediata sin un <link> preload, usa loading="eager" o fetchPriority="high" directamente.
// Imágenes debajo del fold: no necesitan preload, lazy loading es el predeterminado
<Image
src="/article-thumbnail.webp"
alt="Article thumbnail"
width={400}
height={300}
/>
Importa imágenes locales para dimensionamiento automático
import heroImage from "@/public/hero.webp";
import Image from "next/image";
export function Hero() {
// width/height se infieren automáticamente de la imagen importada
return <Image src={heroImage} alt="Hero" priority />;
}
Turbopack: ahora el bundler por defecto
A partir de Next.js 16, Turbopack es estable para next dev y next build y es el bundler por defecto. No necesitas flags — solo ejecuta next dev y next build.
{
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start"
}
}
Si usabas --turbopack en tus scripts, puedes eliminarlo. Turbopack ofrece hasta 76% más rápido en inicio del servidor local y 96% más rápido en Fast Refresh comparado con Webpack.
Si tienes configuración personalizada de webpack
Si tu proyecto tiene una configuración personalizada de webpack en next.config.ts, next build fallará por defecto en Next.js 16 para prevenir problemas de configuración. Tienes tres opciones:
// Opción 1: Migrar a configuración compatible con Turbopack
// next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
turbopack: {
resolveAlias: {
// Reemplaza webpack resolve.fallback
fs: { browser: "./empty.ts" },
},
},
};
export default nextConfig;
// Opción 2: Mantener Webpack solo para producción
{
"scripts": {
"dev": "next dev",
"build": "next build --webpack",
"start": "next start"
}
}
Problemas comunes con Turbopack
# Corregir caché corrupta de Turbopack
rm -rf .next
npm run dev
Estrategia de renderizado y configuración de caché
Elegir el enfoque de renderizado correcto y ser explícito sobre el comportamiento del caché afecta directamente el rendimiento de carga de la página.
Cache Components y PPR (Next.js 16)
En Next.js 16, el antiguo flag experimental.ppr fue reemplazado por cacheComponents. Esto habilita Partial Prerendering (PPR) con la nueva directiva use cache.
// next.config.ts (Next.js 16)
const nextConfig = {
cacheComponents: true,
};
export default nextConfig;
// app/blog/page.tsx
import { Suspense } from "react";
import { cacheLife, cacheTag } from "next/cache";
// Cachea el resultado de esta función con un tiempo de vida personalizado
async function getCachedArticles() {
"use cache";
cacheLife("hours");
cacheTag("articles");
return await db.articles.findMany();
}
export default function BlogPage() {
return (
<main>
{/* Las partes estáticas se renderizan instantáneamente desde caché */}
<h1>Artículos</h1>
<StaticArticleList />
{/* Las partes dinámicas se transmiten progresivamente */}
<Suspense fallback={<div>Cargando...</div>}>
<DynamicUserRecommendations />
</Suspense>
</main>
);
}
Sé explícito sobre el comportamiento del caché de fetch
Cambio importante en Next.js 15+: las peticiones fetch ya no se cachean por defecto. Debes optar explícitamente por el caché con cache: "force-cache" o next: { revalidate }.
// lib/api.ts
// Datos que cambian lentamente (revalidate: 3600 = 1 hora)
export async function getCategories() {
const res = await fetch("https://api.example.com/categories", {
next: { revalidate: 3600, tags: ["categories"] },
});
return res.json();
}
// Datos que cambian frecuentemente (revalidate: 60 = 1 minuto)
export async function getLatestPosts() {
const res = await fetch("https://api.example.com/posts?limit=10", {
next: { revalidate: 60, tags: ["posts"] },
});
return res.json();
}
// Datos en tiempo real (sin caché — este es ahora el comportamiento por defecto en Next.js 15+)
export async function getLivePrice() {
const res = await fetch("https://api.example.com/price");
return res.json();
}
Pre-genera rutas dinámicas en tiempo de build
// app/blog/[slug]/page.tsx
export async function generateStaticParams() {
const posts = await getAllPosts();
return posts.map((post) => ({
slug: post.slug,
}));
}
// Permitir generación bajo demanda para rutas no cacheadas
export const dynamicParams = true;
export default async function BlogPost({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const { slug } = await params;
const post = await getPost(slug);
return <article>{/* article content */}</article>;
}
Acorta los tiempos de build con caché
Separa la verificación de tipos del build
next build incluye verificación de tipos por defecto. En CI, separarlos permite la ejecución en paralelo.
{
"scripts": {
"build": "next build",
"type-check": "tsc --noEmit",
"ci": "npm run type-check && npm run build"
}
}
Ejecútalos como jobs paralelos en GitHub Actions:
# .github/workflows/ci.yml
jobs:
type-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm run type-check
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "22"
cache: "npm"
- run: npm ci
- run: npm run build
Cachea el build de Next.js en CI
# .github/workflows/ci.yml
- name: Cache Next.js build
uses: actions/cache@v4
with:
path: |
.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-v1
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('package-lock.json') }}-
Cachear .next/cache en CI reduce dramáticamente el tiempo de build cuando solo cambiaron unos pocos archivos. En 32blog.com, esto redujo el tiempo de build en CI de 2 minutos 30 segundos a alrededor de 50 segundos.
FAQ
¿Turbopack funciona para builds de producción en Next.js 16?
Sí. A partir de Next.js 16, Turbopack es estable para next dev y next build y es el bundler por defecto. Ya no necesitas el flag --turbopack.
¿Cómo verifico el tamaño del bundle de Next.js?
Instala @next/bundle-analyzer, agrégalo a next.config.ts y ejecuta ANALYZE=true npm run build. Se abre un treemap interactivo mostrando la contribución de cada dependencia. Para verificar paquetes individuales, usa bundlephobia.com.
¿priority sigue siendo válido en next/image?
La prop priority fue deprecada en Next.js 16 en favor de preload. Si estás en Next.js 15 o anterior, priority sigue funcionando. Para Next.js 16+, usa preload para insertar un <link> preload en <head>, o loading="eager" / fetchPriority="high" para otros escenarios de carga inmediata.
¿Qué reemplazó a experimental.ppr en Next.js 16?
El flag experimental.ppr fue eliminado. Usa cacheComponents: true en next.config.ts en su lugar. Esto habilita Partial Prerendering con la nueva directiva use cache y las APIs estables cacheLife / cacheTag.
¿Las peticiones fetch siguen cacheándose por defecto?
No. A partir de Next.js 15, fetch por defecto usa no-store (sin caché). Debes agregar explícitamente cache: "force-cache" o next: { revalidate } para optar por el caché.
¿Debería reemplazar moment.js con date-fns o dayjs?
Ambos funcionan. date-fns es completamente tree-shakeable — solo incluye las funciones que importas (unos pocos KB vs los ~70KB de moment.js). dayjs es una alternativa más pequeña de importación única (~2KB) con una API compatible con moment. Elige según cuántas funciones de fecha necesites.
¿Cuánto ayuda realmente el caché de CI?
Cachear .next/cache en GitHub Actions con actions/cache evita recompilar archivos sin cambios. El impacto depende del tamaño del proyecto — en 32blog.com redujo los builds de CI de 2m30s a ~50s (aproximadamente 60% de reducción).
Conclusión
Aquí tienes un resumen de las optimizaciones de build de Next.js ordenadas por relación impacto-esfuerzo:
| Técnica | Impacto | Esfuerzo |
|---|---|---|
| Bundle analyzer (medir primero) | Visibilidad | Bajo |
| moment.js → date-fns | -70KB+ en bundle | Medio |
| Imports con nombre de lodash | -50KB+ en bundle | Bajo |
| next/image preload en LCP | Mejor LCP | Bajo |
| Turbopack (defecto en Next.js 16) | Dev + build más rápidos | Bajo |
| Revalidate explícito en fetch | Menos llamadas SSR innecesarias | Medio |
| Caché de build en CI | -60% tiempo de build en CI | Medio |
| generateStaticParams | Mejor TTFB | Medio |
use cache + cacheComponents | Caché granular del servidor | Medio |
Nunca optimices sin medir primero. Empieza con el bundle analyzer, encuentra los elementos más grandes y arréglalo en orden de tamaño. Ganancias de bajo esfuerzo y alto impacto primero — Turbopack y el caché de CI son buenos puntos de partida ya que son rápidos de configurar y la mejora es inmediatamente visible.
Artículos relacionados:
- Guía de SSR en Next.js: qué cambió con los Server Components
- Por qué SSR no funciona en Next.js App Router (y cómo solucionarlo)
- Errores de despliegue en Vercel: Guía completa de soluciones
- Cómo solucionar errores de hidratación en Next.js
- Crea un ranking de posts populares con la API de GA4 en Next.js