Now liveThe Skillselion MCP - thousands of ranked skills, loaded into your agent mid-task. No install.Get it →
addyosmani avatar

Core Web Vitals

  • 17.6k installs
  • 2.5k repo stars
  • Updated June 14, 2026
  • addyosmani/web-quality-skills

Core Web Vitals is an optimization skill covering the three Google-ranked page experience metrics (LCP, INP, CLS) with framework-agnostic patterns.

About

Core Web Vitals is a specialized skill for optimizing Largest Contentful Paint (LCP <2.5s), Interaction to Next Paint (INP <200ms), and Cumulative Layout Shift (CLS <0.1). Developers use it to improve page experience signals that directly affect Google Search ranking. Works across React, Vue, Nuxt, Astro, and plain HTML.

  • Covers all three Core Web Vitals: LCP, INP, CLS with specific optimization targets
  • Framework-agnostic patterns for React, Vue, Nuxt, Astro, and static HTML
  • 150+ Lighthouse audit patterns across Performance, Accessibility, SEO, and Best Practices

Core Web Vitals by the numbers

  • 17,619 all-time installs (skills.sh)
  • +1,266 installs in the week ending Jul 28, 2026 (Skillselion tracking)
  • Ranked #32 of 2,277 Frontend Development skills by installs in the Skillselion catalog
  • Security screen: LOW risk (skills.sh audit)
  • Data as of Jul 28, 2026 (Skillselion catalog sync)
At a glance

core-web-vitals capabilities & compatibility

Capabilities
performance audit · seo optimization · accessibility check
Use cases
frontend · seo
npx skills add https://github.com/addyosmani/web-quality-skills --skill core-web-vitals

Add your badge

Show developers this skill is listed on Skillselion. Paste this into your README.

Listed on Skillselion
Installs17.6k
repo stars2.5k
Security audit3 / 3 scanners passed
Last updatedJune 14, 2026
Repositoryaddyosmani/web-quality-skills

What it does

Core Web Vitals is a specialized skill for optimizing Largest Contentful Paint (LCP <2.5s), Interaction to Next Paint (INP <200ms), and Cumulative Layout Shift (CLS <0.1). Developers use it to improv

Who is it for?

Improving search ranking, reducing page load delays, minimizing layout shift

Skip if: Server infrastructure or database optimization

When should I use this skill?

Optimizing page experience, fixing Lighthouse performance scores, or addressing search ranking factors

What you get

Pages reach good thresholds (LCP <=2.5s, INP <=200ms, CLS <=0.1) and improve search ranking signals

  • Optimized LCP loading strategy
  • INP-friendly interaction patterns
  • CLS-stable layout implementation

By the numbers

  • Defines thresholds: LCP <=2.5s good, 2.5-4.0s needs improvement, >4.0s poor
  • Covers 150+ Lighthouse audit patterns across four categories
  • Provides performance budgets: <1.5MB total, <300KB JS (compressed), <100KB CSS

Files

SKILL.mdMarkdownGitHub ↗

Core Web Vitals optimization

Targeted optimization for the three Core Web Vitals metrics that affect Google Search ranking and user experience.

The three metrics

MetricMeasuresGoodNeeds workPoor
LCPLoading≤ 2.5s2.5s – 4s> 4s
INPInteractivity≤ 200ms200ms – 500ms> 500ms
CLSVisual Stability≤ 0.10.1 – 0.25> 0.25

Google measures at the 75th percentile — 75% of page visits must meet "Good" thresholds.

---

LCP: Largest Contentful Paint

LCP measures when the largest visible content element renders. Usually this is:

  • Hero image or video
  • Large text block
  • Background image
  • <svg> element

Common LCP issues

1. Slow server response (TTFB > 800ms)

Fix: CDN, caching, optimized backend, edge rendering

2. Render-blocking resources

<!-- ❌ Blocks rendering -->
<link rel="stylesheet" href="/all-styles.css">

<!-- ✅ Critical CSS inlined, rest deferred -->
<style>/* Critical above-fold CSS */</style>
<link rel="preload" href="/styles.css" as="style" 
      onload="this.onload=null;this.rel='stylesheet'">

3. Slow resource load times

<!-- ❌ No hints, discovered late -->
<img src="/hero.jpg" alt="Hero">

<!-- ✅ Preloaded with high priority -->
<link rel="preload" href="/hero.webp" as="image" fetchpriority="high">
<img src="/hero.webp" alt="Hero" fetchpriority="high">

4. Client-side rendering delays

// ❌ Content loads after JavaScript
useEffect(() => {
  fetch('/api/hero-text').then(r => r.json()).then(setHeroText);
}, []);

// ✅ Server-side or static rendering
// Use SSR, SSG, or streaming to send HTML with content
export async function getServerSideProps() {
  const heroText = await fetchHeroText();
  return { props: { heroText } };
}

5. Make navigations instant with the Speculation Rules API

For most sites, the LCP a user actually experiences is dominated by the next page they navigate to, not the one they landed on. Telling the browser to prerender likely-next pages on hover collapses that LCP to ~0ms.

<script type="speculationrules">
{
  "prerender": [{
    "where": { "href_matches": "/*" },
    "eagerness": "moderate"
  }]
}
</script>

eagerness settings (cheapest → most aggressive): conservative (start on pointerdown), moderate (start after ~200ms hover), eager (start as soon as the link is in the viewport), immediate (start on page load). Start with moderate — it captures most navigations without prerendering pages users never visit.

Caveats:

  • Bandwidth/CPU cost. Each prerender is roughly a full page load. Scope where carefully (href_matches patterns, exclude logout/checkout) and avoid immediate outside small sites.
  • Side effects fire early. Analytics, ads, and any code that runs on load will fire when the prerender starts, not when the user navigates. Gate side effects on the `prerenderingchange` event or document.prerendering.
  • Chromium-only. Safari and Firefox ignore the script — it's a progressive enhancement, never a regression.

LCP optimization checklist

- [ ] TTFB < 800ms (use CDN, edge caching)
- [ ] LCP image preloaded with fetchpriority="high"
- [ ] LCP image optimized (WebP/AVIF, correct size)
- [ ] Critical CSS inlined (< 14KB)
- [ ] No render-blocking JavaScript in <head>
- [ ] Fonts don't block text rendering (font-display: swap)
- [ ] LCP element in initial HTML (not JS-rendered)
- [ ] Speculation Rules added for likely-next navigations (moderate eagerness)

LCP element identification

// Find your LCP element
new PerformanceObserver((list) => {
  const entries = list.getEntries();
  const lastEntry = entries[entries.length - 1];
  console.log('LCP element:', lastEntry.element);
  console.log('LCP time:', lastEntry.startTime);
}).observe({ type: 'largest-contentful-paint', buffered: true });

---

INP: Interaction to Next Paint

INP measures responsiveness across ALL interactions (clicks, taps, key presses) during a page visit. It reports the worst interaction (at 98th percentile for high-traffic pages).

INP breakdown

Total INP = Input Delay + Processing Time + Presentation Delay

PhaseTargetOptimization
Input Delay< 50msReduce main thread blocking
Processing< 100msOptimize event handlers
Presentation< 50msMinimize rendering work

Common INP issues

1. Long tasks blocking main thread

// ❌ Long synchronous task
function processLargeArray(items) {
  items.forEach(item => expensiveOperation(item));
}

// ✅ Break into chunks and yield to the scheduler. scheduler.yield() is the
//    recommended modern API — its continuation is queued at a boosted
//    priority so the rest of your work resumes ahead of unrelated tasks,
//    while still letting the browser handle pending input first.
async function processLargeArray(items) {
  const CHUNK_SIZE = 100;
  for (let i = 0; i < items.length; i += CHUNK_SIZE) {
    items.slice(i, i + CHUNK_SIZE).forEach(expensiveOperation);

    if ('scheduler' in window && 'yield' in scheduler) {
      await scheduler.yield();
    } else {
      // Fallback for browsers without scheduler.yield (Safari, older Firefox).
      // setTimeout(0) yields but loses priority — your continuation may run
      // after unrelated tasks the browser picked up in between.
      await new Promise(r => setTimeout(r, 0));
    }
  }
}

2. Heavy event handlers

// ❌ All work in handler
button.addEventListener('click', () => {
  // Heavy computation
  const result = calculateComplexThing();
  // DOM updates
  updateUI(result);
  // Analytics
  trackEvent('click');
});

// ✅ Prioritize visual feedback, then yield before doing the heavy work
button.addEventListener('click', async () => {
  // 1. Immediate visual feedback (cheap DOM update)
  button.classList.add('loading');

  // 2. Yield so the browser can paint the loading state before we block
  if ('scheduler' in window && 'yield' in scheduler) {
    await scheduler.yield();
  }

  // 3. Now do the heavy work — the user already saw the click register
  const result = calculateComplexThing();
  updateUI(result);

  // 4. Lowest-priority work last, when the main thread is idle
  if ('requestIdleCallback' in window) {
    requestIdleCallback(() => trackEvent('click'));
  } else {
    setTimeout(() => trackEvent('click'), 0);
  }
});

3. Third-party scripts

// ❌ Eagerly loaded, blocks interactions
<script src="https://heavy-widget.com/widget.js"></script>

// ✅ Lazy loaded on interaction or visibility
const loadWidget = () => {
  import('https://heavy-widget.com/widget.js')
    .then(widget => widget.init());
};
button.addEventListener('click', loadWidget, { once: true });

4. Excessive re-renders (React/Vue)

// ❌ Re-renders entire tree
function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Counter count={count} />
      <ExpensiveComponent /> {/* Re-renders on every count change */}
    </div>
  );
}

// ✅ Memoized expensive components
const MemoizedExpensive = React.memo(ExpensiveComponent);

function App() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <Counter count={count} />
      <MemoizedExpensive />
    </div>
  );
}

INP optimization checklist

- [ ] No tasks > 50ms on main thread
- [ ] Event handlers complete quickly (< 100ms)
- [ ] Visual feedback provided immediately
- [ ] Heavy work deferred with requestIdleCallback
- [ ] Third-party scripts don't block interactions
- [ ] Debounced input handlers where appropriate
- [ ] Web Workers for CPU-intensive operations

INP debugging

// Identify slow interactions. durationThreshold: 40 matches what the
// web-vitals library uses — 16 (one frame) fires on nearly every interaction
// and drowns the console; 40 surfaces interactions that are starting to feel
// sluggish without spamming.
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (entry.duration > 200) {
      console.warn('Slow interaction:', {
        type: entry.name,
        duration: entry.duration,
        processingStart: entry.processingStart,
        processingEnd: entry.processingEnd,
        target: entry.target
      });
    }
  }
}).observe({ type: 'event', buffered: true, durationThreshold: 40 });

For field debugging across real users, prefer the web-vitals/attribution build of the web-vitals libraryonINP() from that build attaches a LoAF (Long Animation Frame) breakdown identifying the longest script and the input/processing/presentation phase that ate the budget.

---

CLS: Cumulative Layout Shift

CLS measures unexpected layout shifts. A shift occurs when a visible element changes position between frames without user interaction.

CLS Formula: impact fraction × distance fraction

Common CLS causes

1. Images without dimensions

<!-- ❌ Causes layout shift when loaded -->
<img src="photo.jpg" alt="Photo">

<!-- ✅ Space reserved -->
<img src="photo.jpg" alt="Photo" width="800" height="600">

<!-- ✅ Or use aspect-ratio -->
<img src="photo.jpg" alt="Photo" style="aspect-ratio: 4/3; width: 100%;">

2. Ads, embeds, and iframes

<!-- ❌ Unknown size until loaded -->
<iframe src="https://ad-network.com/ad"></iframe>

<!-- ✅ Reserve space with min-height -->
<div style="min-height: 250px;">
  <iframe src="https://ad-network.com/ad" height="250"></iframe>
</div>

<!-- ✅ Or use aspect-ratio container -->
<div style="aspect-ratio: 16/9;">
  <iframe src="https://youtube.com/embed/..." 
          style="width: 100%; height: 100%;"></iframe>
</div>

3. Dynamically injected content

// ❌ Inserts content above viewport
notifications.prepend(newNotification);

// ✅ Insert below viewport or use transform
const insertBelow = viewport.bottom < newNotification.top;
if (insertBelow) {
  notifications.prepend(newNotification);
} else {
  // Animate in without shifting
  newNotification.style.transform = 'translateY(-100%)';
  notifications.prepend(newNotification);
  requestAnimationFrame(() => {
    newNotification.style.transform = '';
  });
}

4. Web fonts causing FOUT

/* ❌ Font swap shifts text */
@font-face {
  font-family: 'Custom';
  src: url('custom.woff2') format('woff2');
}

/* ✅ Optional font (no shift if slow) */
@font-face {
  font-family: 'Custom';
  src: url('custom.woff2') format('woff2');
  font-display: optional;
}

/* ✅ Or match fallback metrics */
@font-face {
  font-family: 'Custom';
  src: url('custom.woff2') format('woff2');
  font-display: swap;
  size-adjust: 105%; /* Match fallback size */
  ascent-override: 95%;
  descent-override: 20%;
}

5. Animations triggering layout

/* ❌ Animates layout properties */
.animate {
  transition: height 0.3s, width 0.3s;
}

/* ✅ Use transform instead */
.animate {
  transition: transform 0.3s;
}
.animate.expanded {
  transform: scale(1.2);
}

CLS optimization checklist

- [ ] All images have width/height or aspect-ratio
- [ ] All videos/embeds have reserved space
- [ ] Ads have min-height containers
- [ ] Fonts use font-display: optional or matched metrics
- [ ] Dynamic content inserted below viewport
- [ ] Animations use transform/opacity only
- [ ] No content injected above existing content

CLS debugging

// Track layout shifts
new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      console.log('Layout shift:', entry.value);
      entry.sources?.forEach(source => {
        console.log('  Shifted element:', source.node);
        console.log('  Previous rect:', source.previousRect);
        console.log('  Current rect:', source.currentRect);
      });
    }
  }
}).observe({ type: 'layout-shift', buffered: true });

---

Measurement tools

Lab testing

  • Chrome DevTools → Performance panel, Lighthouse
  • WebPageTest → Detailed waterfall, filmstrip
  • Lighthouse CLInpx lighthouse <url>

Field data (real users)

  • Chrome User Experience Report (CrUX) → BigQuery or API
  • Search Console → Core Web Vitals report
  • web-vitals library → Send to your analytics
import {onLCP, onINP, onCLS} from 'web-vitals';

function sendToAnalytics({name, value, rating}) {
  gtag('event', name, {
    event_category: 'Web Vitals',
    value: Math.round(name === 'CLS' ? value * 1000 : value),
    event_label: rating
  });
}

onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);

---

Framework quick fixes

Next.js

// LCP: Use next/image with priority
import Image from 'next/image';
<Image src="/hero.jpg" priority fill alt="Hero" />

// INP: Use dynamic imports
const HeavyComponent = dynamic(() => import('./Heavy'), { ssr: false });

// CLS: Image component handles dimensions automatically

React

// LCP: Preload in head
<link rel="preload" href="/hero.jpg" as="image" fetchpriority="high" />

// INP: Memoize and useTransition
const [isPending, startTransition] = useTransition();
startTransition(() => setExpensiveState(newValue));

// CLS: Always specify dimensions in img tags

Vue/Nuxt

<!-- LCP: Use nuxt/image with preload -->
<NuxtImg src="/hero.jpg" preload loading="eager" />

<!-- INP: Use async components -->
<component :is="() => import('./Heavy.vue')" />

<!-- CLS: Use aspect-ratio CSS -->
<img :style="{ aspectRatio: '16/9' }" />

References

Related skills

How it compares

Use core-web-vitals for structured LCP playbooks when you already know scores are bad but need phase-by-phase fixes.

FAQ

What is LCP and how do I fix it?

Largest Contentful Paint measures when the largest element loads. Target <2.5s. Optimize critical rendering path, use next/image or nuxt/image, implement lazy loading, and preconnect to critical resources.

What framework patterns does this cover?

React/Next.js (next/image, React.lazy, useCallback/useMemo), Vue/Nuxt (nuxt/image, async components, computed), Svelte/SvelteKit, Astro (Image component, partial hydration), and static HTML (native lazy loading, picture element).

Is Core Web Vitals safe to install?

skills.sh reports 3 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.

This week in AI coding

Five minutes, every Monday - the tools, releases and tactics for developers.

unsubscribe anytime.