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],
},
};
Establece priority en imágenes LCP
Las imágenes en el viewport al cargar la página deben tener la prop priority para ser precargadas.
// app/page.tsx
import Image from "next/image";
export default function HeroSection() {
return (
<section>
{/* LCP image: usa priority para precargar */}
<Image
src="/hero.webp"
alt="Hero image"
width={1200}
height={600}
priority
/>
</section>
);
}
// Imágenes debajo del fold: no necesitan priority, 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 para desarrollo local más rápido
Turbopack se volvió estable en Next.js 15. Cambiar el desarrollo local a Turbopack hace una diferencia notable en el tiempo de inicio y la velocidad de HMR.
{
"scripts": {
"dev": "next dev --turbopack",
"build": "next build",
"start": "next start"
}
}
Nota: next build todavía usa Webpack por defecto (Turbopack para builds es experimental). Pero cambiar solo next dev a Turbopack es suficiente para una mejora importante en la experiencia de desarrollo.
Problemas comunes al migrar a Turbopack:
# Fix corrupted Turbopack cache
rm -rf .next
npm run dev
// webpack-specific config won't apply when using Turbopack
// next.config.ts — conditional if needed
const nextConfig = {
webpack: (config, { isServer }) => {
// This block won't run under Turbopack
if (!isServer) {
config.resolve.fallback = { fs: false };
}
return config;
},
};
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.
PPR (Partial Prerendering) — experimental en Next.js 15
// next.config.ts (Next.js 15+)
const nextConfig = {
experimental: {
ppr: true,
},
};
// app/blog/page.tsx
import { Suspense } from "react";
export default function BlogPage() {
return (
<main>
{/* Las partes estáticas se renderizan instantáneamente */}
<h1>Articles</h1>
<StaticArticleList />
{/* Las partes dinámicas se transmiten progresivamente */}
<Suspense fallback={<div>Loading...</div>}>
<DynamicUserRecommendations />
</Suspense>
</main>
);
}
Sé explícito sobre el comportamiento del caché de fetch
// 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é)
export async function getLivePrice() {
const res = await fetch("https://api.example.com/price", {
cache: "no-store",
});
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: "20"
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: "20"
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.
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 priority en LCP | Mejor LCP | Bajo |
| Turbopack (solo dev) | -70% tiempo HMR | 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 |
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.