Performanceintermediate

Performance Testing & Monitoring — The Complete Guide

Master Lighthouse automation, synthetic monitoring, Real User Monitoring, performance budgets, and CI/CD performance checks for web applications.

14 min read·Published Apr 12, 2026
performancetestingmonitoringlighthouse

Why Performance Testing Matters

Performance degrades silently. A new feature adds 50KB of JavaScript. A library update doubles parse time. An unoptimized image slips through code review. Without automated testing and monitoring, you only find out when users complain or rankings drop.

Performance degradation without monitoring:
Week 1: Bundle 200KB,  LCP 1.5s  (baseline)
Week 2: Bundle 220KB,  LCP 1.7s  (+library)
Week 3: Bundle 280KB,  LCP 2.1s  (+unoptimized images)
Week 4: Bundle 350KB,  LCP 2.8s  (+analytics scripts)
Week 5: Bundle 350KB,  LCP 3.2s  (+font loading regression)
                                   ^ Users start leaving
                                   ^ Google ranking drops

With automated testing:
Week 2: CI FAILS — "Bundle budget exceeded by 20KB"
Fix before merge.

Lighthouse Automation

Lighthouse measures lab performance — simulated conditions that are consistent and reproducible. Perfect for catching regressions in CI.

Running Lighthouse Programmatically

// scripts/lighthouse-audit.js
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

async function runAudit(url) {
  const chrome = await chromeLauncher.launch({
    chromeFlags: ['--headless', '--no-sandbox'],
  });

  const result = await lighthouse(url, {
    port: chrome.port,
    onlyCategories: ['performance'],
    formFactor: 'mobile',
    throttling: {
      cpuSlowdownMultiplier: 4,
      downloadThroughputKbps: 1474.56, // Simulated 3G
      uploadThroughputKbps: 675,
      rttMs: 150,
    },
  });

  await chrome.kill();
  return result.lhr;
}

async function main() {
  const urls = [
    'http://localhost:3000',
    'http://localhost:3000/about',
    'http://localhost:3000/blog',
  ];

  for (const url of urls) {
    const lhr = await runAudit(url);
    const score = lhr.categories.performance.score * 100;
    const lcp = lhr.audits['largest-contentful-paint'].numericValue;
    const cls = lhr.audits['cumulative-layout-shift'].numericValue;
    const tbt = lhr.audits['total-blocking-time'].numericValue;

    console.log(`\n${url}`);
    console.log(`  Score: ${score}/100`);
    console.log(`  LCP: ${(lcp / 1000).toFixed(2)}s`);
    console.log(`  CLS: ${cls.toFixed(3)}`);
    console.log(`  TBT: ${tbt.toFixed(0)}ms`);

    if (score < 90) {
      console.error(`  FAIL: Score below 90`);
      process.exitCode = 1;
    }
  }
}

main();

Lighthouse CI (LHCI)

# Install Lighthouse CI
npm install -g @lhci/cli

# Initialize configuration
lhci wizard
// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      // Run against a local dev server
      startServerCommand: 'npm run start',
      startServerReadyPattern: 'ready on',
      url: [
        'http://localhost:3000/',
        'http://localhost:3000/about',
        'http://localhost:3000/blog/first-post',
      ],
      numberOfRuns: 3, // Run 3 times and take median
      settings: {
        formFactor: 'mobile',
        throttling: {
          cpuSlowdownMultiplier: 4,
        },
      },
    },
    assert: {
      assertions: {
        'categories:performance': ['error', { minScore: 0.9 }],
        'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
        'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
        'total-blocking-time': ['warn', { maxNumericValue: 300 }],
        'first-contentful-paint': ['warn', { maxNumericValue: 1800 }],
        // Specific audits
        'uses-responsive-images': 'warn',
        'render-blocking-resources': 'warn',
        'unminified-javascript': 'error',
        'uses-text-compression': 'error',
      },
    },
    upload: {
      target: 'temporary-public-storage', // Free, temporary results storage
      // Or upload to your own LHCI server:
      // target: 'lhci',
      // serverBaseUrl: 'https://lhci.example.com',
    },
  },
};
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]

jobs:
  lighthouse:
    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: Run Lighthouse CI
        run: |
          npm install -g @lhci/cli
          lhci autorun
        env:
          LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}

Lighthouse in CI — Comparing Before/After

// scripts/lighthouse-compare.js
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

async function auditURL(url) {
  const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });
  const { lhr } = await lighthouse(url, {
    port: chrome.port,
    onlyCategories: ['performance'],
  });
  await chrome.kill();

  return {
    score: lhr.categories.performance.score * 100,
    lcp: lhr.audits['largest-contentful-paint'].numericValue,
    cls: lhr.audits['cumulative-layout-shift'].numericValue,
    tbt: lhr.audits['total-blocking-time'].numericValue,
  };
}

async function compare(baseUrl, prUrl) {
  const base = await auditURL(baseUrl);
  const pr = await auditURL(prUrl);

  const diff = {
    score: pr.score - base.score,
    lcp: pr.lcp - base.lcp,
    cls: pr.cls - base.cls,
    tbt: pr.tbt - base.tbt,
  };

  console.log('Performance Comparison:');
  console.log('+----------+----------+----------+----------+');
  console.log('| Metric   | Base     | PR       | Diff     |');
  console.log('+----------+----------+----------+----------+');
  console.log(`| Score    | ${base.score.toFixed(0).padStart(6)}   | ${pr.score.toFixed(0).padStart(6)}   | ${diff.score > 0 ? '+' : ''}${diff.score.toFixed(0).padStart(6)}   |`);
  console.log(`| LCP (ms) | ${base.lcp.toFixed(0).padStart(6)}   | ${pr.lcp.toFixed(0).padStart(6)}   | ${diff.lcp > 0 ? '+' : ''}${diff.lcp.toFixed(0).padStart(6)}   |`);
  console.log(`| CLS      | ${base.cls.toFixed(3).padStart(6)}   | ${pr.cls.toFixed(3).padStart(6)}   | ${diff.cls > 0 ? '+' : ''}${diff.cls.toFixed(3).padStart(6)}   |`);
  console.log(`| TBT (ms) | ${base.tbt.toFixed(0).padStart(6)}   | ${pr.tbt.toFixed(0).padStart(6)}   | ${diff.tbt > 0 ? '+' : ''}${diff.tbt.toFixed(0).padStart(6)}   |`);
  console.log('+----------+----------+----------+----------+');

  // Fail if performance regressed significantly
  if (diff.lcp > 500) {
    console.error('FAIL: LCP increased by more than 500ms');
    process.exit(1);
  }
  if (diff.cls > 0.05) {
    console.error('FAIL: CLS increased by more than 0.05');
    process.exit(1);
  }
}

compare(process.argv[2], process.argv[3]);

Synthetic Monitoring

Synthetic monitoring runs automated tests from distributed locations on a schedule. It catches performance regressions before users report them.

WebPageTest Integration

// scripts/webpagetest-monitor.js
const WebPageTest = require('webpagetest');

const wpt = new WebPageTest('https://www.webpagetest.org', process.env.WPT_API_KEY);

function runTest(url) {
  return new Promise((resolve, reject) => {
    wpt.runTest(
      url,
      {
        location: 'Dulles:Chrome',
        connectivity: '4G',
        runs: 3,
        firstViewOnly: false,
        pollResults: 10, // Poll every 10 seconds
      },
      (err, data) => {
        if (err) return reject(err);
        resolve(data);
      }
    );
  });
}

async function monitor() {
  const urls = [
    'https://example.com/',
    'https://example.com/products',
  ];

  for (const url of urls) {
    const result = await runTest(url);
    const median = result.data.median.firstView;

    console.log(`\n${url}`);
    console.log(`  TTFB: ${median.TTFB}ms`);
    console.log(`  LCP: ${median.chromeUserTiming?.LargestContentfulPaint}ms`);
    console.log(`  CLS: ${median.chromeUserTiming?.CumulativeLayoutShift}`);
    console.log(`  Total Bytes: ${(median.bytesIn / 1024).toFixed(0)}KB`);
    console.log(`  Requests: ${median.requests}`);
    console.log(`  Report: ${result.data.summary}`);
  }
}

monitor();

Scheduled Monitoring with Cron

// scripts/scheduled-monitor.js
// Run this on a schedule (cron job, GitHub Actions schedule, etc.)

async function checkPerformance() {
  const results = await runLighthouseAudit('https://example.com');

  // Store results in a database or file for trend analysis
  const record = {
    timestamp: new Date().toISOString(),
    url: 'https://example.com',
    score: results.score,
    lcp: results.lcp,
    cls: results.cls,
    tbt: results.tbt,
  };

  // Append to results log
  const fs = require('fs');
  const logPath = './performance-log.json';
  const log = JSON.parse(fs.readFileSync(logPath, 'utf-8') || '[]');
  log.push(record);
  fs.writeFileSync(logPath, JSON.stringify(log, null, 2));

  // Alert if metrics exceed thresholds
  if (results.score < 85) {
    await sendAlert({
      channel: '#performance',
      text: `Performance alert: ${record.url} score dropped to ${results.score}`,
      details: record,
    });
  }
}

// Example cron: every 6 hours
// 0 */6 * * * node scripts/scheduled-monitor.js
# .github/workflows/performance-monitor.yml
name: Performance Monitor
on:
  schedule:
    - cron: '0 */6 * * *' # Every 6 hours

jobs:
  monitor:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: node scripts/scheduled-monitor.js
        env:
          SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}

Real User Monitoring (RUM)

Synthetic tests use simulated conditions. RUM captures what real users actually experience, across real devices, connections, and locations.

Implementing RUM

// lib/rum.js — Real User Monitoring collector
import { onCLS, onINP, onLCP, onFCP, onTTFB } from 'web-vitals';

const metrics = [];

function collectMetric(metric) {
  metrics.push({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    // Context
    page: window.location.pathname,
    referrer: document.referrer,
    // Device info
    connection: navigator.connection?.effectiveType || 'unknown',
    deviceMemory: navigator.deviceMemory || 'unknown',
    hardwareConcurrency: navigator.hardwareConcurrency || 'unknown',
    // Viewport
    viewportWidth: window.innerWidth,
    viewportHeight: window.innerHeight,
    // Timestamp
    timestamp: Date.now(),
  });
}

// Register all metric collectors
onCLS(collectMetric);
onINP(collectMetric);
onLCP(collectMetric);
onFCP(collectMetric);
onTTFB(collectMetric);

// Send metrics when user leaves the page
function flushMetrics() {
  if (metrics.length === 0) return;

  const body = JSON.stringify(metrics);
  metrics.length = 0;

  // sendBeacon is reliable even during page unload
  if (navigator.sendBeacon) {
    navigator.sendBeacon('/api/rum', body);
  } else {
    fetch('/api/rum', { body, method: 'POST', keepalive: true });
  }
}

// Flush on page hide (covers tab close, navigation, background)
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'hidden') {
    flushMetrics();
  }
});

RUM Analysis Dashboard

// api/rum-dashboard.js — Analyze collected RUM data
async function getRUMDashboard(timeRange = '24h') {
  const metrics = await db.query(
    'SELECT * FROM rum_metrics WHERE timestamp > ?',
    [getTimestamp(timeRange)]
  );

  // Group by metric name
  const grouped = {};
  for (const m of metrics) {
    if (!grouped[m.name]) grouped[m.name] = [];
    grouped[m.name].push(m.value);
  }

  // Calculate percentiles
  function percentile(arr, p) {
    const sorted = [...arr].sort((a, b) => a - b);
    const idx = Math.ceil(sorted.length * (p / 100)) - 1;
    return sorted[idx];
  }

  const dashboard = {};
  for (const [name, values] of Object.entries(grouped)) {
    dashboard[name] = {
      count: values.length,
      p50: percentile(values, 50),
      p75: percentile(values, 75),
      p90: percentile(values, 90),
      p95: percentile(values, 95),
    };
  }

  return dashboard;
}

// Example output:
// {
//   LCP: { count: 15420, p50: 1800, p75: 2400, p90: 3200, p95: 4100 },
//   INP: { count: 8932,  p50: 80,   p75: 150,  p90: 280,  p95: 450 },
//   CLS: { count: 15420, p50: 0.02, p75: 0.08, p90: 0.15, p95: 0.22 },
// }

Segment RUM by Dimension

// Break down performance by device, connection, page
function segmentMetrics(metrics) {
  const segments = {
    byConnection: {},
    byDevice: {},
    byPage: {},
  };

  for (const m of metrics) {
    // By connection type
    const conn = m.connection || 'unknown';
    if (!segments.byConnection[conn]) segments.byConnection[conn] = [];
    segments.byConnection[conn].push(m.value);

    // By device class (based on memory/cores)
    const deviceClass = m.deviceMemory >= 4 ? 'high-end' :
                        m.deviceMemory >= 2 ? 'mid-range' : 'low-end';
    if (!segments.byDevice[deviceClass]) segments.byDevice[deviceClass] = [];
    segments.byDevice[deviceClass].push(m.value);

    // By page
    const page = m.page;
    if (!segments.byPage[page]) segments.byPage[page] = [];
    segments.byPage[page].push(m.value);
  }

  return segments;
}

// Now you can see:
// "LCP on 3G connections is 4.2s (poor) vs 1.8s on 4G (good)"
// "Low-end devices have 3x worse INP than high-end"
// "/products page has 2x worse CLS than homepage"

Lab vs Field Comparison Table

+------------------+---------------------------+---------------------------+
|                  | Lab (Synthetic)           | Field (RUM)               |
+------------------+---------------------------+---------------------------+
| Data source      | Lighthouse, WebPageTest   | Real user browsers        |
| Consistency      | Same conditions every run | Varies wildly per user    |
| INP available    | No (uses TBT as proxy)    | Yes                       |
| Debugging        | Full trace, screenshots   | Limited (metric values)   |
| When to use      | CI/CD, regression testing | Production monitoring     |
| Volume needed    | 1-3 runs                  | 1000+ page views          |
| Cost             | Free (self-hosted)        | Storage + processing      |
| Captures edge    | No                        | Yes (slow devices, bad    |
| cases            |                           | networks)                 |
+------------------+---------------------------+---------------------------+

Best practice: Use BOTH.
- Lab for catching regressions in CI before deploy
- Field for understanding real user experience in production

Performance Budgets

A performance budget is a set of limits on metrics that affect user experience. If a metric exceeds its budget, the build fails.

Defining Budgets

// performance-budget.json
{
  "budgets": [
    {
      "path": "/",
      "resourceSizes": [
        { "resourceType": "script", "budget": 150 },
        { "resourceType": "stylesheet", "budget": 50 },
        { "resourceType": "image", "budget": 300 },
        { "resourceType": "font", "budget": 100 },
        { "resourceType": "total", "budget": 500 }
      ],
      "resourceCounts": [
        { "resourceType": "script", "budget": 10 },
        { "resourceType": "third-party", "budget": 5 }
      ],
      "timings": [
        { "metric": "largest-contentful-paint", "budget": 2500 },
        { "metric": "cumulative-layout-shift", "budget": 0.1 },
        { "metric": "total-blocking-time", "budget": 300 },
        { "metric": "first-contentful-paint", "budget": 1800 }
      ]
    }
  ]
}

Enforcing Budgets in Lighthouse CI

// lighthouserc.js
module.exports = {
  ci: {
    collect: {
      url: ['http://localhost:3000/'],
      numberOfRuns: 3,
    },
    assert: {
      budgetsFile: './performance-budget.json',
      assertions: {
        // Fail the build if any budget is exceeded
        'resource-summary:script:size': ['error', { maxNumericValue: 153600 }],
        'resource-summary:total:size': ['error', { maxNumericValue: 512000 }],
        'categories:performance': ['error', { minScore: 0.9 }],
      },
    },
  },
};

Bundle Size Budget in webpack

// next.config.js
module.exports = {
  webpack(config) {
    // Warn when any asset exceeds 250KB
    config.performance = {
      maxAssetSize: 256000,         // 250KB per file
      maxEntrypointSize: 512000,    // 500KB per entry point
      hints: 'error',               // 'warning' or 'error'
    };
    return config;
  },
};

Budget Tracking Over Time

// scripts/track-budget.js
const fs = require('fs');

function trackBudgets() {
  const buildStats = JSON.parse(
    fs.readFileSync('.next/build-manifest.json', 'utf-8')
  );

  // Calculate total JS size
  const jsFiles = Object.values(buildStats.pages)
    .flat()
    .filter((f) => f.endsWith('.js'));

  let totalSize = 0;
  for (const file of new Set(jsFiles)) {
    const filePath = `.next/${file}`;
    if (fs.existsSync(filePath)) {
      totalSize += fs.statSync(filePath).size;
    }
  }

  const record = {
    timestamp: new Date().toISOString(),
    commit: process.env.GITHUB_SHA || 'local',
    totalJS: totalSize,
    budget: 512000,
    withinBudget: totalSize <= 512000,
  };

  console.log(`Total JS: ${(totalSize / 1024).toFixed(1)}KB`);
  console.log(`Budget: ${(record.budget / 1024).toFixed(1)}KB`);
  console.log(`Status: ${record.withinBudget ? 'PASS' : 'FAIL'}`);

  // Append to tracking log
  const logPath = './budget-history.json';
  const history = fs.existsSync(logPath)
    ? JSON.parse(fs.readFileSync(logPath, 'utf-8'))
    : [];
  history.push(record);
  fs.writeFileSync(logPath, JSON.stringify(history, null, 2));

  if (!record.withinBudget) process.exit(1);
}

trackBudgets();

CI/CD Performance Checks

GitHub Actions Workflow

# .github/workflows/performance.yml
name: Performance Checks
on:
  pull_request:
    branches: [main]

jobs:
  bundle-size:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      # Check bundle size
      - name: Check bundle budget
        run: node scripts/check-bundle-size.js

      # Compare with main branch
      - name: Bundle size comparison
        uses: andresz1/size-limit-action@v1
        with:
          github_token: ${{ secrets.GITHUB_TOKEN }}

  lighthouse:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: 'npm'

      - run: npm ci
      - run: npm run build

      - name: Start server
        run: npm start &
        env:
          PORT: 3000

      - name: Wait for server
        run: npx wait-on http://localhost:3000

      - name: Run Lighthouse
        run: |
          npm install -g @lhci/cli
          lhci autorun

      - name: Upload Lighthouse results
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: lighthouse-results
          path: .lighthouseci/

PR Comment with Performance Report

// scripts/pr-performance-comment.js
// Creates a GitHub PR comment with performance results

async function postComment(results) {
  const body = `## Performance Report

| Metric | Value | Budget | Status |
|--------|-------|--------|--------|
| Performance Score | ${results.score}/100 | >= 90 | ${results.score >= 90 ? 'Pass' : 'FAIL'} |
| LCP | ${(results.lcp / 1000).toFixed(2)}s | < 2.5s | ${results.lcp < 2500 ? 'Pass' : 'FAIL'} |
| CLS | ${results.cls.toFixed(3)} | < 0.1 | ${results.cls < 0.1 ? 'Pass' : 'FAIL'} |
| TBT | ${results.tbt.toFixed(0)}ms | < 300ms | ${results.tbt < 300 ? 'Pass' : 'FAIL'} |
| Bundle Size | ${(results.bundleSize / 1024).toFixed(0)}KB | < 500KB | ${results.bundleSize < 512000 ? 'Pass' : 'FAIL'} |

${results.score >= 90 ? 'All checks passed.' : 'Performance regression detected.'}`;

  // Post as PR comment via GitHub API
  const { Octokit } = require('@octokit/rest');
  const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });

  await octokit.issues.createComment({
    owner: process.env.GITHUB_REPOSITORY_OWNER,
    repo: process.env.GITHUB_REPOSITORY_NAME,
    issue_number: process.env.PR_NUMBER,
    body,
  });
}

Performance Monitoring Dashboard Architecture

+-------------------+     +-------------------+     +-------------------+
|  Browser (RUM)    |     | CI/CD (Synthetic) |     | Cron (Synthetic)  |
|                   |     |                   |     |                   |
| web-vitals lib    |     | Lighthouse CI     |     | WebPageTest API   |
| sendBeacon()      |     | GitHub Actions    |     | Scheduled runs    |
+--------+----------+     +--------+----------+     +--------+----------+
         |                          |                          |
         v                          v                          v
+---------------------------------------------------------------+
|                    Metrics Ingestion API                       |
|               (POST /api/metrics endpoint)                    |
+---------------------------------------------------------------+
         |
         v
+---------------------------------------------------------------+
|                    Time-Series Database                        |
|              (InfluxDB, TimescaleDB, or just JSON)            |
+---------------------------------------------------------------+
         |
         v
+---------------------------------------------------------------+
|                    Dashboard / Alerts                          |
|          (Grafana, custom dashboard, Slack alerts)             |
+---------------------------------------------------------------+
// Minimal monitoring API
// pages/api/metrics.js
export default async function handler(req, res) {
  if (req.method !== 'POST') return res.status(405).end();

  const metrics = JSON.parse(req.body);

  // Store metrics (adapt to your database)
  for (const metric of metrics) {
    await db.insert('performance_metrics', {
      name: metric.name,
      value: metric.value,
      rating: metric.rating,
      page: metric.page,
      connection: metric.connection,
      device_class: classifyDevice(metric),
      timestamp: new Date(metric.timestamp),
    });
  }

  // Check for alerts
  for (const metric of metrics) {
    if (metric.name === 'LCP' && metric.value > 4000) {
      await sendAlert(`LCP alert: ${metric.value}ms on ${metric.page}`);
    }
    if (metric.name === 'CLS' && metric.value > 0.25) {
      await sendAlert(`CLS alert: ${metric.value} on ${metric.page}`);
    }
  }

  res.status(200).end();
}

Performance Testing Checklist

+---+----------------------------------------------+-----------+
| # | Check                                        | Tool      |
+---+----------------------------------------------+-----------+
| 1 | Lighthouse score >= 90 on all pages          | LHCI      |
| 2 | LCP < 2.5s (lab) on all pages                | LHCI      |
| 3 | CLS < 0.1 (lab) on all pages                 | LHCI      |
| 4 | TBT < 300ms (lab) on all pages               | LHCI      |
| 5 | Total JS bundle < 500KB (gzipped)            | Custom    |
| 6 | No page JS > 200KB (gzipped)                 | Custom    |
| 7 | Images optimized (WebP/AVIF, correct sizes)  | LHCI      |
| 8 | No render-blocking resources                 | LHCI      |
| 9 | Text compression enabled (gzip/brotli)       | LHCI      |
| 10| Font display strategy set                    | LHCI      |
| 11| RUM p75 LCP < 2.5s                           | RUM       |
| 12| RUM p75 INP < 200ms                          | RUM       |
| 13| RUM p75 CLS < 0.1                            | RUM       |
| 14| No performance regression vs previous deploy | CI compare|
+---+----------------------------------------------+-----------+

Key Takeaways

  • Automate Lighthouse in CI to catch regressions before deploy
  • Run Lighthouse CI with 3+ runs and use median results for stability
  • Synthetic monitoring (scheduled tests) catches regressions between deploys
  • RUM captures real user experience across diverse devices and connections
  • Use both lab and field data — they serve different purposes
  • Performance budgets set hard limits on bundle size and metrics
  • Fail the build when budgets are exceeded — do not just warn
  • Track metrics over time to spot gradual degradation
  • Segment RUM data by device class, connection type, and page to find specific problems
  • Post performance reports as PR comments so reviewers see the impact
  • Schedule periodic monitoring even if your CI checks every PR

Found this helpful?

Support devsofus — help us keep creating free dev guides.

Related Articles