web-vitals
Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js
Best use case
web-vitals is best used when you need a repeatable AI agent workflow instead of a one-off prompt.
Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js
Teams using web-vitals should expect a more consistent output, faster repeated execution, less prompt rewriting.
When to use this skill
- You want a reusable workflow that can be run more than once with consistent structure.
When not to use this skill
- You only need a quick one-off answer and do not need a reusable workflow.
- You cannot install or maintain the underlying files, dependencies, or repository context.
Installation
Claude Code / Cursor / Codex
Manual Installation
- Download SKILL.md from GitHub
- Place it in
.claude/skills/web-vitals/SKILL.mdinside your project - Restart your AI agent — it will auto-discover the skill
How web-vitals Compares
| Feature / Agent | web-vitals | Standard Approach |
|---|---|---|
| Platform Support | Not specified | Limited / Varies |
| Context Awareness | High | Baseline |
| Installation Complexity | Unknown | N/A |
Frequently Asked Questions
What does this skill do?
Core Web Vitals monitoring (LCP, FID, CLS, INP, TTFB), measurement with web-vitals library, reporting to analytics, and optimization strategies for Next.js
Where can I find the source code?
You can find the source code on GitHub using the link provided at the top of the page.
SKILL.md Source
# Core Web Vitals Specialist
## Purpose
Measure, monitor, and optimize Core Web Vitals -- the user-centric performance metrics that Google uses for search ranking. This skill covers the `web-vitals` library, real-user monitoring (RUM), lab testing, and concrete optimization strategies for each metric.
## Key Metrics
### The Core Web Vitals (2024+)
| Metric | Full Name | Good | Needs Improvement | Poor | What It Measures |
|--------|-----------|------|-------------------|------|-----------------|
| **LCP** | Largest Contentful Paint | <= 2.5s | <= 4.0s | > 4.0s | Loading performance |
| **INP** | Interaction to Next Paint | <= 200ms | <= 500ms | > 500ms | Responsiveness (replaced FID) |
| **CLS** | Cumulative Layout Shift | <= 0.1 | <= 0.25 | > 0.25 | Visual stability |
### Supporting Metrics
| Metric | Full Name | Good | What It Measures |
|--------|-----------|------|-----------------|
| **TTFB** | Time to First Byte | <= 800ms | Server responsiveness |
| **FCP** | First Contentful Paint | <= 1.8s | Perceived load speed |
| **FID** | First Input Delay (deprecated) | <= 100ms | Legacy responsiveness metric |
## Key Patterns
### 1. Measuring with the web-vitals Library
```typescript
// lib/web-vitals.ts
import { onLCP, onINP, onCLS, onFCP, onTTFB, type Metric } from "web-vitals";
type ReportHandler = (metric: Metric) => void;
const sendToAnalytics: ReportHandler = (metric) => {
const body = {
name: metric.name,
value: metric.value,
rating: metric.rating, // "good" | "needs-improvement" | "poor"
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
entries: metric.entries.map((e) => ({
name: e.name,
startTime: e.startTime,
duration: "duration" in e ? e.duration : undefined,
})),
};
// Use sendBeacon for reliability during page unload
if (navigator.sendBeacon) {
navigator.sendBeacon("/api/vitals", JSON.stringify(body));
} else {
fetch("/api/vitals", {
method: "POST",
body: JSON.stringify(body),
keepalive: true,
});
}
};
export function reportWebVitals() {
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onFCP(sendToAnalytics);
onTTFB(sendToAnalytics);
}
```
### 2. Next.js Integration
```typescript
// app/layout.tsx
import { WebVitalsReporter } from "@/components/web-vitals-reporter";
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<body>
{children}
<WebVitalsReporter />
</body>
</html>
);
}
```
```typescript
// components/web-vitals-reporter.tsx
"use client";
import { useEffect } from "react";
import { reportWebVitals } from "@/lib/web-vitals";
export function WebVitalsReporter() {
useEffect(() => {
reportWebVitals();
}, []);
return null;
}
```
```typescript
// app/api/vitals/route.ts
import { NextRequest, NextResponse } from "next/server";
export async function POST(request: NextRequest) {
const metric = await request.json();
// Log for monitoring (replace with your analytics pipeline)
console.log(`[Web Vital] ${metric.name}: ${metric.value} (${metric.rating})`);
// Forward to your analytics service
// await analytics.track("web_vital", metric);
return NextResponse.json({ ok: true });
}
```
### 3. LCP Optimization
```typescript
// Priority hints for LCP element
// In Next.js, use priority prop on the LCP image
import Image from "next/image";
export function HeroSection() {
return (
<section>
{/* priority prop adds fetchpriority="high" and preloads the image */}
<Image
src="/hero.webp"
alt="Hero banner"
width={1200}
height={600}
priority
sizes="100vw"
/>
<h1>Welcome</h1>
</section>
);
}
```
```html
<!-- Manual preload for critical resources -->
<head>
<!-- Preload LCP image -->
<link rel="preload" as="image" href="/hero.webp" fetchpriority="high" />
<!-- Preload critical fonts -->
<link rel="preload" as="font" href="/fonts/inter-var.woff2"
type="font/woff2" crossorigin="anonymous" />
<!-- Preconnect to critical origins -->
<link rel="preconnect" href="https://cdn.example.com" />
<link rel="dns-prefetch" href="https://api.example.com" />
</head>
```
```typescript
// next.config.ts -- optimize images for LCP
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
formats: ["image/avif", "image/webp"],
deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048],
minimumCacheTTL: 31536000, // 1 year
},
experimental: {
optimizeCss: true, // Minimize CSS for faster rendering
optimizePackageImports: [
"lucide-react",
"@radix-ui/react-icons",
],
},
};
export default nextConfig;
```
### 4. CLS Prevention
```typescript
// Always set explicit dimensions on images and videos
// BAD: causes layout shift
<img src="/photo.jpg" alt="Photo" />
// GOOD: reserves space
<img src="/photo.jpg" alt="Photo" width={800} height={600} />
// Next.js Image handles this automatically
<Image src="/photo.jpg" alt="Photo" width={800} height={600} />
```
```css
/* Reserve space for dynamic content with aspect-ratio */
.video-container {
aspect-ratio: 16 / 9;
width: 100%;
background-color: #f0f0f0; /* placeholder color */
}
/* Prevent font swap layout shifts */
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-display: optional; /* or "swap" with size-adjust */
size-adjust: 100%;
ascent-override: 90%;
descent-override: 20%;
line-gap-override: 0%;
}
/* Prevent ad/embed CLS */
.ad-slot {
min-height: 250px; /* reserve minimum space */
contain: layout; /* CSS containment */
}
```
```typescript
// Prevent CLS from dynamically loaded content
"use client";
import { useState, useTransition } from "react";
export function DynamicList() {
const [items, setItems] = useState<Item[]>([]);
const [isPending, startTransition] = useTransition();
const loadMore = () => {
startTransition(async () => {
const newItems = await fetchItems();
setItems((prev) => [...prev, ...newItems]);
});
};
return (
<div>
{/* Fixed-height container prevents CLS */}
<div style={{ minHeight: items.length > 0 ? "auto" : "400px" }}>
{items.map((item) => (
<div key={item.id} className="h-20"> {/* fixed item height */}
{item.name}
</div>
))}
</div>
<button onClick={loadMore} disabled={isPending}>
{isPending ? "Loading..." : "Load More"}
</button>
</div>
);
}
```
### 5. INP Optimization
```typescript
// Break up long tasks with yield-to-main patterns
function yieldToMain(): Promise<void> {
return new Promise((resolve) => {
// scheduler.yield() is the modern API (Chrome 129+)
if ("scheduler" in globalThis && "yield" in (globalThis as any).scheduler) {
(globalThis as any).scheduler.yield().then(resolve);
} else {
setTimeout(resolve, 0);
}
});
}
// Use in long-running event handlers
async function handleExpensiveClick(items: Item[]) {
for (let i = 0; i < items.length; i++) {
processItem(items[i]);
// Yield every 50 items to keep INP low
if (i % 50 === 0 && i > 0) {
await yieldToMain();
}
}
}
```
```typescript
// Use React transitions for non-urgent updates
"use client";
import { useState, useTransition, useDeferredValue } from "react";
export function SearchWithResults() {
const [query, setQuery] = useState("");
const deferredQuery = useDeferredValue(query);
const [isPending, startTransition] = useTransition();
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
// Urgent: update input value immediately
setQuery(e.target.value);
// Results will re-render with deferred value (non-blocking)
};
return (
<div>
<input value={query} onChange={handleInput} />
<div style={{ opacity: isPending ? 0.7 : 1 }}>
<SearchResults query={deferredQuery} />
</div>
</div>
);
}
```
### 6. TTFB Optimization
```typescript
// next.config.ts -- enable streaming and edge rendering where appropriate
const nextConfig: NextConfig = {
// Use PPR (Partial Prerendering) for optimal TTFB
experimental: {
ppr: true,
},
};
```
```typescript
// app/products/page.tsx -- streaming with Suspense
import { Suspense } from "react";
export default function ProductsPage() {
return (
<div>
{/* Shell renders immediately (fast TTFB) */}
<h1>Products</h1>
<SearchBar />
{/* Data streams in as it resolves */}
<Suspense fallback={<ProductGridSkeleton />}>
<ProductGrid />
</Suspense>
<Suspense fallback={<RecommendationsSkeleton />}>
<Recommendations />
</Suspense>
</div>
);
}
```
### 7. Performance Budget Monitoring
```typescript
// performance-budget.config.ts
export const performanceBudget = {
lcp: 2500, // ms
inp: 200, // ms
cls: 0.1, // score
ttfb: 800, // ms
fcp: 1800, // ms
// Bundle budgets
jsBundle: 150_000, // 150KB compressed
cssBundle: 50_000, // 50KB compressed
totalTransfer: 500_000, // 500KB total
} as const;
// CI check with Lighthouse
// .github/workflows/lighthouse.yml
/*
name: Lighthouse CI
on: [pull_request]
jobs:
lighthouse:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- run: npm ci && npm run build
- name: Lighthouse
uses: treosh/lighthouse-ci-action@v12
with:
urls: |
http://localhost:3000/
http://localhost:3000/products
budgetPath: ./budget.json
uploadArtifacts: true
*/
```
```json
// budget.json (Lighthouse CI budget)
[
{
"path": "/*",
"timings": [
{ "metric": "largest-contentful-paint", "budget": 2500 },
{ "metric": "cumulative-layout-shift", "budget": 0.1 },
{ "metric": "interactive", "budget": 3500 },
{ "metric": "first-contentful-paint", "budget": 1800 }
],
"resourceSizes": [
{ "resourceType": "script", "budget": 150 },
{ "resourceType": "stylesheet", "budget": 50 },
{ "resourceType": "total", "budget": 500 }
]
}
]
```
## Best Practices
1. **Measure in the field, not just in the lab** -- Lighthouse (lab) gives a starting point, but RUM data from `web-vitals` reflects real user experience
2. **Use the 75th percentile** -- Google evaluates Web Vitals at the 75th percentile of page loads, not the average
3. **Prioritize the LCP resource** -- Use `fetchpriority="high"`, preload, and avoid lazy-loading the LCP element
4. **Avoid layout shifts from web fonts** -- Use `font-display: optional` or `swap` with proper fallback metrics
5. **Defer non-critical JavaScript** -- Use dynamic imports, `next/dynamic`, or `<script defer>` for non-essential code
6. **Use CSS containment** -- `contain: layout` prevents layout recalculations from propagating
7. **Minimize main-thread work** -- Offload heavy computation to Web Workers, and yield to main between tasks
8. **Optimize server response time** -- Use edge rendering, streaming SSR, and CDN caching to reduce TTFB
9. **Set explicit sizes** on all images, videos, iframes, and ad slots to prevent CLS
10. **Test on real devices** -- Use Chrome DevTools throttling or real mobile devices, not just fast desktops
## Common Pitfalls
| Pitfall | Impact | Fix |
|---------|--------|-----|
| Lazy-loading the LCP image | LCP delayed by intersection observer | Use `priority` prop or `fetchpriority="high"` |
| Injecting content above the fold after load | CLS spike | Reserve space or use CSS `contain` |
| Large synchronous event handlers | Poor INP | Break up work, use `startTransition`, yield to main |
| Third-party scripts blocking render | LCP and INP degradation | Load with `async`/`defer`, use Partytown for heavy scripts |
| No font fallback metrics | CLS on font swap | Use `size-adjust`, `ascent-override` in `@font-face` |
| Measuring only in lab (Lighthouse) | Misses real-world variation | Set up RUM with `web-vitals` library |
| Not segmenting by page type | Aggregate data hides problems | Report vitals per route/page template |
| Render-blocking CSS | Slow FCP and LCP | Inline critical CSS, defer non-critical stylesheets |
## Examples
### Example 1: Full Next.js Web Vitals Pipeline
```
1. Install: npm install web-vitals
2. Create lib/web-vitals.ts with measurement + sendBeacon reporting
3. Create WebVitalsReporter client component, mount in root layout
4. Create /api/vitals endpoint to receive and forward metrics
5. Set up Lighthouse CI in GitHub Actions for PR checks
6. Configure performance budgets in budget.json
7. Monitor field data in analytics dashboard
```
### Example 2: Optimizing a Slow Product Page
```
Problem: LCP = 4.2s, CLS = 0.35, INP = 450ms
LCP Fix:
- Move hero image from lazy to priority loading
- Preconnect to image CDN origin
- Convert from PNG to AVIF/WebP
CLS Fix:
- Add width/height to all product images
- Reserve space for price badge that loads async
- Set font-display: optional on custom font
INP Fix:
- Wrap filter state update in startTransition
- Virtualize product grid (only render visible items)
- Move sort computation to Web Worker
Result: LCP = 1.8s, CLS = 0.02, INP = 120ms
```Related Skills
ultrathink
UltraThink Workflow OS — 4-layer skill mesh with persistent memory and privacy hooks for complex engineering tasks. Routes prompts through intent detection to activate the right domain skills automatically.
ultrathink_review
Multi-pass code review powered by UltraThink's quality gate — checks correctness, security (OWASP), performance, readability, and project conventions in a single structured pass.
ultrathink_memory
Persistent memory system for UltraThink — search, save, and recall project context, decisions, and patterns across sessions using Postgres-backed fuzzy search with synonym expansion.
ui-design
Comprehensive UI design system: 230+ font pairings, 48 themes, 65 design systems, 23 design languages, 30 UX laws, 14 color systems, Swiss grid, Gestalt principles, Pencil.dev workflow. Inherits ui-ux-pro-max (99 UX rules) + impeccable-frontend-design (anti-AI-slop). Triggers on any design, UI, layout, typography, color, theme, or styling task.
Zod
> TypeScript-first schema validation with static type inference.
webinar-registration-page
Build a webinar or live event registration page as a self-contained HTML file with countdown timer, speaker bio, agenda, and registration form. Triggers on: "build a webinar registration page", "create a webinar sign-up page", "event registration landing page", "live training registration page", "workshop sign-up page", "create a webinar page", "build an event page", "free webinar landing page", "live demo registration page", "online event page", "create a registration page for my webinar", "build a training event page".
webhooks
Webhook design patterns — delivery, retry with exponential backoff, HMAC signature verification, payload validation, idempotency keys
web-workers
Offload heavy computation from the main thread using Web Workers, SharedWorkers, and Comlink — structured messaging, transferable objects, and off-main-thread architecture patterns
web-components
Native Web Components, custom elements API, Shadow DOM, HTML templates, slots, lifecycle callbacks, and framework-agnostic design patterns
wasm
WebAssembly integration — Rust to WASM with wasm-pack/wasm-bindgen, WASI, browser usage, server-side WASM, and performance considerations
vue
Vue 3 Composition API, Nuxt patterns, reactivity system, component architecture, and production development practices
Vitest
> Blazing fast unit testing powered by Vite — Jest-compatible API, native ESM, TypeScript.