Blog Post
5 min read

Performance Budgets for Frontend Teams

Performance budgets fail when they are aspirational. A budget of "LCP under 2 seconds" means nothing if you do not measure, enforce, and respond.

Published on March 26, 2026

Performance budgets fail when they are aspirational. A budget of "LCP under 2 seconds" means nothing if you do not measure, enforce, and respond.

A real budget has teeth: it gates PRs, alerts on regressions, and provides a playbook to stay within bounds.

Pick Metrics That Move UX

You cannot optimize what you do not measure. Pick three to five metrics that genuinely impact user experience.

Largest Contentful Paint (LCP)

LCP measures when the largest piece of content becomes visible. This could be a hero image, a headline, or a text block.

Why it matters: Users perceive pages based on what they see first. If the hero takes 5 seconds, the page feels slow, no matter what happens after.

Target: < 2.5 seconds (Web Vitals "Good" threshold).

Measure: Real User Monitoring (RUM) and synthetic tests (Lighthouse, WebPageTest).

Interaction to Next Paint (INP)

INP measures the time from user input (click, tap, keystroke) to the next visual feedback.

Why it matters: A fast-loading page that feels sluggy on interaction is worse than a slightly slower page that responds instantly.

Target: < 200 milliseconds (Web Vitals "Good" threshold).

Measure: Real user interactions. Synthetic tests are limited because they cannot predict user interaction timing.

Cumulative Layout Shift (CLS)

CLS measures how much the layout moves around while the user is viewing it.

Why it matters: Ads loading, late images, or fonts blocking layout causes the text to shift under the user's finger. They click on the wrong button. It feels janky.

Target: < 0.1 (Web Vitals "Good" threshold).

Measure: RUM. CLS only happens with real user interaction patterns.

Hydration Time

For server-rendered or streaming apps, track how long it takes from HTML arrival to interactive.

Why it matters: A page that renders fast but hydrates slowly still feels sluggy.

Target: < 1 second for the critical path (buttons, forms).

Measure: Custom instrumentation. React DevTools Profiler or Web Vitals library.

Error Budgets

Track failed interactions, JavaScript errors, and silent failures (requests that error without user feedback).

Why it matters: A performant page that breaks silently is worse than a slow page that works.

Example SLA: < 0.1% of sessions have a JavaScript error that breaks the page.

Gates That Developers Respect

Developers ignore budgets they do not see during development. Add gates to the PR workflow.

PR Checks with Synthetic Budgets

Run Lighthouse, PageSpeed Insights, or custom timing scripts on every PR:

# In CI (e.g., GitHub Actions)
npx lighthouse https://staging.example.com/pr-${{ github.event.number }} \
  --output=json \
  --output=html \
  --chrome-flags="--headless"

Report the results inline in the PR:

❌ Performance Budget Violations
- LCP: 3.2s (budget: 2.5s) — hero image not optimized
- Bundle size: 450KB (budget: 400KB) — new dependency added
- TTI: 5.1s (budget: 4.5s) — heavy script on main thread

Suggestions: Optimize hero image with next/image. Audit new dependencies for size.

Canary RUM with Alerts

Deploy to a staging or canary environment. Collect real user metrics. Alert if key metrics degrade:

// Send to analytics backend
if (new PerformanceObserver.supportedEntryTypes?.includes('largest-contentful-paint')) {
  new PerformanceObserver((list) => {
    const entries = list.getEntries();
    const lastEntry = entries[entries.length - 1];
    analytics.send({
      metric: 'lcp',
      value: lastEntry.renderTime || lastEntry.loadTime,
      url: window.location.pathname
    });
  }).observe({ entryTypes: ['largest-contentful-paint'] });
}

Alert on > 10% regression:

# Alert config
alert:
  name: LCP Regression
  condition: LCP > baseline * 1.1
  action: Notify #frontend Slack, block deployment

Block on Regressions > 5–10%

A rule: PRs that increase bundle size by more than 10% or degrade LCP by more than 10% require a waiver.

The waiver forces a conversation: "Is this feature worth the cost?"

Often, the answer is "no, let's optimize." Sometimes, it's "yes, and here's our plan to offset it elsewhere."

Without the waiver, the rule is arbitrary. With it, it's a tradeoff.

Playbooks: How to Fix Common Failures

A budget with no playbook is just a wall. Pair each metric with a how-to guide.

LCP Playbook

Problem: Hero image takes 5 seconds to render.

Root causes:

  1. Image not preloaded.
  2. Image size too large.
  3. Image served from slow CDN.
  4. Image has render-blocking CSS or script above it.

Solutions:

  1. Add <link rel="preload" as="image" href="..." /> in <head>.
  2. Compress and resize: use WebP, AVIF, and responsive srcsets.
  3. Use fetchPriority="high" on <img> (fetch before other images).
  4. Serve from a fast CDN (Cloudflare, Fastly, Akamai). Measure TTFB.
  5. Defer non-critical scripts and CSS until after hero render.
<!-- Good -->
<head>
  <link rel="preload" as="image" href="hero.webp" fetchpriority="high" />
</head>
<body>
  <img
    src="hero.webp"
    alt="Hero"
    fetchpriority="high"
    width="1200"
    height="600"
  />
</body>

INP Playbook

Problem: Button clicks take 500ms to show feedback.

Root causes:

  1. Heavy event listener on the main thread.
  2. Long script execution during interaction.
  3. Layout thrashing (read, write, read, write).
  4. Slow state update batching.

Solutions:

  1. Defer non-critical work: use startTransition in React to de-prioritize expensive updates.
    function handleClick() {
      startTransition(() => {
        // Heavy update: filtering, sorting
        setFilteredItems(expensiveCompute(items));
      });
      // Quick update: visual feedback
      setLoading(false);
    }
    
  2. Batch DOM reads and writes: Read all, then write all.
  3. Use debounce or throttle for frequent events (scroll, resize).
  4. Offload heavy work to a Web Worker or schedule it with requestIdleCallback.

CLS Playbook

Problem: Text shifts when an image loads.

Root causes:

  1. Image has no width/height, so browser cannot reserve space.
  2. Font loads late, causing fallback font to be taller.
  3. Ad or lazy-loaded content pushes layout.

Solutions:

  1. Always specify width and height on images. If responsive, use aspect-ratio:
    <img
      src="..."
      alt="..."
      width="1200"
      height="600"
      style="aspect-ratio: 2 / 1"
    />
    
  2. Use font-display: swap to show fallback immediately:
    @font-face {
      font-family: "MyFont";
      src: url("myfont.woff2") format("woff2");
      font-display: swap; /* Show fallback until loaded */
    }
    
  3. Reserve space for ads or lazy content with a container query or fixed height.

The Mindset

Budgeting is successful when it guides trade-offs, not when it shouts red numbers.

When a feature adds 50KB to the bundle, the team sees it during PR review and decides: "Is this worth it?" If yes, how do we offset it elsewhere? If no, how do we refactor?

That conversation—that is the value of the budget.