Skills

Install

$ npx ai-agents-skills add --skill web-performance
Domain v1.0

Web Performance

Optimize loading speed, interactivity, and visual stability using Core Web Vitals and resource budgets.

When to Use

  • Improving Lighthouse performance scores
  • Diagnosing slow LCP, high INP, or layout shifts (CLS)
  • Reducing JavaScript or CSS bundle size
  • Optimizing images, fonts, or third-party scripts
  • Auditing page load performance across frameworks

Don’t use for:

  • React component re-render optimization (use react)
  • Next.js image/font components (use next)
  • Accessibility audits (use a11y)
  • SEO meta tags or structured data (use web-seo)

Critical Patterns

✅ REQUIRED: Core Web Vitals Thresholds

LCP (Largest Contentful Paint)  Good: ≤ 2.5s  Poor: > 4.0s
INP (Interaction to Next Paint)  Good: ≤ 200ms  Poor: > 500ms
CLS (Cumulative Layout Shift)    Good: ≤ 0.1   Poor: > 0.25

Measure with: Chrome DevTools Performance tab, Lighthouse, Web Vitals extension, or web-vitals npm package.

✅ REQUIRED: Resource Budgets

JavaScript (compressed): < 300 KB
CSS (compressed):        < 100 KB
Total page weight:       < 1.5 MB

Enforce with bundler size limits (Vite build.rollupOptions, Webpack performance.maxAssetSize).

✅ REQUIRED: Image Optimization

<!-- ✅ CORRECT: Modern format, explicit size, lazy loading -->
<img src="hero.webp" width="800" height="600"
     alt="Hero image" loading="lazy" fetchpriority="high" />

<!-- ❌ WRONG: No dimensions (causes CLS), no lazy loading -->
<img src="photo.jpg" alt="Photo" />

Rules: Always specify width/height to prevent CLS. Use loading="lazy" for below-fold images. Use fetchpriority="high" on the LCP image only.

✅ REQUIRED: Font Loading — Eliminate CLS

<!-- ✅ Preload critical font + swap -->
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preload" href="/fonts/inter.woff2" as="font"
      type="font/woff2" crossorigin />
/* ✅ font-display: swap prevents invisible text, reduces CLS */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: swap;
}

✅ REQUIRED: Script Loading

<!-- ✅ Async for independent scripts -->
<script src="analytics.js" async></script>

<!-- ✅ Defer for scripts that need DOM -->
<script src="app.js" defer></script>

<!-- ❌ WRONG: Blocking render -->
<script src="app.js"></script>

❌ NEVER: Render-Blocking Resources

<!-- ❌ WRONG: Blocks rendering -->
<link rel="stylesheet" href="non-critical.css" />

<!-- ✅ CORRECT: Inline critical CSS, async-load the rest -->
<style>/* critical above-fold styles */</style>
<link rel="stylesheet" href="styles.css" media="print"
      onload="this.media='all'" />

Decision Tree

LCP > 2.5s?
  → Is LCP element an image? → Optimize image size, add fetchpriority="high", use WebP/AVIF
  → Is LCP element text? → Check font loading (font-display: swap + preload)
  → Slow server response? → Add SSR/SSG or caching layer
  → See references/core-web-vitals.md for full LCP diagnosis

INP > 200ms?
  → Long JS tasks on interaction? → Split tasks with scheduler.yield() or setTimeout
  → Heavy input handler? → Debounce/throttle, move work to Web Worker
  → See references/core-web-vitals.md for full INP diagnosis

CLS > 0.1?
  → Images without dimensions? → Add explicit width/height attributes
  → Font swap causing jump? → Preload font + font-display: swap or optional
  → Dynamic content inserted above fold? → Reserve space with min-height
  → See references/core-web-vitals.md for full CLS diagnosis

Bundle too large (JS > 300KB)?
  → Identify with source-map-explorer or bundler analyzer
  → Lazy-load routes and heavy components (dynamic import)
  → Tree-shake unused exports, audit dependencies
  → See references/resource-optimization.md

Images unoptimized?
  → Convert to WebP/AVIF, add responsive srcset, enable lazy loading
  → See references/image-video.md

Font causing layout shift?
  → Add font-display: swap, preload critical fonts, use size-adjust
  → See references/font-loading.md

Framework-specific?
  → React: re-render optimization → react skill
  → Next.js: next/image, next/font, PPR → next skill
  → Astro: output mode, islands → astro skill

Example

Diagnosing and fixing poor LCP on an e-commerce product page.

<!-- Before: LCP image unoptimized, no priority hint -->
<img src="product.jpg" class="hero" alt="Running shoes" />

<!-- After: WebP format, explicit dimensions, LCP hint, lazy for rest -->
<img
  src="product.webp"
  width="800" height="600"
  alt="Running shoes"
  fetchpriority="high"
  decoding="async"
/>
<img src="thumbnail.webp" width="200" height="150"
     alt="Side view" loading="lazy" />
<!-- Preload font to prevent invisible text during load -->
<link rel="preload" href="/fonts/inter-regular.woff2"
      as="font" type="font/woff2" crossorigin />
// Measure LCP with web-vitals library
import { onLCP, onINP, onCLS } from 'web-vitals';

onLCP(({ value }) => console.log('LCP:', value));
onINP(({ value }) => console.log('INP:', value));
onCLS(({ value }) => console.log('CLS:', value));

Result: LCP drops from 4.2s → 1.8s after image format change + fetchpriority hint.


Edge Cases

Third-party scripts: Ad scripts, chat widgets, and analytics are common INP offenders. Load with async, defer initialization, or use a Facade pattern (lazy-load on first interaction).

Variable fonts: A single variable font file replaces multiple weight files, reducing requests. Use font-display: optional if layout shift on first load is unacceptable.

Responsive images: Use srcset + sizes for art direction. Without sizes, browser downloads the wrong size.

<img srcset="img-400.webp 400w, img-800.webp 800w"
     sizes="(max-width: 600px) 400px, 800px"
     src="img-800.webp" alt="..." />

INP vs FID: FID (First Input Delay) was replaced by INP in March 2024. INP measures all interactions throughout page lifetime, not just the first.


Resources

See references/README.md for navigation.