Presupuesto de Rendimiento Frontend: 6 Estrategias Clave para la Optimización de Core Web Vitals

性能优化

Puntos de Dolor del Rendimiento Frontend: Por Qué Tu Sitio Sigue Volviéndose Más Lento

En 2026, la complejidad de los proyectos frontend sigue en aumento. Excesos de LCP, saltos de CLS, retrasos de INP, inflación de bundles y regresiones de rendimiento invisibles—estos problemas están erosionando la experiencia de usuario y el posicionamiento en buscadores.

Punto de Dolor Síntoma Típico Impacto en el Negocio
Exceso de LCP Primera pintura > 3s Tasa de rebote aumenta 53%
Saltos de CLS Desplazamiento de layout > 0.25 Clics erróneos, erosión de confianza
Retraso de INP Respuesta a interacción > 500ms Abandono de usuarios aumenta 40%
Inflación de bundle JS > 500KB (gzip) Tiempo de primera carga se duplica
Regresión invisible Puntuaciones se degradan silenciosamente después del deploy Problemas se acumulan por meses

Referencia Rápida de Conceptos Clave

Concepto Descripción Objetivo
Presupuesto de Rendimiento Umbrales cuantificados para métricas de rendimiento Definido por el equipo
LCP Largest Contentful Paint ≤ 2.5s
FID First Input Delay ≤ 100ms
CLS Cumulative Layout Shift ≤ 0.1
INP Interaction to Next Paint ≤ 200ms
TTFB Time to First Byte ≤ 800ms
Lighthouse Herramienta automatizada de auditoría de rendimiento de Google Puntuación ≥ 90
RUM (Real User Monitoring) Recopilación de datos de rendimiento de usuarios reales P75 aprobando
Análisis de Bundle Seguimiento de composición y tamaño del bundle JS ≤ 200KB
Code Splitting División de unidades de carga por ruta/funcionalidad Primera pantalla ≤ 100KB

Cinco Desafíos: Por Qué los Presupuestos de Rendimiento No Se Mantienen

1. Definiciones de Métricas Poco Claras — No hay consenso en el equipo sobre qué es "buen rendimiento". ¿Datos de laboratorio o de campo? ¿P75 o P95?

2. Brechas en la Aplicación del Presupuesto — Presupuestos definidos pero no aplicados. Los PRs se fusionan sin importar las violaciones.

3. Impacto de Scripts de Terceros — Analíticas, anuncios y widgets de chat contribuyen al 40-60% del tiempo de ejecución de JS, y apenas los controlas.

4. Brecha de Rendimiento Móvil — Lighthouse en escritorio 95, LCP en Android de gama baja 6 segundos. La brecha es enorme.

5. Puntos Ciegos en la Detección de Regresiones — Sin monitoreo continuo, el rendimiento se degrada como hervir una rana—lo notas cuando ya es grave.


Estrategia 1: Recopilación y Análisis de Core Web Vitals

Usa la biblioteca web-vitals para recopilar métricas de usuarios reales y reportarlas a tu plataforma de analíticas:

import { onLCP, onFID, onCLS, onINP, onTTFB } from 'web-vitals';

interface PerformanceMetric {
  name: string;
  value: number;
  rating: 'good' | 'needs-improvement' | 'poor';
  delta: number;
  navigationType: string;
  timestamp: number;
}

function reportMetric(metric: PerformanceMetric): void {
  const body = JSON.stringify({
    name: metric.name,
    value: Math.round(metric.value),
    rating: metric.rating,
    delta: Math.round(metric.delta),
    navigationType: metric.navigationType,
    url: window.location.href,
    timestamp: metric.timestamp,
    userAgent: navigator.userAgent,
    connection: (navigator as any).connection?.effectiveType ?? 'unknown',
  });

  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/v1/metrics', body);
  } else {
    fetch('/api/v1/metrics', { body, method: 'POST', keepalive: true });
  }
}

onLCP(reportMetric);
onFID(reportMetric);
onCLS(reportMetric);
onINP(reportMetric);
onTTFB(reportMetric);

Agregación del lado del servidor por P75, comparada contra umbrales del presupuesto:

const PERFORMANCE_BUDGET: Record<string, { good: number; poor: number }> = {
  LCP:  { good: 2500, poor: 4000 },
  FID:  { good: 100,  poor: 300 },
  CLS:  { good: 0.1,  poor: 0.25 },
  INP:  { good: 200,  poor: 500 },
  TTFB: { good: 800,  poor: 1800 },
};

function evaluateBudget(
  metric: string,
  p75Value: number,
): 'pass' | 'warning' | 'fail' {
  const budget = PERFORMANCE_BUDGET[metric];
  if (!budget) return 'pass';
  if (p75Value <= budget.good) return 'pass';
  if (p75Value <= budget.poor) return 'warning';
  return 'fail';
}

Estrategia 2: Presupuesto de Tamaño de Bundle e Integración con Lighthouse CI

Aplica presupuestos de tamaño de bundle y puntuación de Lighthouse en tu pipeline de CI/CD:

// lighthouse-budget.json
const lighthouseBudget = {
  budgets: [
    {
      path: '/*',
      options: {
        firstContentfulPaint: 1800,
        largestContentfulPaint: 2500,
        cumulativeLayoutShift: 0.1,
        totalBlockingTime: 200,
        interactive: 3500,
      },
      resourceSizes: [
        { resourceType: 'script', budget: 200 },
        { resourceType: 'stylesheet', budget: 50 },
        { resourceType: 'image', budget: 300 },
        { resourceType: 'total', budget: 800 },
      ],
      resourceCounts: [
        { resourceType: 'third-party', budget: 5 },
        { resourceType: 'total', budget: 30 },
      ],
    },
  ],
};

export default lighthouseBudget;

Integración con GitHub Actions:

# .github/workflows/lighthouse-ci.yml
name: Lighthouse CI
on: [push]
jobs:
  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - run: npm ci && npm run build
      - name: Run Lighthouse CI
        uses: treosh/lighthouse-ci-action@v12
        with:
          configPath: .lighthouserc.json
          budgetPath: lighthouse-budget.json
          uploadArtifacts: true
          failOnBudgetExceeded: true
{
  "ci": {
    "collect": {
      "numberOfRuns": 3,
      "startServerCommand": "npm run preview",
      "url": ["http://localhost:4173/"]
    },
    "assert": {
      "assertions": {
        "categories:performance": ["error", { "minScore": 0.9 }],
        "first-contentful-paint": ["error", { "maxNumericValue": 1800 }],
        "largest-contentful-paint": ["error", { "maxNumericValue": 2500 }],
        "cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }],
        "total-blocking-time": ["error", { "maxNumericValue": 200 }]
      }
    }
  }
}

Estrategia 3: Optimización de Carga de Imágenes y Recursos

interface ImageOptimizationConfig {
  maxWidth: number;
  quality: number;
  formats: string[];
  sizes: string;
  lazyThreshold: number;
}

const imageConfig: ImageOptimizationConfig = {
  maxWidth: 1600,
  quality: 80,
  formats: ['avif', 'webp', 'jpg'],
  sizes: '(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 1200px',
  lazyThreshold: 0.1,
};

function generatePictureElement(
  src: string,
  alt: string,
  width: number,
  height: number,
  isLcp: boolean = false,
): string {
  const { formats, sizes, quality } = imageConfig;
  const widths = [400, 800, 1200, 1600];

  const sources = formats
    .filter((f) => f !== 'jpg')
    .map((format) => {
      const srcset = widths
        .map((w) => `${src}?w=${w}&q=${quality}&f=${format} ${w}w`)
        .join(', ');
      return `<source srcset="${srcset}" type="image/${format}" />`;
    })
    .join('\n');

  const imgSrcset = widths
    .map((w) => `${src}?w=${w}&q=${quality} ${w}w`)
    .join(', ');

  const lcpAttrs = isLcp
    ? 'fetchpriority="high" loading="eager"'
    : 'loading="lazy"';

  return `<picture>\n${sources}\n<img src="${src}?w=${width}&q=${quality}" srcset="${imgSrcset}" sizes="${sizes}" alt="${alt}" width="${width}" height="${height}" decoding="async" ${lcpAttrs} />\n</picture>`;
}

function setupResourceHints(): void {
  const preconnectDomains = [
    'https://fonts.googleapis.com',
    'https://cdn.example.com',
  ];
  preconnectDomains.forEach((domain) => {
    const link = document.createElement('link');
    link.rel = 'preconnect';
    link.href = domain;
    link.crossOrigin = 'anonymous';
    document.head.appendChild(link);
  });
}

Estrategia 4: Code Splitting y Lazy Loading

import { defineAsyncComponent } from 'vue';

interface ChunkConfig {
  name: string;
  test: (modulePath: string) => boolean;
  priority: number;
  minSize: number;
}

const chunkStrategy: ChunkConfig[] = [
  { name: 'vendor-vue', test: /node_modules\/vue/, priority: 10, minSize: 0 },
  { name: 'vendor-ui', test: /node_modules\/@ui-lib/, priority: 8, minSize: 10000 },
  { name: 'vendor-utils', test: /node_modules\/(lodash|date-fns)/, priority: 5, minSize: 0 },
  { name: 'vendor-other', test: /node_modules/, priority: -10, minSize: 20000 },
];

function buildRollupOutputChunks() {
  const manualChunks = (id: string) => {
    if (!id.includes('node_modules')) return;
    for (const chunk of chunkStrategy) {
      if (chunk.test(id)) return chunk.name;
    }
  };
  return { manualChunks };
}

const LazyChart = defineAsyncComponent({
  loader: () => import('@/components/HeavyChart.vue'),
  loadingComponent: () => null,
  delay: 200,
  timeout: 10000,
});

const LazyEditor = defineAsyncComponent({
  loader: () => import('@/components/RichEditor.vue'),
  loadingComponent: () => null,
  delay: 200,
  timeout: 10000,
});

function setupIntersectionLazyLoad(): void {
  const observer = new IntersectionObserver(
    (entries) => {
      entries.forEach((entry) => {
        if (entry.isIntersecting) {
          const el = entry.target as HTMLElement;
          const modulePath = el.dataset.lazyModule;
          if (modulePath) {
            import(/* @vite-ignore */ modulePath).then((mod) => {
              el.dispatchEvent(new CustomEvent('lazy-loaded', { detail: mod }));
            });
            observer.unobserve(el);
          }
        }
      });
    },
    { rootMargin: '200px' },
  );

  document.querySelectorAll('[data-lazy-module]').forEach((el) => {
    observer.observe(el);
  });
}

Estrategia 5: Gobernanza de Scripts de Terceros

interface ThirdPartyScript {
  name: string;
  src: string;
  category: 'analytics' | 'ads' | 'chat' | 'social' | 'other';
  impact: 'high' | 'medium' | 'low';
  loadStrategy: 'async' | 'defer' | 'lazy' | 'worker';
  condition?: () => boolean;
}

const thirdPartyRegistry: ThirdPartyScript[] = [
  {
    name: 'google-analytics',
    src: 'https://www.googletagmanager.com/gtag/js',
    category: 'analytics',
    impact: 'medium',
    loadStrategy: 'async',
    condition: () => !navigator.doNotTrack,
  },
  {
    name: 'live-chat',
    src: 'https://chat.example.com/widget.js',
    category: 'chat',
    impact: 'high',
    loadStrategy: 'lazy',
    condition: () => window.innerWidth >= 768,
  },
  {
    name: 'ad-network',
    src: 'https://ads.example.com/sdk.js',
    category: 'ads',
    impact: 'high',
    loadStrategy: 'worker',
  },
];

function loadThirdPartyScript(script: ThirdPartyScript): Promise<void> {
  if (script.condition && !script.condition()) {
    return Promise.resolve();
  }

  return new Promise((resolve, reject) => {
    const el = document.createElement('script');
    el.src = script.src;

    switch (script.loadStrategy) {
      case 'async':
        el.async = true;
        document.head.appendChild(el);
        break;
      case 'defer':
        el.defer = true;
        document.head.appendChild(el);
        break;
      case 'lazy':
        setTimeout(() => {
          el.async = true;
          document.head.appendChild(el);
        }, 3000);
        break;
      case 'worker':
        fetch(script.src)
          .then((r) => r.text())
          .then((code) => {
            const blob = new Blob([code], { type: 'application/javascript' });
            const worker = new Worker(URL.createObjectURL(blob));
            worker.postMessage({ type: 'init' });
          })
          .catch(reject);
        break;
    }

    el.onload = () => resolve();
    el.onerror = () => reject(new Error(`Failed to load: ${script.name}`));
  });
}

function initThirdPartyScripts(): void {
  const highPriority = thirdPartyRegistry.filter((s) => s.impact === 'low');
  const lowPriority = thirdPartyRegistry.filter((s) => s.impact !== 'low');

  highPriority.forEach(loadThirdPartyScript);

  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => {
      lowPriority.forEach(loadThirdPartyScript);
    });
  } else {
    setTimeout(() => {
      lowPriority.forEach(loadThirdPartyScript);
    }, 5000);
  }
}

Estrategia 6: Sistema de Monitoreo de Rendimiento y Alertas

interface AlertRule {
  metric: string;
  threshold: number;
  windowMinutes: number;
  sampleSize: number;
  channels: ('email' | 'slack' | 'webhook')[];
}

interface PerformanceAlert {
  metric: string;
  currentValue: number;
  threshold: number;
  affectedUsers: number;
  timestamp: string;
}

const alertRules: AlertRule[] = [
  { metric: 'LCP', threshold: 4000, windowMinutes: 30, sampleSize: 50, channels: ['slack', 'email'] },
  { metric: 'CLS', threshold: 0.25, windowMinutes: 30, sampleSize: 50, channels: ['slack'] },
  { metric: 'INP', threshold: 500, windowMinutes: 60, sampleSize: 100, channels: ['slack', 'email'] },
  { metric: 'TTFB', threshold: 1800, windowMinutes: 30, sampleSize: 50, channels: ['slack'] },
];

class PerformanceMonitor {
  private metricsBuffer: Map<string, number[]> = new Map();

  addMetric(name: string, value: number): void {
    if (!this.metricsBuffer.has(name)) {
      this.metricsBuffer.set(name, []);
    }
    const buffer = this.metricsBuffer.get(name)!;
    buffer.push(value);
    if (buffer.length > 1000) buffer.shift();
    this.checkAlerts(name);
  }

  private checkAlerts(metricName: string): void {
    const rule = alertRules.find((r) => r.metric === metricName);
    if (!rule) return;

    const buffer = this.metricsBuffer.get(metricName) ?? [];
    const recentValues = buffer.slice(-rule.sampleSize);
    if (recentValues.length < rule.sampleSize) return;

    const p75 = this.calculatePercentile(recentValues, 75);
    if (p75 > rule.threshold) {
      this.fireAlert({
        metric: metricName,
        currentValue: p75,
        threshold: rule.threshold,
        affectedUsers: recentValues.length,
        timestamp: new Date().toISOString(),
      });
    }
  }

  private calculatePercentile(values: number[], percentile: number): number {
    const sorted = [...values].sort((a, b) => a - b);
    const index = Math.ceil((percentile / 100) * sorted.length) - 1;
    return sorted[index];
  }

  private fireAlert(alert: PerformanceAlert): void {
    console.warn(`[Performance Alert] ${alert.metric} P75=${alert.currentValue}ms exceeds ${alert.threshold}ms`);
    fetch('/api/v1/alerts', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(alert),
      keepalive: true,
    });
  }
}

const monitor = new PerformanceMonitor();

Guía de Errores Comunes

❌ Enfoque Incorrecto ✅ Enfoque Correcto
Verificar solo datos de laboratorio de Lighthouse Combinar con datos de campo RUM—validación dual laboratorio + campo
Presupuestos definidos solo en documentación Aplicar en CI/CD con failOnBudgetExceeded: true
Lazy loading en imágenes LCP Los elementos LCP deben usar loading="eager" + fetchpriority="high"
Cargar todos los scripts de terceros sincrónicamente Priorizar por impacto; diferir los de baja prioridad con requestIdleCallback
Optimizar solo para escritorio Usar móvil de gama baja como referencia—Moto G Power y dispositivos similares de gama media

Solución de Problemas

Síntoma Posible Causa Solución
La puntuación de Lighthouse fluctúa Inestabilidad de red, limitación de CPU inconsistente Ejecutar múltiples veces, tomar la mediana, usar numberOfRuns: 3
LCP consistentemente > 4s Imágenes no optimizadas o sin precarga Usar AVIF + fetchpriority="high" + preload
CLS > 0.25 Imágenes/anuncios sin dimensiones declaradas Siempre establecer width/height o aspect-ratio
INP > 500ms Tareas largas bloqueando el hilo principal Dividir tareas largas, usar scheduler.yield() para ceder
Presupuesto de bundle en CI no funciona budgetPath incorrecto Verificar ruta relativa, revisar sintaxis JSON
Pérdida de datos de web-vitals sendBeacon cancelado al descargar la página Usar fetch + keepalive: true como respaldo
Code splitting hace la primera pintura más lenta Sobre-división causa cascadas de solicitudes Fusionar chunks pequeños, inliner recursos de ruta crítica
Fallo de carga de script de terceros Política CSP bloqueando Agregar dominios correspondientes a Content-Security-Policy
TTFB > 1.8s SSR lento o fallo de caché CDN Habilitar caché en el borde, optimizar estrategia de caché SSR
Parpadeo de fuentes (FOIT/FOUT) Falta declaración font-display Usar font-display: swap + preload

Optimización Avanzada

  1. Mover scripts de terceros a Web Workers con Partytown — Descargar analíticas y otro JS no crítico del hilo principal. INP puede reducirse 30-50%. Simplemente configura <script type="text/partytown">.

  2. Prerrenderizado con Speculation Rules — Usa <script type="speculationrules"> para prerrenderizar páginas que los usuarios probablemente visitarán. LCP puede bajar de 0.5s.

  3. Experimentos A/B de rendimiento basados en RUM — Aplica diferentes estrategias de optimización a diferentes segmentos de usuarios, compara diferencias de métricas P75 y deja que los datos guíen las decisiones en lugar de adivinar.

  4. Caché de streaming con Service Worker — Usa la Streams API para respuestas cacheadas progresivas, de modo que la primera pintura no se bloquee por descargas completas de recursos.


Comparación de Herramientas

Dimensión Lighthouse WebPageTest SpeedCurve Calibre
Enfoque Auditoría automatizada Análisis profundo de red Monitoreo continuo Colaboración en equipo
Tipo de Datos Sintético Sintético Sintético + RUM Sintético + RUM
Emulación de Dispositivo Limitada (Throttling) Rica (dispositivos reales) Multi-región, multi-dispositivo Multi-región, multi-dispositivo
Integración CI Excelente (CI oficial) Regular Excelente Excelente
Costo Gratuito Nivel gratuito De pago De pago
Mejor Para Auditorías rápidas, puertas de CI Diagnósticos profundos de red Monitoreo de tendencias a largo plazo Colaboración en equipo y alertas

Resumen y Perspectivas

Los presupuestos de rendimiento frontend no son una tarea de una sola vez—son una práctica de ingeniería continua. Tendencias clave para 2026:

  • INP reemplaza a FID como métrica oficial de Core Web Vitals—la capacidad de respuesta a interacciones es el nuevo foco
  • Presupuestos de rendimiento en CI — de convenciones en documentación a aplicación en código, failOnBudgetExceeded es la línea base
  • Decisiones basadas en RUM — los datos de laboratorio son solo el punto de partida; los datos de usuarios reales revelan la verdad
  • Descarga a Web Workers — soluciones como Partytown evitan que los scripts de terceros arrastren el hilo principal

Construye un sistema de presupuesto de rendimiento para que tu sitio tenga una "verificación de seguridad de rendimiento" con cada lanzamiento—en lugar de descubrir problemas solo después de que los usuarios se quejen.


Herramientas en Línea Recomendadas

  • Formateador JSON — Formatear datos JSON de reportes de Lighthouse
  • Calculadora de Hash — Generar hashes de archivos de recursos para validación de caché
  • cURL a Código — Convertir solicitudes API a código para integración rápida de monitoreo de rendimiento

Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →

#性能预算#前端性能优化#Core Web Vitals#Lighthouse#包体积优化#2026#性能优化