Orçamento de Performance Frontend: 6 Estratégias Chave para Otimização de Core Web Vitals

性能优化

Pontos de Dor de Performance Frontend: Por Que Seu Site Continua Ficando Mais Lento

Em 2026, a complexidade dos projetos frontend continua aumentando. Excesso de LCP, saltos de CLS, atrasos de INP, inchaço de bundles e regressões de performance invisíveis—esses problemas estão corroendo a experiência do usuário e os rankings de busca.

Ponto de Dor Sintoma Típico Impacto no Negócio
Excesso de LCP Primeira pintura > 3s Taxa de rejeição aumenta 53%
Saltos de CLS Deslocamento de layout > 0.25 Cliques errados, erosão de confiança
Atraso de INP Resposta a interação > 500ms Churn de usuários aumenta 40%
Inchaço de bundle JS > 500KB (gzip) Tempo de primeiro carregamento dobra
Regressão invisível Pontuações degradam silenciosamente após deploy Problemas se acumulam por meses

Referência Rápida de Conceitos Chave

Conceito Descrição Objetivo
Orçamento de Performance Limites quantificados para métricas de performance Definido pelo time
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 Ferramenta automatizada de auditoria de performance do Google Pontuação ≥ 90
RUM (Real User Monitoring) Coleta de dados de performance de usuários reais P75 aprovando
Análise de Bundle Rastreamento de composição e tamanho do bundle JS ≤ 200KB
Code Splitting Divisão de unidades de carregamento por rota/funcionalidade Primeira tela ≤ 100KB

Cinco Desafios: Por Que Orçamentos de Performance Não Se Mantêm

1. Definições de Métricas Pouco Claras — Sem consenso do time sobre o que é "boa performance." Dados de laboratório ou de campo? P75 ou P95?

2. Lacunas na Aplicação do Orçamento — Orçamentos definidos mas não aplicados. PRs são mesclados independentemente de violações.

3. Impacto de Scripts de Terceiros — Analytics, anúncios e widgets de chat contribuem com 40-60% do tempo de execução de JS, e você mal os controla.

4. Lacuna de Performance Mobile — Lighthouse no desktop 95, LCP no Android de entrada 6 segundos. A lacuna é enorme.

5. Pontos Cegos na Detecção de Regressões — Sem monitoramento contínuo, a performance degrada como ferver um sapo—você percebe quando já está grave.


Estratégia 1: Coleta e Análise de Core Web Vitals

Use a biblioteca web-vitals para coletar métricas de usuários reais e reportar à sua plataforma de analytics:

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);

Agregação no lado do servidor por P75, comparada contra limites do orçamento:

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';
}

Estratégia 2: Orçamento de Tamanho de Bundle e Integração com Lighthouse CI

Aplique orçamentos de tamanho de bundle e pontuação do Lighthouse no seu 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;

Integração com 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 }]
      }
    }
  }
}

Estratégia 3: Otimização de Carregamento de Imagens e 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);
  });
}

Estratégia 4: Code Splitting e 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);
  });
}

Estratégia 5: Governança de Scripts de Terceiros

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

Estratégia 6: Sistema de Monitoramento de Performance e 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();

Guia de Armadilhas

❌ Abordagem Errada ✅ Abordagem Certa
Verificar apenas dados de laboratório do Lighthouse Combinar com dados de campo RUM—validação dupla laboratório + campo
Orçamentos definidos apenas em documentação Aplicar no CI/CD com failOnBudgetExceeded: true
Lazy loading em imagens LCP Elementos LCP devem usar loading="eager" + fetchpriority="high"
Carregar todos os scripts de terceiros sincronamente Priorizar por impacto; adiar os de baixa prioridade com requestIdleCallback
Otimizar apenas para desktop Usar mobile de entrada como referência—Moto G Power e dispositivos similares de gama média

Solução de Problemas

Sintoma Possível Causa Solução
Pontuação do Lighthouse flutua Instabilidade de rede, limitação de CPU inconsistente Executar múltiplas vezes, tirar a mediana, usar numberOfRuns: 3
LCP consistentemente > 4s Imagens não otimizadas ou sem pré-carregamento Usar AVIF + fetchpriority="high" + preload
CLS > 0.25 Imagens/anúncios sem dimensões declaradas Sempre definir width/height ou aspect-ratio
INP > 500ms Tarefas longas bloqueando a thread principal Dividir tarefas longas, usar scheduler.yield() para ceder
Orçamento de bundle no CI não funciona budgetPath incorreto Verificar caminho relativo, checar sintaxe JSON
Perda de dados do web-vitals sendBeacon cancelado ao descarregar a página Usar fetch + keepalive: true como fallback
Code splitting torna a primeira pintura mais lenta Excesso de divisão causa cascatas de requisições Mesclar chunks pequenos, inliner recursos de caminho crítica
Falha no carregamento de script de terceiros Política CSP bloqueando Adicionar domínios correspondentes ao Content-Security-Policy
TTFB > 1.8s SSR lento ou falta de cache CDN Habilitar cache de borda, otimizar estratégia de cache SSR
Flash de fontes (FOIT/FOUT) Falta de declaração font-display Usar font-display: swap + preload

Otimização Avançada

  1. Mover scripts de terceiros para Web Workers com Partytown — Descarregar analytics e outro JS não crítico da thread principal. INP pode cair 30-50%. Simplesmente configure <script type="text/partytown">.

  2. Pré-renderização com Speculation Rules — Use <script type="speculationrules"> para pré-renderizar páginas que os usuários provavelmente visitarão. LCP pode cair abaixo de 0.5s.

  3. Experimentos A/B de performance baseados em RUM — Aplique diferentes estratégias de otimização a diferentes segmentos de usuários, compare diferenças de métricas P75 e deixe os dados guiar as decisões em vez de adivinhar.

  4. Cache de streaming com Service Worker — Use a Streams API para respostas cacheadas progressivas, para que a primeira pintura não seja bloqueada por downloads completos de recursos.


Comparação de Ferramentas

Dimensão Lighthouse WebPageTest SpeedCurve Calibre
Foco Auditoria automatizada Análise profunda de rede Monitoramento contínuo Colaboração em equipe
Tipo de Dados Sintético Sintético Sintético + RUM Sintético + RUM
Emulação de Dispositivo Limitada (Throttling) Rica (dispositivos reais) Multi-região, multi-dispositivo Multi-região, multi-dispositivo
Integração CI Excelente (CI oficial) Regular Excelente Excelente
Custo Gratuito Nível gratuito Pago Pago
Melhor Para Auditorias rápidas, portas de CI Diagnósticos profundos de rede Monitoramento de tendências de longo prazo Colaboração em equipe e alertas

Resumo e Perspectivas

Orçamentos de performance frontend não são uma tarefa única—são uma prática de engenharia contínua. Tendências-chave para 2026:

  • INP substitui FID como métrica oficial do Core Web Vitals—responsividade de interação é o novo foco
  • Orçamentos de performance no CI — de convenções em documentação para aplicação em código, failOnBudgetExceeded é a linha de base
  • Decisões baseadas em RUM — dados de laboratório são apenas o ponto de partida; dados de usuários reais revelam a verdade
  • Descarregamento para Web Workers — soluções como Partytown impedem que scripts de terceiros arrastem a thread principal

Construa um sistema de orçamento de performance para que seu site tenha uma "verificação de segurança de performance" a cada release—em vez de descobrir problemas só depois que os usuários reclamam.


Ferramentas Online Recomendadas

  • Formatador JSON — Formatar dados JSON de relatórios do Lighthouse
  • Calculadora de Hash — Gerar hashes de arquivos de recursos para validação de cache
  • cURL para Código — Converter requisições API em código para integração rápida de monitoramento de performance

Experimente estas ferramentas executadas localmente no navegador — nenhum cadastro necessário →

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