32blogby StudioMitsu

Optimización de Build en Next.js: Guía Completa

Técnicas prácticas para reducir los tiempos de build y el tamaño del bundle en Next.js — basado en cambios reales realizados en 32blog.com.

9 min read
Next.jsbuildoptimizationperformancebundle-sizeturbopack
Contenido

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.

bash
npm install --save-dev @next/bundle-analyzer
typescript
// next.config.ts
import withBundleAnalyzer from "@next/bundle-analyzer";

const nextConfig = {
  // your config here
};

export default withBundleAnalyzer({
  enabled: process.env.ANALYZE === "true",
})(nextConfig);
bash
# 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 con date-fns o dayjs)
  • 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

typescript
// 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.

bash
npm install lodash-es
npm install --save-dev @types/lodash-es
typescript
// 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.

bash
npm uninstall moment
npm install date-fns
typescript
// 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

typescript
// 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:

typescript
// 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

typescript
// 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.

tsx
// 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>
  );
}
tsx
// 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

tsx
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.

json
{
  "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:

bash
# Fix corrupted Turbopack cache
rm -rf .next
npm run dev
typescript
// 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

typescript
// next.config.ts (Next.js 15+)
const nextConfig = {
  experimental: {
    ppr: true,
  },
};
tsx
// 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

typescript
// 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

typescript
// 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.

json
{
  "scripts": {
    "build": "next build",
    "type-check": "tsc --noEmit",
    "ci": "npm run type-check && npm run build"
  }
}

Ejecútalos como jobs paralelos en GitHub Actions:

yaml
# .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

yaml
# .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écnicaImpactoEsfuerzo
Bundle analyzer (medir primero)VisibilidadBajo
moment.js → date-fns-70KB+ en bundleMedio
Imports con nombre de lodash-50KB+ en bundleBajo
next/image priority en LCPMejor LCPBajo
Turbopack (solo dev)-70% tiempo HMRBajo
Revalidate explícito en fetchMenos llamadas SSR innecesariasMedio
Caché de build en CI-60% tiempo de build en CIMedio
generateStaticParamsMejor TTFBMedio

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.