Skip to main content

Tutorial: Build a Performance Budget and Profiling Lab

Why this tutorial matters

Performance architecture is not "run Lighthouse at the end." It is a system of budgets, profiling, field telemetry, CI gates, and incident response. This tutorial builds a mid-level Performance Lab around a content-heavy lead page and an authenticated dashboard.

The goal is to practice diagnosing and preventing regressions, not only optimizing one page.

What you will build

CapabilityPurpose
Route budgetsDefine allowed JS, CSS, image, request, and CWV limits
Web Vitals captureMeasure LCP, INP, CLS in the field
Synthetic checksRun repeatable lab tests before release
Bundle analysisTrack route-level JavaScript cost
Profiling workflowDiagnose long tasks and render cost
CI gateBlock obvious regressions
Incident playbookRespond when production performance regresses

Core Web Vitals targets from web.dev are the baseline: LCP within 2.5s, INP within 200ms, and CLS no more than 0.1 at the 75th percentile. For critical funnels, set stricter internal budgets.

Milestone 1: Define route budgets

Create performance-budgets.json:

{
"/": {
"lcpMs": 1800,
"inpMs": 150,
"cls": 0.05,
"initialJsKb": 120,
"imageKb": 350,
"requests": 45
},
"/dashboard": {
"lcpMs": 2500,
"inpMs": 200,
"cls": 0.1,
"initialJsKb": 250,
"requests": 80
}
}

Rules:

  • budgets are route-specific
  • user-centric metrics and quantity metrics are both included
  • budgets have owners
  • exceptions require expiry date

Acceptance criteria:

  • every critical route has a budget
  • budget decisions are documented in an ADR
  • product/design know the tradeoffs

Milestone 2: Capture Web Vitals

Install field metric capture using web-vitals or your RUM provider.

Event shape:

type WebVitalEvent = {
name: "LCP" | "INP" | "CLS";
value: number;
rating: "good" | "needs-improvement" | "poor";
route: string;
release: string;
deviceClass?: string;
connectionType?: string;
};

Segment by:

  • route
  • release
  • device class
  • region
  • traffic source
  • experiment variant
  • logged-in vs anonymous

Acceptance criteria:

  • p75 metrics are visible per critical route
  • metrics can be compared before/after release
  • dashboard excludes obvious bot/test traffic where possible

Milestone 3: Add synthetic checks

Use Lighthouse, WebPageTest, Playwright tracing, or equivalent. Synthetic tests do not replace field data; they catch repeatable regressions before users see them.

Test profiles:

ProfilePurpose
mobile slow CPUcatches hydration/rendering cost
slow 4Gcatches network and asset weight
desktop fastcatches layout and route regressions
repeat viewvalidates cache behavior

Acceptance criteria:

  • synthetic test runs on main route and one authenticated-like route
  • results are stored with build id
  • regressions produce actionable diff: JS, image, request, main-thread, layout

Milestone 4: Bundle and request budget gate

Add a CI step that checks:

  • initial route JS
  • async chunk size
  • CSS size
  • image bytes
  • third-party domains
  • request count from static analysis or synthetic run

Gate rule:

fail if initial JS increases by >20KB on a critical route
warn if a new third-party domain appears
fail if route budget is exceeded without an exception file

Acceptance criteria:

  • a deliberate budget failure blocks CI
  • exceptions are reviewed and expire
  • dashboard shows trend over time

Milestone 5: Profile INP

Create a deliberately slow interaction, then fix it.

Example slow paths:

  • filtering a large table synchronously
  • rendering 1000 rows without virtualization
  • expensive derived state on every keystroke
  • client-side markdown/rendering in the input path
  • analytics call blocking a click handler

Workflow:

  1. Record interaction in Chrome DevTools Performance panel.
  2. Identify long tasks over 50ms.
  3. Identify React render cause.
  4. Move expensive work off input path.
  5. Add memoization only where profiling proves it helps.
  6. Re-test INP and user-visible behavior.

Acceptance criteria:

  • before/after trace is attached to the ADR or PR
  • long task count decreases
  • INP improves without breaking behavior

Milestone 6: Optimize LCP and CLS

LCP checklist:

  • identify LCP element
  • avoid delaying it behind client JS
  • use correct image dimensions and priority
  • reduce render-blocking CSS
  • serve from CDN
  • avoid late personalization for hero content

CLS checklist:

  • reserve image/media dimensions
  • avoid inserting banners above content after load
  • keep skeleton dimensions stable
  • avoid font swap layout shifts
  • avoid generated UI that changes layout without reserved space

Acceptance criteria:

  • LCP element is known for each critical route
  • CLS sources are documented and fixed
  • synthetic and field metrics both improve

Milestone 7: Third-party governance

Inventory scripts:

ScriptOwnerPurposeLoad strategyCostRemoval plan

Rules:

  • no third-party script without owner
  • async/defer is not enough if script consumes CPU
  • analytics should batch or move server-side where possible
  • replay tools should be sampled or delayed
  • marketing tags need performance budget impact

Acceptance criteria:

  • third-party request count is visible
  • one high-cost script is removed, delayed, or moved server-side
  • future additions require review

Milestone 8: Performance incident playbook

Define:

  • detection threshold
  • owner
  • rollback path
  • communication channel
  • regression triage checklist
  • postmortem template

Example trigger:

p75 LCP for /quote worsens by 20% for mobile traffic after release.

Triage:

  1. Compare by release.
  2. Segment by route/device/region/variant.
  3. Check bundle delta and new domains.
  4. Review RUM errors and long tasks.
  5. Roll back feature flag if user harm is material.
  6. Add regression guard.

Lab pages to build

Build two intentionally imperfect pages:

PageInitial flaws
Lead landing pageoversized hero image, late font, third-party tag, layout shift banner
Dashboardlarge table, expensive filter, heavy chart library, duplicate fetches

Then optimize them in stages. This teaches diagnosis instead of cargo-cult fixes.

Optimization backlog

ProblemFixMetric affected
oversized heroresponsive image, dimensions, priorityLCP, CLS
late font swappreload or font-display strategyCLS, LCP
heavy chart on first loadlazy load below fold/tabJS KB, INP
large table rendervirtualization or paginationINP
synchronous filteringdebounce, worker, memoized indexINP
third-party tagdelay, server-side route, removerequests, CPU
duplicate fetchesroute loader/cache key ownershiprequests, TTFB

Every optimization should include before/after evidence.

CI budget file shape

Create one checked-in budget file and one generated report.

{
"route": "/lead",
"owner": "growth-web",
"budgets": {
"initialJsKb": 120,
"totalImageKb": 400,
"thirdPartyRequests": 5,
"lighthousePerformance": 90
},
"exceptions": []
}

Exception example:

{
"metric": "initialJsKb",
"allowedUntil": "2026-07-01",
"reason": "temporary chart migration",
"owner": "analytics-team"
}

Performance review packet

For a serious change, attach:

  • route budget diff
  • bundle diff
  • Web Vitals before/after
  • top long tasks
  • new third-party domains
  • screenshots of LCP element before/after
  • rollback plan

This packet makes performance review factual rather than taste-based.

Source lens

This tutorial is informed by web.dev Web Vitals and performance budget guidance, plus the book's chapters on Core Web Vitals, bundle topology, RUM/synthetic correlation, interaction latency, and performance incident response.