StackPractices
intermediate Por Mathias Paulenko

Plantilla de Performance Budget para Frontend

Plantilla para definir budgets de JS, CSS, imágenes y fuentes por ruta, con un método para elegir los números, enforcement en CI y umbrales de monitoreo en producción.

Resumen

Un performance budget pone límites al tamaño y al tiempo de los recursos que componen una página. Sin uno, el peso de la página crece dependencia a dependencia hasta que alguien nota que el sitio va lento — y normalmente lo nota un usuario, no un desarrollador. Esta plantilla te da budgets por ruta, un gate de CI que rompe el build cuando te pasas, y umbrales de monitoreo para después del despliegue.

Cuándo Usar

  • Al empezar un proyecto web nuevo
  • Cuando sufres regresiones de rendimiento
  • Al configurar monitoreo de Core Web Vitals
  • Al establecer estándares de rendimiento del equipo
  • Al preparar lanzamientos sensibles al rendimiento

Solución

# Performance Budget — `<Nombre del Proyecto>`

## Resumen del Budget

| Campo | Valor |
|-------|-------|
| Proyecto | App Web de Ejemplo |
| Última Actualización | 2026-07-05 |
| Responsable | Equipo Frontend |
| Enforcement | Gate de CI + Lighthouse CI |
| Monitoreo | Real User Monitoring (RUM) + Sintético |

## 1. Budgets por Ruta

### Página Principal (`/`)

| Tipo de Recurso | Budget | Actual | Estado | Notas |
|-----------------|--------|--------|--------|-------|
| JS total (gzipped) | 150 KB | 142 KB | ✅ | Incluye vendor bundle |
| CSS total (gzipped) | 30 KB | 28 KB | ✅ | Tailwind + custom |
| Imágenes totales | 500 KB | 480 KB | ✅ | Hero + thumbnails |
| Fuentes totales | 100 KB | 80 KB | ✅ | 2 familias de fuentes |
| HTML | 50 KB | 35 KB | ✅ | Renderizado en servidor |
| Peso total de página | 830 KB | 765 KB | ✅ | Suma de todos los recursos |
| Requests de JS | 3 | 3 | ✅ | Vendor + app + lazy |
| Requests de CSS | 1 | 1 | ✅ | Hoja de estilos única |
| Requests de imágenes | 8 | 7 | ✅ | — |
| Requests de fuentes | 2 | 2 | ✅ | — |
| Requests totales | 15 | 14 | ✅ | — |

### Página de Producto (`/products/:id`)

| Tipo de Recurso | Budget | Actual | Estado | Notas |
|-----------------|--------|--------|--------|-------|
| JS total (gzipped) | 180 KB | 165 KB | ✅ | Incluye galería de producto |
| CSS total (gzipped) | 35 KB | 32 KB | ✅ | — |
| Imágenes totales | 800 KB | 750 KB | ✅ | Imágenes de producto |
| Fuentes totales | 100 KB | 80 KB | ✅ | Igual que la principal |
| HTML | 60 KB | 42 KB | ✅ | Datos de producto |
| Peso total de página | 1,175 KB | 1,069 KB | ✅ | — |
| Requests de JS | 4 | 4 | ✅ | Vendor + app + gallery + reviews |
| Requests de CSS | 1 | 1 | ✅ | — |
| Requests de imágenes | 12 | 10 | ✅ | Galería + thumbnails |
| Requests de fuentes | 2 | 2 | ✅ | — |
| Requests totales | 20 | 18 | ✅ | — |

### Página de Checkout (`/checkout`)

| Tipo de Recurso | Budget | Actual | Estado | Notas |
|-----------------|--------|--------|--------|-------|
| JS total (gzipped) | 200 KB | 188 KB | ✅ | Incluye SDK de pago |
| CSS total (gzipped) | 35 KB | 30 KB | ✅ | — |
| Imágenes totales | 100 KB | 50 KB | ✅ | Imágenes mínimas |
| Fuentes totales | 100 KB | 80 KB | ✅ | — |
| HTML | 40 KB | 30 KB | ✅ | — |
| Peso total de página | 475 KB | 428 KB | ✅ | — |
| Requests de JS | 5 | 5 | ✅ | Vendor + app + payment + validation + analytics |
| Requests de CSS | 1 | 1 | ✅ | — |
| Requests de imágenes | 2 | 2 | ✅ | Logo + sello de seguridad |
| Requests de fuentes | 2 | 2 | ✅ | — |
| Requests totales | 11 | 11 | ✅ | — |

### Página de Dashboard (`/dashboard`)

| Tipo de Recurso | Budget | Actual | Estado | Notas |
|-----------------|--------|--------|--------|-------|
| JS total (gzipped) | 250 KB | 245 KB | ⚠️ | La librería de gráficos pesa |
| CSS total (gzipped) | 40 KB | 38 KB | ✅ | — |
| Imágenes totales | 200 KB | 150 KB | ✅ | Avatares + gráficos |
| Fuentes totales | 100 KB | 80 KB | ✅ | — |
| HTML | 80 KB | 65 KB | ✅ | Datos del dashboard |
| Peso total de página | 670 KB | 578 KB | ✅ | — |
| Requests de JS | 6 | 6 | ✅ | Vendor + app + charts + date + table + auth |
| Requests de CSS | 1 | 1 | ✅ | — |
| Requests de imágenes | 5 | 4 | ✅ | — |
| Requests de fuentes | 2 | 2 | ✅ | — |
| Requests totales | 15 | 14 | ✅ | — |

## 2. Objetivos de Core Web Vitals

| Métrica | Bueno | Necesita Mejora | Pobre | Objetivo | Actual (p75) | Estado |
|---------|-------|-----------------|-------|----------|--------------|--------|
| LCP | < 2.5s | 2.5s - 4.0s | > 4.0s | < 2.5s | 2.1s | ✅ |
| INP | < 200ms | 200ms - 500ms | > 500ms | < 200ms | 180ms | ✅ |
| CLS | < 0.1 | 0.1 - 0.25 | > 0.25 | < 0.1 | 0.05 | ✅ |
| FCP | < 1.8s | 1.8s - 3.0s | > 3.0s | < 1.8s | 1.4s | ✅ |
| TTFB | < 800ms | 800ms - 1.8s | > 1.8s | < 800ms | 650ms | ✅ |

## 3. Budgets de Tiempo

| Métrica | Budget | Actual | Estado |
|---------|--------|--------|--------|
| Time to First Byte (TTFB) | < 800ms | 650ms | ✅ |
| First Contentful Paint (FCP) | < 1.8s | 1.4s | ✅ |
| Largest Contentful Paint (LCP) | < 2.5s | 2.1s | ✅ |
| Time to Interactive (TTI) | < 3.5s | 3.1s | ✅ |
| Total Blocking Time (TBT) | < 200ms | 150ms | ✅ |
| Cumulative Layout Shift (CLS) | < 0.1 | 0.05 | ✅ |
| Interaction to Next Paint (INP) | < 200ms | 180ms | ✅ |

## 4. Budgets de Terceros

| Tercero | Tipo | Budget de JS | Actual | Estado | Notas |
|---------|------|--------------|--------|--------|-------|
| Google Analytics | Analytics | 45 KB | 42 KB | ✅ | Carga async |
| Stripe.js | Pago | 50 KB | 48 KB | ✅ | Solo en checkout |
| Sentry | Error tracking | 25 KB | 22 KB | ✅ | Carga async |
| Google Maps | Mapas | 80 KB | 75 KB | ✅ | Solo en localizador de tiendas |
| Intercom | Soporte | 60 KB | 55 KB | ✅ | Carga diferida |
| **Total terceros** | | **260 KB** | **242 KB** | **✅** | |

## 5. Enforcement en CI

### Chequeo de Tamaño de Bundle

```yaml
name: Performance Budget Check
on: [pull_request]

jobs:
  budget-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: '20'
      - run: npm ci
      - run: npm run build

      - name: Check bundle sizes
        run: |
          node scripts/check-bundle-size.js

      - name: Lighthouse CI
        run: |
          npm install -g @lhci/cli
          lhci collect --url=http://localhost:3000 --numberOfRuns=3
          lhci assert --preset=lighthouse:recommended
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_TOKEN }}
```

### Script de Tamaño de Bundle

```javascript
// scripts/check-bundle-size.js
const fs = require('fs');
const path = require('path');
const gzipSize = require('gzip-size');

const budgets = {
  'dist/assets/vendor.js': { max: 100000, type: 'gzipped' },
  'dist/assets/app.js': { max: 50000, type: 'gzipped' },
  'dist/assets/styles.css': { max: 30000, type: 'gzipped' },
};

let failed = false;

for (const [file, budget] of Object.entries(budgets)) {
  const filePath = path.join(process.cwd(), file);
  if (!fs.existsSync(filePath)) {
    console.error(`MISSING: ${file}`);
    failed = true;
    continue;
  }

  const content = fs.readFileSync(filePath);
  const size = budget.type === 'gzipped' ? gzipSize.sync(content) : content.length;
  const sizeKB = (size / 1024).toFixed(1);
  const maxKB = (budget.max / 1024).toFixed(1);

  if (size > budget.max) {
    console.error(`FAIL: ${file} — ${sizeKB} KB (budget: ${maxKB} KB)`);
    failed = true;
  } else {
    console.log(`OK: ${file} — ${sizeKB} KB (budget: ${maxKB} KB)`);
  }
}

if (failed) {
  process.exit(1);
}
```

### Configuración de Lighthouse CI

```json
{
  "ci": {
    "collect": {
      "url": [
        "http://localhost:3000",
        "http://localhost:3000/products/sample",
        "http://localhost:3000/checkout"
      ],
      "numberOfRuns": 3
    },
    "assert": {
      "preset": "lighthouse:recommended",
      "assertions": {
        "categories:performance": ["warn", { "minScore": 0.9 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["warn", { "maxNumericValue": 200 }]
      }
    }
  }
}
```

## 6. Monitoreo en Producción

### Umbrales RUM

| Métrica | Umbral de Alerta | Página | Acción |
|---------|------------------|--------|--------|
| LCP p75 > 3s | Advertencia | Cualquiera | Investigar rutas lentas |
| LCP p75 > 4s | Crítica | Cualquiera | Rollback si hubo deploy reciente |
| INP p75 > 300ms | Advertencia | Cualquiera | Revisar tareas largas |
| CLS p75 > 0.15 | Advertencia | Cualquiera | Revisar layout shifts |
| TTFB p75 > 1.2s | Advertencia | Cualquiera | Revisar respuesta del servidor |
| Tamaño de bundle JS > budget | Advertencia | Cualquiera | Investigar crecimiento del bundle |

### Monitoreo Sintético

| Ruta | Frecuencia | Métricas | Alerta |
|------|------------|----------|--------|
| / | Cada 5 min | LCP, CLS, INP, TTFB | LCP > 3s |
| /products/sample | Cada 10 min | LCP, CLS, INP | LCP > 3s |
| /checkout | Cada 10 min | LCP, CLS, INP | LCP > 3s |
| /dashboard | Cada 10 min | LCP, CLS, INP | LCP > 3s |

Explicación

Los budgets funcionan porque el límite existe antes que el código, y CI lo hace cumplir. El desglose por ruta importa: una página de checkout cargando un SDK de pago necesita legítimamente más JS que un post de blog, así que un único budget global o estrangula las páginas pesadas o queda flojo en todas partes.

Los objetivos de Core Web Vitals salen directamente de los umbrales publicados de Google: LCP, INP y CLS son las tres métricas de la señal de ranking. Cumplirlos te compra la experiencia de usuario y el lado SEO a la vez.

Los scripts de terceros son la fuente más común de desbordes de budget. Cada tercero debería tener su propio budget y cargarse solo donde se necesita. Stripe.js debería cargar solo en la página de checkout, no globalmente. Analytics debería cargar asíncrono para no bloquear el renderizado.

El enforcement en CI es lo que convierte un budget en algo real en lugar de aspiracional. El chequeo de bundle corre en cada PR y rompe el build si un asset supera su número; Lighthouse CI carga el sitio construido y falla en los umbrales de CWV. Juntos atrapan tanto “el archivo creció” como “la página se hizo más lenta”.

El monitoreo en producción atrapa lo que CI no puede ver: CI prueba en un runner limpio, mientras tus usuarios están en teléfonos reales sobre redes reales. RUM te da la realidad del p75, el mismo número que Google usa para ranking — y los chequeos sintéticos te dan una base estable contra la que comparar.

Cómo Elegir Tus Números

Los números de budget de la plantilla son ilustrativos; los tuyos deberían salir de medir, no de copiar.

Paso 1: Mide lo que sirves hoy. Corre un build, comprime los bundles con gzip y anota los tamaños reales por ruta. Usa Lighthouse o WebPageTest para el lado de tiempos, y saca el p75 de RUM si ya tienes analytics. Esos son tus números de base, sean buenos o vergonzosos.

Paso 2: Fija budgets en la base, más holgura. Si tus números ya son aceptables, ancla los budgets al tamaño actual para que nada pueda retroceder. Así es como los budgets se ganan la vida. Si ya estás por encima, fija el budget en el tamaño actual igualmente y obliga a cada feature nueva a pagarse sola eliminando algo viejo; baja el budget un 5-10% cada trimestre a medida que limpias.

Paso 3: Budget por ruta, no por sitio. Una página de checkout con un SDK de pago legítimamente carga más JS que un post de blog. Un único budget global o bloquea las rutas pesadas o queda flojo en todas partes. Las tablas por ruta de la plantilla existen precisamente porque “un solo número” no funciona.

Paso 4: Budgeta los terceros por separado. Van a suponer entre un tercio y la mitad de tu peso de página, y son los assets que olvidas hasta que alguien añade un widget de chat a todas las páginas. Dale a cada uno su propia línea y su propio budget — hace concreta la conversación de “¿de verdad necesitamos esto?”.

Paso 5: Revisa cada trimestre. Los budgets se pudren. Un número que era ajustado en enero se convierte en mentira para junio: o la app creció legítimamente o el budget estaba mal. Pon un recordatorio en el campo Última Actualización de la plantilla y trata la revisión como una auditoría de dependencias.

Mermaid flowchart TD diagram

El bucle es el punto: CI detiene las regresiones antes de que salgan, RUM atrapa lo que el entorno limpio de CI no puede ver, y la revisión trimestral mantiene los números honestos a medida que la app evoluciona.

Historias Reales de Budgets

Los budgets parecen abstractos hasta que ves qué pasa sin ellos — o con ellos haciendo enforcement.

El rebuild de Pinterest (2018). Tras reconstruir su experiencia web móvil por rendimiento, el equipo de ingeniería de Pinterest reportó un aumento del 40% en el tiempo en sitio y mejoras significativas en registros desde tráfico de búsqueda. La lección que publicaron fue contundente: la velocidad es la feature — no fue un efecto secundario del rediseño, fue el rediseño. Un budget es cómo evitas que esa victoria se evapore seis meses después.

La matemática de conversión de Walmart. La cifra tan citada — cada 100 ms de mejora en tiempo de carga correlacionaba con aproximadamente 1% de aumento en conversión — salió del análisis propio de Walmart. Es el número que hace los budgets legibles para quien no es ingeniero: una línea de budget no es “200 KB de JS”, es “una porción medible de ingresos”.

La tendencia de peso de página del HTTP Archive. El peso mediano de una página desktop ha crecido de menos de 1 MB hace una década a más de 2 MB hoy — y el móvil siguió la misma curva. Nada de ese crecimiento es una decisión que alguien tomó; son mil pequeños PRs de “solo un script más”. Un budget es el cable de trampa que convierte el goteo en un trade-off consciente.

El estándar publicado de gov.uk. El Government Service Standard del Reino Unido exige a los equipos medir el rendimiento continuamente e incluye el peso de página en sus criterios de performance — un budget en todo menos en el nombre, aplicado a servicios públicos usados por millones. Si un gobierno puede mantener la línea en peso de página, un equipo de producto no tiene excusa.

El patrón en los cuatro: los equipos que mantienen páginas rápidas no tienen mejor disciplina — tienen un número, un gate y una cadencia de revisión. Exactamente las tres partes de esta plantilla.

Variantes

ContextoEnfoqueNotas
SPABudget por ruta (lazy loaded)El code splitting es crítico
SSR / SSGBudget por plantilla de páginaEl renderizado en servidor reduce el JS necesario
Mobile-firstBudgets más estrictos (50% del desktop)Las redes móviles son más lentas
E-commerceLCP más estricto (< 2s)Impacto del rendimiento en ingresos
Sitio de contenidoFoco en budgets de imágenesLas imágenes son los assets más pesados
App enterprisePermite bundles más grandesUsuarios internos, red controlada

Lo que funciona

  1. Fijar budgets por ruta; un único budget global es demasiado restrictivo
  2. Hacer enforcement en CI; atrapa las regresiones antes de producción
  3. Monitorear en producción; CI no puede replicar las condiciones de usuarios reales
  4. Rastrear los scripts de terceros por separado; son el desborde más común
  5. Usar tamaños gzip; los tamaños crudos engañan
  6. Revisar los budgets cada trimestre; ajústalos a medida que la app evoluciona
  7. Hacer los budgets visibles; dashboards mostrando el estado del budget por ruta

Errores Comunes

  1. Sin enforcement: budgets sin gates de CI son aspiracionales, no reales
  2. Demasiado generosos: budgets que nunca se superan no previenen regresiones
  3. Demasiado estrictos: budgets que bloquean cada PR acaban ignorados o eliminados
  4. Ignorar los scripts de terceros: pueden suponer más del 50% del peso de página
  5. No monitorear producción: CI pasa pero los usuarios reales sufren
  6. Usar tamaños crudos en vez de gzipped: 200 KB crudos pueden ser 60 KB comprimidos
  7. Sin desglose por ruta: un budget global de 200 KB de JS no contempla las necesidades de cada ruta

Ver También

Código complementario: recursos de performance budget: la plantilla, el workflow de CI, el script de chequeo de bundle y la configuración de Lighthouse de esta página.

Preguntas frecuentes

¿Cómo determino el budget correcto para mi app?

Mide primero el rendimiento actual. Fija budgets un 10-20% por encima de los tamaños actuales para permitir el desarrollo normal. Si los tamaños actuales ya son demasiado grandes, fija los budgets en los tamaños actuales y exige que cualquier código nuevo se compense eliminando código viejo. Usa Lighthouse y WebPageTest para medir.

¿Qué pasa si no podemos cumplir el budget?

Si no puedes cumplir un budget, o el budget es irrealista o el código necesita optimización. Empieza analizando el bundle con webpack-bundle-analyzer o source-map-explorer. Busca dependencias grandes que se puedan reemplazar, tree-shakear o cargar de forma diferida. Si el budget es genuinamente demasiado bajo, súbelo con justificación.

¿Deberíamos usar Lighthouse CI o chequeos de tamaño de bundle?

Ambos. Los chequeos de tamaño atrapan el crecimiento de assets (una dependencia nueva suma 50 KB). Lighthouse CI atrapa regresiones de rendimiento que los chequeos de tamaño no ven (una animación nueva bloquea el hilo principal). Miden cosas distintas y se complementan.

¿Cómo manejamos los scripts de terceros que no podemos controlar?

Cárgalos asíncronos, difiérelos o usa un tag manager. Fija budgets para su impacto en peso de página y tiempos. Monitorea su rendimiento en producción. Si un script de terceros supera consistentemente su budget o causa problemas de rendimiento, evalúa alternativas.

¿Cuál es la diferencia entre monitoreo RUM y sintético?

RUM (Real User Monitoring) recoge datos de usuarios reales que visitan tu sitio: refleja dispositivos, redes y patrones de uso reales. El monitoreo sintético corre pruebas programadas desde entornos controlados. RUM muestra la experiencia real; el sintético da baselines consistentes para comparar. Usa ambos.