[TERMINAL · SKILLS]
> mounting /skills...
> indexing skill manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Use Cases/Detect and Fix Performance Regressions Before They Hit Production

Detect and Fix Performance Regressions Before They Hit Production

An e-commerce team builds an automated performance regression detection system — running Lighthouse and custom load tests in CI for every PR, comparing against baselines, blocking merges that degrade Core Web Vitals, and alerting when API latency exceeds budgets — catching performance problems in code review instead of production incidents.

Development#testing#vite#typescript#mocking#coverage
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 4 skills

Avg quality 94/100·All SAFE
>

vitest

v1.1.0

Vitest is a test runner for JavaScript and TypeScript built on Vite, with a Jest-compatible API, built-in mocking, snapshots and code coverage. Use when a user asks to set up or run unit tests in a Vite or Node project, write tests with describe/it/expect, mock modules with vi.mock, add coverage thresholds, configure vitest.config.ts or test projects, migrate from Jest, or upgrade to Vitest 5.

100/100 quality
1.50× impact
SAFE· stale score
View skill
>

docker-helper

v1.1.0

Build, debug, and optimize Docker configurations. Use when a user asks to create a Dockerfile, fix Docker build errors, optimize image size, write docker-compose files, debug container issues, set up multi-stage builds, or troubleshoot networking between containers. Covers Dockerfile best practices and compose orchestration.

100/100 quality
1.44× impact
SAFE· stale score
View skill
>

otel-js

v1.1.0

OpenTelemetry for Node.js adds distributed tracing and metrics to a service and exports them over OTLP to any backend. Use when a user asks to add distributed tracing, collect metrics, instrument HTTP requests, trace database queries, or set up observability for microservices. Covers the current SDK (resourceFromAttributes, ESM loader hook, OTLP endpoint rules).

77/100 quality
10.00× impact
SAFE· stale score
View skill
>

checkly

v1.1.0

Checkly is a synthetic monitoring platform that runs API checks and Playwright browser checks from locations worldwide and lets you define them as code with the Checkly CLI. Use when a developer asks to set up monitoring as code, write API or browser checks, configure Slack or email alerts, run checks in CI after a deploy, or turn Playwright tests into production monitors.

100/100 quality
2.47× impact
SAFE· stale score
View skill
$

Jess is a frontend lead at an e-commerce platform doing $50M/year. Last quarter, a seemingly innocent "refactor" of the product listing page added 800ms to First Contentful Paint. Nobody noticed for 3 weeks. By the time they caught it, conversion rate had dropped 4% — $170K in lost revenue. Jess's rule going forward: no PR merges without performance proof.

The Problem: Performance Dies by a Thousand Cuts

Performance regressions rarely come from a single catastrophic change. They accumulate: a new analytics script here (+50ms), an unoptimized image component there (+100ms), an N+1 query in an API route (+200ms). Each one passes code review because "it's just 50ms." By the time anyone notices, the page load time has doubled.

The fix isn't monitoring in production — that's too late. The fix is catching regressions in CI, before they merge.

Step 1: Performance Budget in CI

Every PR runs Lighthouse against a preview deployment. If any Core Web Vital exceeds the budget, the PR is blocked:

typescript
// scripts/perf-audit.ts — Run against preview deployment
import lighthouse from "lighthouse";
import * as chromeLauncher from "chrome-launcher";

interface PerfBudget {
  metric: string;
  budget: number;
  weight: number;
}

const BUDGETS: PerfBudget[] = [
  { metric: "first-contentful-paint", budget: 1200, weight: 1.0 },    // ms
  { metric: "largest-contentful-paint", budget: 2500, weight: 1.5 },   // ms — most important
  { metric: "cumulative-layout-shift", budget: 0.1, weight: 1.0 },
  { metric: "total-blocking-time", budget: 200, weight: 1.2 },        // ms
  { metric: "speed-index", budget: 2000, weight: 0.8 },               // ms
];

const PAGES = [
  { name: "Homepage", path: "/" },
  { name: "Product Listing", path: "/products" },
  { name: "Product Detail", path: "/products/sample-product" },
  { name: "Cart", path: "/cart" },
  { name: "Checkout", path: "/checkout" },
];

async function runPerfAudit(baseUrl: string) {
  const chrome = await chromeLauncher.launch({ chromeFlags: ["--headless", "--no-sandbox"] });
  const results: PageResult[] = [];

  for (const page of PAGES) {
    // Run 3 times, take median (reduces noise)
    const runs: any[] = [];
    for (let i = 0; i < 3; i++) {
      const result = await lighthouse(`${baseUrl}${page.path}`, {
        port: chrome.port,
        onlyCategories: ["performance"],
        formFactor: "mobile",              // Test mobile — worst case
        throttling: { cpuSlowdownMultiplier: 4, downloadThroughputKbps: 1600, uploadThroughputKbps: 750 },
      });
      runs.push(result.lhr);
    }

    // Median results
    const medianRun = runs.sort((a, b) =>
      a.categories.performance.score - b.categories.performance.score
    )[1];

    const violations: string[] = [];
    for (const budget of BUDGETS) {
      const actual = medianRun.audits[budget.metric]?.numericValue || 0;
      if (actual > budget.budget) {
        violations.push(
          `❌ ${page.name}: ${budget.metric} = ${Math.round(actual)}ms (budget: ${budget.budget}ms, +${Math.round(actual - budget.budget)}ms over)`
        );
      }
    }

    results.push({
      page: page.name,
      score: Math.round(medianRun.categories.performance.score * 100),
      violations,
      metrics: Object.fromEntries(
        BUDGETS.map(b => [b.metric, Math.round(medianRun.audits[b.metric]?.numericValue || 0)])
      ),
    });
  }

  await chrome.kill();
  return results;
}

// Generate PR comment with results
function formatPRComment(results: PageResult[], baselineResults?: PageResult[]): string {
  let comment = "## ⚡ Performance Audit Results\n\n";
  let hasViolations = false;

  for (const result of results) {
    const baseline = baselineResults?.find(b => b.page === result.page);
    const scoreDiff = baseline ? result.score - baseline.score : 0;
    const scoreEmoji = scoreDiff > 0 ? "🟢" : scoreDiff < -5 ? "🔴" : "🟡";

    comment += `### ${result.page} — ${scoreEmoji} ${result.score}/100`;
    if (baseline) comment += ` (${scoreDiff > 0 ? "+" : ""}${scoreDiff} vs main)`;
    comment += "\n\n";

    if (result.violations.length > 0) {
      hasViolations = true;
      comment += result.violations.join("\n") + "\n\n";
    }

    // Metrics table
    comment += "| Metric | Value | Budget | Status |\n|--------|-------|--------|--------|\n";
    for (const budget of BUDGETS) {
      const actual = result.metrics[budget.metric];
      const status = actual <= budget.budget ? "✅" : "❌";
      comment += `| ${budget.metric} | ${actual}ms | ${budget.budget}ms | ${status} |\n`;
    }
    comment += "\n";
  }

  if (hasViolations) {
    comment += "---\n⛔ **This PR exceeds performance budgets.** Please fix the violations above before merging.\n";
  }

  return comment;
}

Step 2: API Latency Regression Testing

Frontend performance depends on API speed. Every PR also runs API latency tests against the preview backend:

typescript
// tests/api-perf.test.ts — Run with Vitest
import { describe, it, expect } from "vitest";

const API_BASE = process.env.PREVIEW_API_URL!;
const LATENCY_BUDGETS: Record<string, number> = {
  "GET /api/products": 200,                // ms
  "GET /api/products/:id": 100,
  "GET /api/cart": 150,
  "POST /api/cart/items": 300,
  "GET /api/search?q=shoes": 500,
};

describe("API Latency Budgets", () => {
  for (const [endpoint, budget] of Object.entries(LATENCY_BUDGETS)) {
    it(`${endpoint} responds within ${budget}ms`, async () => {
      const [method, path] = endpoint.split(" ");
      const url = `${API_BASE}${path.replace(":id", "sample-id")}`;

      // Warm up
      await fetch(url, { method });

      // Measure 10 requests, take P95
      const latencies: number[] = [];
      for (let i = 0; i < 10; i++) {
        const start = performance.now();
        await fetch(url, {
          method,
          body: method === "POST" ? JSON.stringify({ productId: "test", quantity: 1 }) : undefined,
          headers: method === "POST" ? { "Content-Type": "application/json" } : {},
        });
        latencies.push(performance.now() - start);
      }

      latencies.sort((a, b) => a - b);
      const p95 = latencies[Math.floor(latencies.length * 0.95)];

      expect(p95, `${endpoint} P95 latency ${Math.round(p95)}ms exceeds budget ${budget}ms`).toBeLessThanOrEqual(budget);
    });
  }
});

Step 3: Bundle Size Tracking

The final piece: tracking JavaScript bundle size changes per PR, because every KB costs mobile users.

yaml
# .github/workflows/perf-check.yml
- name: Build and measure bundle
  run: |
    npm run build
    node scripts/measure-bundle.js > /tmp/bundle-current.json

- name: Compare with main
  run: |
    git stash
    git checkout main
    npm ci && npm run build
    node scripts/measure-bundle.js > /tmp/bundle-baseline.json
    git checkout -

    node scripts/compare-bundles.js /tmp/bundle-baseline.json /tmp/bundle-current.json
typescript
// scripts/compare-bundles.ts
const BUDGET_KB = 250;                     // Total JS budget

const baseline = JSON.parse(readFileSync(process.argv[2], "utf-8"));
const current = JSON.parse(readFileSync(process.argv[3], "utf-8"));

const diff = current.totalSizeKB - baseline.totalSizeKB;
if (diff > 10) {
  console.error(`❌ Bundle increased by ${diff.toFixed(1)}KB. Investigate before merging.`);
  process.exit(1);
}
if (current.totalSizeKB > BUDGET_KB) {
  console.error(`❌ Total bundle ${current.totalSizeKB.toFixed(1)}KB exceeds ${BUDGET_KB}KB budget.`);
  process.exit(1);
}
console.log(`✅ Bundle: ${current.totalSizeKB.toFixed(1)}KB (${diff > 0 ? "+" : ""}${diff.toFixed(1)}KB vs main)`);

Results

After 6 months of performance-gated CI:

  • Regressions caught: 34 PRs blocked for performance violations; all fixed before merge
  • LCP improvement: Product listing page LCP improved from 3.2s to 1.8s (45% faster)
  • Revenue impact: Conversion rate recovered +4% within 2 weeks of fixing the original regression
  • Bundle size: Reduced from 380KB to 210KB through budget enforcement; new bloat caught immediately
  • API latency: P95 API latency held steady at 180ms; 3 N+1 queries caught in PR review
  • Culture shift: Developers now check performance before submitting PRs; "it's just 50ms" is no longer acceptable
  • Cost: Zero additional infrastructure — runs on existing CI; Lighthouse is free, Vitest is free
  • False positives: <5% of blocked PRs were false positives (CI noise); 3-run median reduces flaky results

Related use cases