# Web Performance Audit

> Diagnose and fix real web performance problems measured against Core Web Vitals, not vibes. Use when a page feels slow, a Lighthouse/CWV score regressed, or before shipping a heavy feature. Walks load-time (LCP/bundle/images), interactivity (INP/main-thread), and layout-stability (CLS) with the specific fix for each, and forbids premature micro-optimization.

- Skill: `omonuj/web-performance-audit` (Agent Skill)
- Install (CLI): `npx skillmds@latest add omonuj/web-performance-audit`
- Raw SKILL.md: https://api.skillmd.com/api/skills/omonuj/web-performance-audit/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: omonuj (https://skillmd.com/u/omonuj)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/omonuj/web-performance-audit

---


# web-performance-audit

Performance work goes wrong in two directions: shipping a heavy page nobody profiled, or `useMemo`-ing everything on a hunch. This skill does neither — it measures against **Core Web Vitals**, finds the actual bottleneck, and applies the fix that moves that specific metric. Measure, fix the biggest thing, re-measure. Never guess.

## Measure first (always)
- **Field data > lab data.** Real-user monitoring (CrUX / web-vitals library reporting to your analytics) reflects real devices and networks; Lighthouse is a lab proxy. Trust the field; use the lab to reproduce and iterate.
- Profile on a **throttled mid-tier mobile** (4x CPU slowdown, Fast 3G), not your laptop. Your machine hides every problem your users have.
- Establish a baseline number before touching anything, so you can prove the fix worked. No baseline = no claim of improvement.

## The three vitals and their fixes

### LCP — Largest Contentful Paint (loading), target < 2.5s
The biggest above-the-fold element rendered. Usually a hero image or a heading blocked by resources.
- **Images:** serve responsive sizes (`srcset`/`sizes`), modern formats (AVIF/WebP), and `priority`/`fetchpriority=high` + preload for the LCP image. Lazy-load *below*-the-fold images only — never the LCP one.
- **Render-blocking resources:** inline critical CSS, defer non-critical CSS/JS, `preconnect` to required origins. Every blocking `<script>` in `<head>` delays paint.
- **Server/TTFB:** if the document itself is slow, no front-end trick helps — fix the backend/cache/CDN first.
- **Fonts:** `font-display: swap` (or `optional`) and preload the font so text isn't invisible while the font loads.

### INP — Interaction to Next Paint (interactivity), target < 200ms
How fast the UI responds to input. Dominated by long tasks blocking the main thread.
- **Break up long tasks.** A JS task > 50ms blocks input. Chunk heavy work, `yield` to the event loop (`scheduler.yield()` / `setTimeout` / `isInputPending`), or move it off-thread to a **Web Worker**.
- **Ship less JS.** Code-split by route and lazy-load below-the-fold/interaction-triggered components. The fastest script is the one you don't send.
- **Fix expensive re-renders** in the framework: memoize genuinely-expensive subtrees, virtualize long lists (render only visible rows), debounce high-frequency handlers. But profile first — most `useMemo` is noise.
- **Hydration:** for SSR, large hydration is a common INP killer — consider partial/progressive hydration or server components to hydrate less.

### CLS — Cumulative Layout Shift (stability), target < 0.1
Content jumping as the page loads. Almost always avoidable.
- **Reserve space** for images/video/embeds with explicit `width`/`height` or `aspect-ratio` — never let a late-loading image reflow text.
- **Reserve space for dynamic content** (ads, banners, async data) with a min-height skeleton so its arrival doesn't shove content down.
- **Fonts:** size-adjust the fallback font to match the web font's metrics so the swap doesn't reflow.
- Never insert content above existing content unless in response to a user interaction.

## Bundle discipline
- Set a **performance budget** (e.g. main bundle < 170KB gzipped) and fail CI when it's exceeded — perf regressions creep in one dependency at a time.
- Analyze the bundle (`source-map-explorer` / bundler analyzer) and hunt: a moment.js locale bomb, an entire lodash for one function, a charting lib pulled in eagerly, duplicated deps from version skew.
- Prefer tree-shakeable, ESM, side-effect-free libraries; import the function, not the namespace (`import debounce from 'lodash/debounce'`).
- Lazy-load routes and heavy, non-critical components (modals, editors, charts) behind dynamic imports.

## The premature-optimization guardrails (what NOT to do)
- Don't `useMemo`/`useCallback`/`React.memo` everything — each has a cost, and most components are cheap. Add memoization only where the profiler shows an expensive render firing too often.
- Don't micro-optimize a loop that runs 10 times. Optimize the thing the profiler says is hot; ignore the rest.
- Don't chase a Lighthouse 100 at the cost of real UX. The score is a proxy; field vitals and actual responsiveness are the target.
- Don't add a Web Worker / virtualization / SSR complexity before proving the simpler fix (ship less JS) isn't enough.

## Procedure
1. Capture the baseline: field vitals if available, plus a throttled Lighthouse/profiler trace to reproduce.
2. Identify which vital is failing and find the single biggest contributor (LCP element, longest task, largest layout shift).
3. Apply the targeted fix from the section above.
4. Re-measure on the same throttled profile; confirm the metric moved. If it didn't, you fixed the wrong thing — revert and re-diagnose.
5. Repeat for the next-biggest contributor until vitals are within target.
6. Add a bundle-size budget + a CWV check to CI so the win doesn't regress.

## Definition of done
- LCP < 2.5s, INP < 200ms, CLS < 0.1 on a throttled mid-tier mobile profile (and improving in field data).
- Each fix verified by before/after measurement, not asserted.
- A bundle budget and vitals check run in CI.
- No speculative memoization added; changes trace to a profiled bottleneck.

