32blogby Studio Mitsu

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.

by omitsu12 min read
Next.jsbuildoptimizationperformancebundle-sizeturbopack
Contenido

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.

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],
  },
};

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.

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

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

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

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

typescript
// 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;
json
// Opción 2: Mantener Webpack solo para producción
{
  "scripts": {
    "dev": "next dev",
    "build": "next build --webpack",
    "start": "next start"
  }
}

Problemas comunes con Turbopack

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

typescript
// next.config.ts (Next.js 16)
const nextConfig = {
  cacheComponents: true,
};

export default nextConfig;
tsx
// 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 }.

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é — 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

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

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.

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écnicaImpactoEsfuerzo
Bundle analyzer (medir primero)VisibilidadBajo
moment.js → date-fns-70KB+ en bundleMedio
Imports con nombre de lodash-50KB+ en bundleBajo
next/image preload en LCPMejor LCPBajo
Turbopack (defecto en Next.js 16)Dev + build más rápidosBajo
Revalidate explícito en fetchMenos llamadas SSR innecesariasMedio
Caché de build en CI-60% tiempo de build en CIMedio
generateStaticParamsMejor TTFBMedio
use cache + cacheComponentsCaché granular del servidorMedio

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: