wandres.dev
OPTIMIZACIÓN DE PRODUCCIÓN · bundle budgets

Presupuestos de tamaño: fallar el CI si el bundle crece

Un treemap te dice qué pesa hoy, pero el peso vuelve a subir en el próximo PR si nadie lo vigila. Un presupuesto de tamaño convierte una intención en una restricción mecánica: size-limit mide el bundle comprimido, lo compara con un límite que tú fijas y hace fallar el CI cuando un cambio lo supera. Con presupuestos por ruta, cada pantalla defiende su propio umbral y las regresiones de peso se detectan en la revisión, no en producción.

⏱ 16 min

El treemap es una foto; el peso, una película. Optimizas un bundle a 160 KB, y tres semanas después alguien añade una librería “pequeña”, otro un polyfill, otro un import descuidado, y sin que nadie lo decida el bundle vuelve a 240 KB. La regresión de rendimiento rara vez llega de golpe: llega por mil cortes, cada uno defendible en aislamiento. La única defensa que escala es hacer del peso una restricción mecánica: fijar un presupuesto y que el CI falle cuando un PR lo supere. size-limit mide el tamaño comprimido real y bloquea el merge si el número cruza la línea. Deja de ser algo que recuerdas vigilar y pasa a ser algo que el sistema no te deja olvidar.

🎯 Al terminar esta lección sabrás
  • Configurar size-limit para medir el tamaño comprimido de tus bundles de producción.
  • Integrar la comprobación en el CI para que un PR que engorda el bundle falle antes del merge.
  • Definir presupuestos por ruta, de modo que cada pantalla defienda su propio umbral.
  • Entender la diferencia entre un umbral fijo y un presupuesto que baja como un trinquete.

Medir el comprimido, no el disco

size-limit no lee el tamaño en disco de tus archivos: los procesa como lo haría un navegador. Coge el fichero de salida, lo comprime como lo haría un CDN —brotli por defecto en las versiones recientes, que es lo que sirve la red de verdad— y compara esa cifra con el límite que declaras. El preset @size-limit/preset-app va un paso más allá y estima, además del peso, el tiempo de descarga en una conexión lenta y de ejecución en un dispositivo modesto, uniendo tamaño y experiencia en una sola medida.

// .size-limit.json — un presupuesto por artefacto, con limite explicito
[
  {
    "name": "App inicial",
    "path": "dist/assets/index-*.js",
    "limit": "160 KB"
  }
]
// package.json — el preset elige como medir; file basta si solo quieres peso
{
  "scripts": { "size": "size-limit" },
  "devDependencies": {
    "size-limit": "^11.0.0",
    "@size-limit/preset-app": "^11.0.0"
  }
}

El campo path acepta globs, de modo que el asterisco absorbe el hash de contenido que cambia en cada build. Ejecutar size imprime el tamaño comprimido y sale con código cero si cabe en el presupuesto, o con código distinto de cero si lo rebasa —exactamente lo que un CI necesita para pasar o fallar—. Y cuando el número sube y no sabes por qué, size-limit --why abre un analizador que explica qué módulos entraron, cerrando el círculo con la lección anterior.

ℹ️
Presupuesto no es lo mismo que talla

El treemap responde a la pregunta descriptiva —qué hay dentro— y size-limit a la normativa —cuánto se permite—. Son complementarios: usas el mapa para diagnosticar y reducir, y el presupuesto para que la reducción no se deshaga sola. Sin el mapa no sabes qué cortar; sin el presupuesto, todo lo que cortas vuelve a crecer.

El presupuesto como puerta del CI

Un presupuesto que solo corres en tu máquina es un buen propósito; uno que corre en cada PR es una ley. La integración con GitHub comenta en el pull request el tamaño actual y su delta respecto a la rama base, de modo que quien revisa ve, sin salir del diff, que este cambio suma 34 KB comprimidos. Si el total supera el límite, el check se pone en rojo y bloquea el merge.

# .github/workflows/size.yml — el presupuesto vive en cada pull request
name: size
on: [pull_request]
jobs:
  size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci && npm run build
      - uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}
flowchart TD
pr[Abres un pull request] --> build[El CI construye el bundle]
build --> medir[size-limit comprime y mide]
medir --> puerta{Supera el presupuesto}
puerta -->|si| rojo[Check en rojo bloquea el merge]
puerta -->|no| verde[Check en verde con el delta comentado]
style rojo fill:#f38ba8,color:#11111b
style verde fill:#a6e3a1,color:#11111b

Lo valioso de la puerta no es solo que atrape la regresión, sino cuándo la atrapa: en el momento de la revisión, cuando el autor todavía tiene el contexto fresco y el coste de revertir es mínimo. Detectar el mismo problema semanas después, en un informe de rendimiento de producción, significa arqueología —averiguar cuál de cien commits lo introdujo— en lugar de una conversación de dos líneas en el PR. Mover la detección hacia la izquierda del ciclo es la esencia de la calidad barata.

Presupuestos por ruta y el trinquete

Un único presupuesto global es un comienzo, pero pronto miente. No todas las pantallas merecen el mismo límite: la landing pública que decide tu tasa de rebote debe ser mucho más ligera que un panel de administración que solo ven usuarios ya comprometidos. Un presupuesto global promedia ambos y esconde el problema —la landing puede engordar mientras el promedio se mantiene porque el panel adelgazó—. La solución es un presupuesto por ruta: una entrada por chunk de ruta, cada una con su propio umbral.

// .size-limit.json — cada ruta defiende su propio limite
[
  {
    "name": "Landing (critica)",
    "path": "dist/assets/landing-*.js",
    "limit": "90 KB"
  },
  {
    "name": "Panel (tolerante)",
    "path": "dist/assets/panel-*.js",
    "limit": "320 KB"
  }
]

Fijar el número correcto es un arte con una trampa: el instinto es poner el límite muy por encima del tamaño actual “por si acaso”, pero un presupuesto holgado no protege de nada. La disciplina es el trinquete: ajusta el límite un pelo por encima del peso real y, cada vez que una optimización lo reduzca, baja el límite hasta el nuevo suelo. Así el presupuesto solo se mueve en una dirección —hacia abajo— y el terreno ganado queda blindado contra la regresión.

⚠️
Un presupuesto flojo es teatro de seguridad

Poner el límite en 500 KB cuando tu bundle pesa 150 KB da la sensación de control sin ningún control: das margen para cuadruplicar el peso antes de que salte una alarma. El presupuesto útil aprieta, incomoda un poco y obliga a justificar cada crecimiento. Si nunca falla, no te está protegiendo; solo te está mintiendo con luz verde.

El presupuesto convierte el rendimiento en una restricción de diseño

La lección profunda de los presupuestos de tamaño es que el rendimiento no se defiende con buena voluntad, sino con restricciones que el sistema hace cumplir sin depender de la memoria de nadie. Un equipo que “intenta mantener el bundle pequeño” pierde siempre a largo plazo, y no por falta de talento, sino por la estructura del problema: cada decisión individual que engorda el bundle es localmente razonable —esta librería resuelve mi ticket, este polyfill arregla ese navegador, este import es más cómodo—, mientras que el coste es global, difuso y diferido, pagado por un usuario anónimo en un dispositivo lento que nadie del equipo tiene delante. Es una tragedia de los comunes clásica, y las tragedias de los comunes no se resuelven con virtud, se resuelven con reglas. El presupuesto es esa regla: hace visible en el instante del PR un coste que de otro modo sería invisible hasta producción, y traslada la carga de la prueba —quien quiera cruzar la línea debe justificarlo y subir explícitamente el límite, dejando rastro—. Esto reordena el diseño entero: cuando el peso es una restricción dura, la arquitectura se organiza para respetarla —se difiere lo secundario, se elige la librería ligera, se cuestiona cada dependencia— igual que un presupuesto económico moldea un proyecto. La restricción no limita la creatividad, la enfoca. Y hay un principio universal debajo: en cualquier sistema donde una propiedad deseable se degrada por acumulación de decisiones locales —el tamaño, la latencia, la deuda técnica, la cobertura de tests—, la única defensa que escala no es la disciplina individual sino un invariante verificado por la máquina en cada cambio. Quien entiende size-limit no ha aprendido una herramienta; ha entendido que la calidad sostenible es la que un pipeline se niega a dejar retroceder.

⚔️ Instala una puerta de peso
  1. Configura size-limit con un presupuesto ajustado un poco por encima del tamaño comprimido actual de tu bundle inicial.
  2. Añade el check al CI en cada pull request y confirma que comenta el delta de tamaño sobre la rama base.
  3. Provoca una regresión a propósito —importa una librería pesada— y verifica que el check se pone en rojo y bloquea el merge.
  4. Divide el presupuesto global en presupuestos por ruta, con un límite estricto para tu pantalla más crítica.
  5. Aplica una optimización real, baja el límite hasta el nuevo suelo y comprueba que el trinquete impide que el peso vuelva a subir.