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: swap with 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 transform animations instead of top /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 requestIdleCallback for non-urgent work
requestIdleCallback(() => { // 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-Encoding header 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 alt attributes
-
Deferred scripts do not break keyboard navigation
-
Skeleton loaders have aria-busy="true" and aria-label
-
prefers-reduced-motion is 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)
- [Finding] - [Impact] - [Recommended Fix]
P1 (Address This Sprint)
- [Finding] - [Impact] - [Recommended Fix]
P2 (Address This Quarter)
- [Finding] - [Impact] - [Recommended Fix]
Optimization Recommendations (Priority Order)
- [Recommendation with estimated impact]
- [Recommendation with estimated impact]
- [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