web-perf
Web Performance Audit
Structured 5-phase web performance audit workflow. Diagnose performance bottlenecks, measure Core Web Vitals, and produce actionable optimization recommendations.
When to Apply
Use this skill when:
- Auditing website performance for Core Web Vitals compliance
- Diagnosing slow page loads, high Time to Interactive, or layout shifts
- Optimizing Largest Contentful Paint (LCP), Cumulative Layout Shift (CLS), or Interaction to Next Paint (INP)
- Reviewing frontend code for performance anti-patterns
- Preparing a site for Google's page experience ranking signals
- Optimizing build output for Webpack, Vite, Next.js, or Nuxt
Core Web Vitals Thresholds
| Metric | Good | Needs Improvement | Poor | What It Measures |
|---|---|---|---|---|
| LCP | <= 2.5s | 2.5s - 4.0s | > 4.0s | Loading performance |
| CLS | <= 0.1 | 0.1 - 0.25 | > 0.25 | Visual stability |
| INP | <= 200ms | 200ms - 500ms | > 500ms | Interactivity (replaced FID) |
Additional Performance Metrics
| Metric | Good | Poor | What It Measures |
|---|---|---|---|
| FCP | <= 1.8s | > 3.0s | First content rendered |
| TTFB | <= 800ms | > 1800ms | Server response time |
| TBT | <= 200ms | > 600ms | Main thread blocking |
| Speed Index | <= 3.4s | > 5.8s | Visual completeness over time |
5-Phase Audit Workflow
Phase 1: Performance Trace
Capture a performance trace to establish baseline metrics.
Browser-Based (Chrome DevTools):
- Open Chrome DevTools (F12) > Performance tab
- Click "Record" and reload the page
- Stop recording after page fully loads
- Analyze the flame chart for:
- Long tasks (> 50ms, marked in red)
- Layout thrashing (forced reflow cycles)
- Render-blocking resources
- JavaScript execution bottlenecks
Lighthouse Audit:
# CLI-based Lighthouse audit
npx lighthouse https://example.com --output=json --output-path=./lighthouse-report.json
# With specific categories
npx lighthouse https://example.com --only-categories=performance --output=html
# Mobile simulation (default)
npx lighthouse https://example.com --preset=perf --throttling-method=simulate
Key Trace Indicators:
- Main thread busy time: Should be < 4s total
- Largest task duration: Should be < 250ms
- Script evaluation time: Should be < 2s
- Layout/style recalculation: Should be < 500ms
Phase 2: Core Web Vitals Analysis
Measure each Core Web Vital and identify specific causes.
LCP Diagnosis
LCP measures loading performance -- when the largest content element becomes visible.
Common LCP Elements:
<img>elements (hero images)<video>poster images- Block-level elements with background images
- Text blocks (
<h1>,<p>)
LCP Optimization Checklist:
-
Preload the LCP resource
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" /> -
Eliminate render-blocking resources
<!-- Defer non-critical CSS --> <link rel="stylesheet" href="/non-critical.css" media="print" onload="this.media='all'" /> <!-- Async non-critical JS --> <script src="/analytics.js" async></script> -
Optimize server response time (TTFB)
- Use CDN for static assets
- Enable HTTP/2 or HTTP/3
- Implement server-side caching
- Use streaming SSR where supported
-
Optimize image delivery
<!-- Modern format with fallback --> <picture> <source srcset="/hero.avif" type="image/avif" /> <source srcset="/hero.webp" type="image/webp" /> <img src="/hero.jpg" alt="Hero" width="1200" height="600" fetchpriority="high" decoding="async" /> </picture>
CLS Diagnosis
CLS measures visual stability -- unexpected layout shifts during page load.
Common CLS Causes:
- Images without explicit dimensions
- Ads or embeds without reserved space
- Dynamically injected content above the fold
- Web fonts causing FOIT/FOUT (Flash of Invisible/Unstyled Text)
CLS Optimization Checklist:
-
Always set image dimensions
<img src="/photo.jpg" width="800" height="600" alt="Photo" />Or use CSS aspect-ratio:
.hero-image { aspect-ratio: 16 / 9; width: 100%; } -
Reserve space for dynamic content
.ad-slot { min-height: 250px; } .skeleton { height: 200px; background: #f0f0f0; } -
Use
font-display: swapwith size-adjust@font-face { font-family: 'CustomFont'; src: url('/font.woff2') format('woff2'); font-display: swap; size-adjust: 100.5%; /* Match fallback font metrics */ } -
Avoid inserting content above existing content
- Banners should push down from top, not shift existing content
- Use
transformanimations instead oftop/left/width/height
INP Diagnosis
INP measures interactivity -- the delay between user interaction and visual response.
Common INP Causes:
- Long JavaScript tasks blocking the main thread
- Synchronous layout/style recalculations
- Heavy event handlers
- Excessive re-renders (React, Vue)
INP Optimization Checklist:
-
Break up long tasks
// Instead of one long task function processAllItems(items) { for (const item of items) { /* heavy work */ } } // Break into chunks with scheduler async function processAllItems(items) { for (const item of items) { processItem(item); // Yield to main thread between items await scheduler.yield(); } } -
Debounce/throttle event handlers
// Throttle scroll handler let ticking = false; window.addEventListener( 'scroll', () => { if (!ticking) { requestAnimationFrame(() => { updateUI(); ticking = false; }); ticking = true; } }, { passive: true } ); -
Use
requestIdleCallbackfor non-urgent workrequestIdleCallback(() => { // Analytics, prefetching, non-visible updates sendAnalytics(data); });
Phase 3: Network Analysis
Analyze network waterfall for optimization opportunities.
Key Checks:
-
Resource count and total size
- Target: < 100 requests, < 2MB total (compressed)
- Check:
performance.getEntriesByType('resource').length
-
Critical request chains
- Identify chains longer than 3 requests
- Break chains with preload/prefetch hints
-
Compression
- All text resources should use Brotli (br) or gzip
- Check
Content-Encodingheader in response
-
Caching headers
# Immutable assets (hashed filenames) Cache-Control: public, max-age=31536000, immutable # HTML documents Cache-Control: no-cache # API responses Cache-Control: private, max-age=0, must-revalidate -
HTTP/2+ multiplexing
- Verify protocol in DevTools Network tab
- Multiple resources should load in parallel over single connection
Phase 4: Accessibility Performance
Performance optimizations must not degrade accessibility.
Validation Checklist:
- Lazy-loaded images have
altattributes - Deferred scripts do not break keyboard navigation
- Skeleton loaders have
aria-busy="true"andaria-label -
prefers-reduced-motionis respected for animations - Focus management works with dynamically loaded content
/* Respect reduced motion preference */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
scroll-behavior: auto !important;
}
}
Phase 5: Codebase Analysis
Review source code for performance anti-patterns.
Webpack Optimization
// webpack.config.js
module.exports = {
optimization: {
splitChunks: {
chunks: 'all',
maxInitialRequests: 25,
minSize: 20000,
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(/[\\/]node_modules[\\/](.*?)([\\/]|$)/)[1];
return `vendor.${packageName.replace('@', '')}`;
},
},
},
},
},
};
Vite Optimization
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
},
},
},
cssCodeSplit: true,
sourcemap: false, // Disable in production
},
});
Next.js Optimization
// next.config.ts
const nextConfig = {
images: {
formats: ['image/avif', 'image/webp'],
deviceSizes: [640, 750, 828, 1080, 1200],
},
experimental: {
optimizePackageImports: ['lucide-react', '@heroicons/react'],
},
};
Common Code Anti-Patterns
| Anti-Pattern | Impact | Fix |
|---|---|---|
| Barrel file imports | Bundle bloat | Import directly from module |
Synchronous localStorage in render |
Main thread block | Move to useEffect or worker |
| Unoptimized images | LCP, bandwidth | Use next/image or <picture> |
Inline <script> in body |
Render blocking | Use async or defer |
CSS @import chains |
CSSOM blocking | Concatenate or inline critical CSS |
| Unthrottled scroll listeners | INP | Use passive: true + requestAnimationFrame |
document.querySelectorAll in loops |
Layout thrashing | Cache DOM references |
Audit Report Template
# Web Performance Audit Report
**URL:** [target URL]
**Date:** [audit date]
**Tool:** Lighthouse [version] / Chrome DevTools
## Core Web Vitals Summary
| Metric | Score | Rating | Target |
| ------ | ----- | --------------------------- | -------- |
| LCP | X.Xs | GOOD/NEEDS IMPROVEMENT/POOR | <= 2.5s |
| CLS | X.XX | GOOD/NEEDS IMPROVEMENT/POOR | <= 0.1 |
| INP | Xms | GOOD/NEEDS IMPROVEMENT/POOR | <= 200ms |
| FCP | X.Xs | - | <= 1.8s |
| TTFB | Xms | - | <= 800ms |
| TBT | Xms | - | <= 200ms |
## Critical Findings
### P0 (Immediate Action Required)
1. [Finding] - [Impact] - [Recommended Fix]
### P1 (Address This Sprint)
1. [Finding] - [Impact] - [Recommended Fix]
### P2 (Address This Quarter)
1. [Finding] - [Impact] - [Recommended Fix]
## Optimization Recommendations (Priority Order)
1. [Recommendation with estimated impact]
2. [Recommendation with estimated impact]
3. [Recommendation with estimated impact]
Anti-Patterns
- Do NOT optimize without measuring first -- always capture baseline metrics
- Do NOT lazy-load above-the-fold content -- it worsens LCP
- Do NOT remove image dimensions to "fix" CLS -- use CSS aspect-ratio instead
- Do NOT bundle all JS into a single file -- use code splitting
- Do NOT ignore mobile performance -- test with CPU/network throttling
- Do NOT use
loading="lazy"on the LCP image -- it delays loading - Do NOT serve images without modern formats (AVIF/WebP)
References
- web.dev Core Web Vitals
- Chrome User Experience Report
- Lighthouse Performance Scoring
- Web Almanac Performance Chapter
Iron Laws
- ALWAYS measure Core Web Vitals (LCP, INP, CLS) with field data (CrUX) before proposing optimizations
- NEVER optimize based on lab data alone — real user metrics determine actual user experience
- ALWAYS prioritize LCP ≤2.5s, INP ≤200ms, CLS ≤0.1 as the primary performance targets
- NEVER ship a performance fix without a before/after measurement proving improvement
- ALWAYS address critical rendering path issues before layout or paint optimizations
Anti-Patterns
| Anti-Pattern | Why It Fails | Correct Approach |
|---|---|---|
| Optimizing without baseline measurement | Can't prove improvement, may optimize wrong thing | Measure CWV with Lighthouse and CrUX first |
| Lab-only metrics (Lighthouse only) | Doesn't reflect real user network/device conditions | Combine lab data with CrUX field data |
| Fixing CLS before LCP is addressed | LCP impacts far more users than CLS | Prioritize in order: LCP → INP → CLS |
| Shipping without before/after metrics | No evidence of improvement for stakeholders | Record pre-fix and post-fix CWV scores |
| Adding polyfills without code splitting | Bloats JS bundle for all users | Use dynamic import() with target browserslist |
Memory Protocol (MANDATORY)
Before starting:
Read .claude/context/memory/learnings.md
After completing:
- New pattern ->
.claude/context/memory/learnings.md - Issue found ->
.claude/context/memory/issues.md - Decision made ->
.claude/context/memory/decisions.md