Frontend Performance Budget: 6 Key Strategies for Core Web Vitals Optimization

性能优化

Frontend Performance Pain Points: Why Your Site Keeps Getting Slower

In 2026, frontend project complexity continues to climb. LCP overshoots, CLS jank, INP delays, bundle bloat, and invisible performance regression—these problems are eating away at your user experience and search rankings.

Pain Point Typical Symptom Business Impact
LCP overshoot First paint > 3s Bounce rate up 53%
CLS jank Layout shift > 0.25 Misclicks, trust erosion
INP delay Interaction response > 500ms User churn up 40%
Bundle bloat JS > 500KB (gzip) First load time doubles
Invisible regression Scores silently degrade after deploy Issues accumulate for months

Core Concepts Quick Reference

Concept Description Target
Performance Budget Quantified thresholds for performance metrics Team-defined
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 Google automated performance audit tool ≥ 90 score
RUM (Real User Monitoring) Real user performance data collection P75 passing
Bundle Analysis Bundle composition and size tracking JS ≤ 200KB
Code Splitting Split loading units by route/feature First screen ≤ 100KB

Five Challenges: Why Performance Budgets Fail to Stick

1. Unclear Metric Definitions — No team consensus on "good performance." Lab data or field data? P75 or P95?

2. Budget Enforcement Gaps — Budgets defined but not enforced. PRs merge regardless of violations.

3. Third-Party Script Impact — Analytics, ads, and chat widgets contribute 40-60% of JS execution time, yet you barely control them.

4. Mobile Performance Gap — Desktop Lighthouse 95, low-end Android LCP 6 seconds. The gap is enormous.

5. Regression Detection Blind Spots — No continuous monitoring means performance degrades like boiling a frog—you notice when it's already severe.


Strategy 1: Core Web Vitals Collection and Analysis

Use the web-vitals library to collect real user metrics and report to your analytics platform:

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

Server-side aggregation by P75, compared against budget thresholds:

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

Strategy 2: Bundle Size Budget and Lighthouse CI Integration

Enforce bundle size and Lighthouse score budgets in your CI/CD pipeline:

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

GitHub Actions integration:

# .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 }]
      }
    }
  }
}

Strategy 3: Image and Resource Loading Optimization

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

Strategy 4: Code Splitting and 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);
  });
}

Strategy 5: Third-Party Script Governance

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

Strategy 6: Performance Monitoring and Alerting System

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

Pitfall Guide

❌ Wrong Approach ✅ Right Approach
Only checking Lighthouse lab data Combine with RUM field data—lab + field dual validation
Budgets defined only in documentation Enforce in CI/CD with failOnBudgetExceeded: true
Lazy loading LCP images LCP elements must use loading="eager" + fetchpriority="high"
Loading all third-party scripts synchronously Prioritize by impact; defer low-priority with requestIdleCallback
Only optimizing for desktop Use low-end mobile as baseline—Moto G Power and similar mid-range devices

Troubleshooting

Symptom Possible Cause Solution
Lighthouse score fluctuates Network instability, inconsistent CPU throttling Run multiple times, take median, use numberOfRuns: 3
LCP consistently > 4s Unoptimized or un-preloaded images Use AVIF + fetchpriority="high" + preload
CLS > 0.25 Images/ads without declared dimensions Always set width/height or aspect-ratio
INP > 500ms Long tasks blocking main thread Split long tasks, use scheduler.yield() to yield
Bundle budget CI not working Incorrect budgetPath Verify relative path, check JSON syntax
web-vitals data loss sendBeacon cancelled on page unload Use fetch + keepalive: true as fallback
Code splitting makes first paint slower Over-splitting causes request waterfalls Merge small chunks, inline critical-path resources
Third-party script load failure CSP policy blocking Add corresponding domains to Content-Security-Policy
TTFB > 1.8s Slow SSR or CDN cache miss Enable edge caching, optimize SSR cache strategy
Font flash (FOIT/FOUT) Missing font-display declaration Use font-display: swap + preload

Advanced Optimization

  1. Move third-party scripts to Web Workers with Partytown — Offload analytics and other non-critical JS from the main thread. INP can drop 30-50%. Simply configure <script type="text/partytown">.

  2. Speculation Rules prerendering — Use <script type="speculationrules"> to prerender pages users are likely to visit. LCP can drop below 0.5s.

  3. RUM-driven A/B performance experiments — Apply different optimization strategies to different user segments, compare P75 metric differences, and let data drive decisions instead of guessing.

  4. Service Worker streaming cache — Use the Streams API for progressive cached responses, so first paint isn't blocked by full resource downloads.


Tool Comparison

Dimension Lighthouse WebPageTest SpeedCurve Calibre
Focus Automated audit Deep network analysis Continuous monitoring Team collaboration
Data Type Synthetic Synthetic Synthetic + RUM Synthetic + RUM
Device Emulation Limited (Throttling) Rich (real devices) Multi-region, multi-device Multi-region, multi-device
CI Integration Excellent (official CI) Fair Excellent Excellent
Cost Free Free tier Paid Paid
Best For Quick audits, CI gates Deep network diagnostics Long-term trend monitoring Team collaboration & alerting

Summary and Outlook

Frontend performance budgets aren't a one-time task—they're an ongoing engineering practice. Key trends for 2026:

  • INP replaces FID as an official Core Web Vitals metric—interaction responsiveness is the new focus
  • Performance budgets in CI — from documentation conventions to code enforcement, failOnBudgetExceeded is the baseline
  • RUM-driven decisions — lab data is just the starting point; real user data reveals the truth
  • Web Worker offloading — solutions like Partytown keep third-party scripts from dragging down the main thread

Build a performance budget system so your site gets a "performance security check" with every release—instead of discovering problems only after users complain.


  • JSON Formatter — Format Lighthouse report JSON data
  • Hash Calculator — Generate resource file hashes for cache validation
  • cURL to Code — Convert API requests to code for quick performance monitoring integration

Try these browser-local tools — no sign-up required →

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