Web Vitals Analyzer
Diagnoses Core Web Vitals failures and frontend performance issues by analyzing page structure, resource loading patterns, JavaScript bundle composition, and rendering behavior. Produces prioritized fixes with estimated metric improvements.
When to Use
- User asks to optimize frontend performance or Core Web Vitals (LCP, CLS, INP).
- User shares a Lighthouse report (JSON or screenshot) and wants root-cause analysis.
- User reports slow page load, layout shift, or laggy interactions.
- User asks to reduce JavaScript bundle size or identify render-blocking resources.
- User mentions specific metrics: "LCP is 5s", "CLS is 0.25", "INP is 600ms".
- User asks for quick wins to improve Lighthouse performance score.
Trigger keywords: Core Web Vitals, LCP, CLS, INP, Lighthouse, PageSpeed, performance score, render-blocking, bundle size, lazy loading, layout shift, main thread, long tasks, code splitting, TTFB, first paint.
Prerequisites
- A Lighthouse report (JSON preferred), PageSpeed Insights URL, or Chrome DevOps performance trace if available.
- The page URL or source code (HTML, CSS, JS entry points).
- Framework identification (React, Next.js, Vue, Nuxt, SvelteKit, vanilla, etc.).
- Current metric values if known (LCP, CLS, INP, TTFB, total bundle size).
If any of these are missing, ask for them before proceeding. Do not give generic advice without knowing what is actually slow.
Procedure
1. Gather Current State
Ask for or analyze:
- Lighthouse report (JSON or screenshot)
- The page URL or source code (HTML, CSS, JS entry points)
- Framework used (React, Next.js, Vue, vanilla, etc.)
- Current LCP, CLS, and INP values if available
2. Analyze Largest Contentful Paint (LCP) — target < 2.5s
Identify the LCP element (usually hero image, heading, or above-fold content), then check each item:
- Is the LCP image lazy-loaded? It should NOT be. Remove
loading="lazy" from above-fold hero images.
- Is there a
<link rel="preload"> for the LCP resource? If not, add one:<link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">
- Are render-blocking CSS/JS delaying first paint? Inline critical CSS; defer non-critical stylesheets.
- Server response time (TTFB): If > 800ms, flag as a server-side issue (CDN, caching, SSR optimization).
- Font loading strategy: Is
font-display: swap or optional used? If not, add it.
- Critical CSS: Is it inlined in
<head> or loaded as a blocking stylesheet? Inline above-fold critical CSS.
3. Analyze Cumulative Layout Shift (CLS) — target < 0.1
Identify elements causing layout shifts (images without dimensions, dynamic content, ads, web fonts), then check:
- Do all
<img> and <video> tags have explicit width and height? If not, add them.
- Is content injected above existing content after load? Reserve space with fixed-height containers.
- Are web fonts causing FOUT/FOIT shifts? Use
font-display: swap and size-adjust to match fallback metrics.
- Are dynamic ads or embeds reserving space? Use
aspect-ratio or fixed container dimensions:.ad-slot { aspect-ratio: 728 / 90; min-height: 90px; }
4. Analyze Interaction to Next Paint (INP) — target < 200ms
Identify heavy JavaScript on the main thread, then check:
- Are event handlers doing synchronous layout thrashing? Batch DOM reads and writes separately.
- Long tasks (> 50ms) blocking the main thread? Break them up with
scheduler.yield() or setTimeout.
- Unnecessary re-rendering? For React: check missing
useMemo, React.memo(), excessive state updates. For Vue: check reactive dependencies. For Svelte: check unnecessary store subscriptions.
- Third-party scripts blocking interaction? (analytics, chat widgets, A/B testing). Load with
async/defer; move tracking to requestIdleCallback:document.addEventListener('click', (e) => {
requestIdleCallback(() => trackClick(e));
});
- Suggest offloading:
requestIdleCallback, scheduler.yield(), web workers for heavy computation.
5. Analyze Bundle and Resource Loading
- Identify total JS bundle size and largest chunks.
- Check for unused JavaScript (Chrome DevTools Coverage tab).
- Suggest code splitting opportunities (route-based, component-based, dynamic
import()).
- Check image formats: suggest WebP/AVIF for photos, SVG for icons. Note: AVIF has better compression but slower decode — WebP is the safer default.
- Identify duplicate dependencies in the bundle (check
package-lock.json or npm ls).
- Verify text compression (gzip/brotli) is enabled on the server.
6. Output Format
Produce a structured diagnosis:
Core Web Vitals Diagnosis
==========================
LCP: [current] (target: < 2.5s)
CLS: [current] (target: < 0.1)
INP: [current] (target: < 200ms)
Root Causes (ranked by impact):
1. [SEVERITY]: [Description]
File: [exact file and line]
Fix: [specific code change]
Expected improvement: [-Xs / -Xms]
2. ...
Quick wins (< 15 min each):
- [Actionable item]
- [Actionable item]
Severity levels: CRITICAL, HIGH, MODERATE, LOW.
Examples
Example 1: Poor LCP on a marketing site
Input:
Our landing page scores 38 on Lighthouse performance. LCP is 5.2s. Here's the page source.
Output:
Core Web Vitals Diagnosis
==========================
LCP: 5.2s (target: < 2.5s)
CLS: 0.03 (target: < 0.1)
INP: N/A (no interaction data)
Root Causes (ranked by impact):
1. CRITICAL: Hero image (2.4MB JPEG) is lazy-loaded
File: src/components/Hero.tsx:12
Fix: Remove loading="lazy" from the above-fold hero image.
Add: <link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">
Convert to WebP: reduces 2.4MB → 340KB
Expected LCP improvement: -2.1s
2. HIGH: 3 render-blocking CSS files (total 180KB uncompressed)
Files: fonts.css (89KB), animations.css (52KB), above-fold.css (39KB)
Fix: Inline critical CSS (above-fold.css) in <head>.
Load fonts.css and animations.css with media="print" onload hack.
Expected LCP improvement: -0.8s
3. MODERATE: Google Fonts loaded via @import in CSS (parser-blocking)
Fix: Switch to <link rel="preconnect" href="https://fonts.gstatic.com">
and <link rel="stylesheet" href="fonts-url"> in HTML <head>
Expected LCP improvement: -0.3s
Quick wins (< 15 min each):
- Add fetchpriority="high" to hero image tag
- Add width/height to hero image (already good CLS, but defensive)
- Enable text compression (gzip/brotli) on server — saves 60% on CSS
Example 2: High INP on a React dashboard
Input:
Our dashboard app has INP of 890ms. Users complain clicks feel laggy. It's a React 18 app with Zustand state management.
Output:
INP Diagnosis: 890ms (target: < 200ms)
Root Causes:
1. CRITICAL: Filter dropdown triggers full data grid re-render (45,000 rows)
File: src/components/DataGrid.tsx
The grid component re-renders on every filter change because the parent
passes a new array reference each time.
Fix: Memoize the filtered data with useMemo:
const filteredData = useMemo(() => data.filter(filterFn), [data, filters]);
Wrap DataGrid in React.memo().
Expected INP improvement: -500ms
2. HIGH: Analytics script (third-party) runs 180ms synchronous task on click
Source: analytics.min.js:1 event listener on document.body
Fix: Load analytics with async/defer. Move tracking to requestIdleCallback:
document.addEventListener('click', (e) => {
requestIdleCallback(() => trackClick(e));
});
Expected INP improvement: -180ms
3. MODERATE: Date formatting in table cells using Intl.DateTimeFormat per-render
Fix: Create formatter once outside component:
const fmt = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' });
Expected INP improvement: -80ms
Pitfalls
- Never give generic advice without identifying the specific LCP element. Always pinpoint what is actually slow.
- CLS fixes must include explicit code changes — dimensions,
aspect-ratio values, container styles. Do not say "add dimensions" without specifying the values.
- For INP, focus on the main thread. Network latency does not directly affect INP.
- Recommend
fetchpriority="high" only for the single most important above-fold resource. Adding it to multiple elements defeats the purpose.
- AVIF has better compression but slower decode. WebP is the safer default for broad compatibility.
- Do not recommend service workers for performance unless the use case specifically involves repeat visits.
- Account for framework-specific patterns: Next.js Image component, Nuxt's
useAsyncData, SvelteKit's load functions. Do not give vanilla JS advice for a framework app.
- Do not remove
loading="lazy" from below-fold images — only the LCP element and above-fold content.
font-display: optional causes fonts to not render at all on slow connections. Use swap for body text, optional only for non-critical decorative fonts.
media="print" onload CSS hack can cause FOUC. Test visually after applying.
Verification
After implementing fixes, verify with:
Re-run Lighthouse and confirm the performance score improved:
npx lighthouse https://your-site.com --output=json --output-path=./lighthouse-after.json --preset=desktop
Compare lighthouse-after.json to the before report.
Check Core Web Vitals field data via PageSpeed Insights:
https://pagespeed.web.dev/analysis?url=https://your-site.com
Verify LCP element changed in Chrome DevTools:
- Open DevTools → Performance tab
- Record a page load
- Check "Largest Contentful Paint" marker identifies the correct (optimized) element
Verify no new layout shifts:
- Open DevTools → Performance → Record page load
- Check "Layout Shift" entries — CLS should be < 0.1
Verify INP improvement:
- Open DevTools → Performance → Record interaction
- Check total blocking time and long tasks (> 50ms) are reduced
Verify bundle size reduction:
# Next.js
npx @next/bundle-analyzer
# Webpack
npx webpack-bundle-analyzer dist/stats.json
# Vite
npx vite-bundle-visualizer
Verify image format conversion:
# Check file sizes
ls -lh public/images/hero.webp public/images/hero.jpg
Related Skills
- frontend-design-system: For component-level accessibility, design tokens, and UI state coverage.
- accessibility-audit: For WCAG 2.2 compliance checks that overlap with performance (focus management, reduced motion).
References
Re-check official/current docs before relying on provider-specific APIs, policy, pricing, security behavior, or platform rules.
1---2name: web-vitals-analyzer3description: Diagnoses Core Web Vitals failures (LCP, CLS, INP) from Lighthouse JSON or PageSpeed traces into ranked file-and-line root causes with expected metric deltas. Use when a Lighthouse score, LCP/CLS/INP number, layout shift, laggy click, or bundle-size complaint needs RCA. Not a CI budget gate (frontend-lighthouse) and not the markup-remediation implementer (performance-and-web-vitals).4---5
6# Web Vitals Analyzer
7
8Diagnoses Core Web Vitals failures and frontend performance issues by analyzing page structure, resource loading patterns, JavaScript bundle composition, and rendering behavior. Produces prioritized fixes with estimated metric improvements.
9
10## When to Use
11
12- User asks to optimize frontend performance or Core Web Vitals (LCP, CLS, INP).
13- User shares a Lighthouse report (JSON or screenshot) and wants root-cause analysis.
14- User reports slow page load, layout shift, or laggy interactions.
15- User asks to reduce JavaScript bundle size or identify render-blocking resources.
16- User mentions specific metrics: "LCP is 5s", "CLS is 0.25", "INP is 600ms".
17- User asks for quick wins to improve Lighthouse performance score.
18
19Trigger keywords: Core Web Vitals, LCP, CLS, INP, Lighthouse, PageSpeed, performance score, render-blocking, bundle size, lazy loading, layout shift, main thread, long tasks, code splitting, TTFB, first paint.
20
21## Prerequisites
22
23- A Lighthouse report (JSON preferred), PageSpeed Insights URL, or Chrome DevOps performance trace if available.
24- The page URL or source code (HTML, CSS, JS entry points).
25- Framework identification (React, Next.js, Vue, Nuxt, SvelteKit, vanilla, etc.).
26- Current metric values if known (LCP, CLS, INP, TTFB, total bundle size).
27
28If any of these are missing, ask for them before proceeding. Do not give generic advice without knowing what is actually slow.
29
30## Procedure
31
32### 1. Gather Current State
33
34Ask for or analyze:
35- Lighthouse report (JSON or screenshot)
36- The page URL or source code (HTML, CSS, JS entry points)
37- Framework used (React, Next.js, Vue, vanilla, etc.)
38- Current LCP, CLS, and INP values if available
39
40### 2. Analyze Largest Contentful Paint (LCP) — target < 2.5s
41
42Identify the LCP element (usually hero image, heading, or above-fold content), then check each item:
43
441. **Is the LCP image lazy-loaded?** It should NOT be. Remove `loading="lazy"` from above-fold hero images.
452. **Is there a `<link rel="preload">` for the LCP resource?** If not, add one:
46 ```html
47 <link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">
48 ```
493. **Are render-blocking CSS/JS delaying first paint?** Inline critical CSS; defer non-critical stylesheets.
504. **Server response time (TTFB):** If > 800ms, flag as a server-side issue (CDN, caching, SSR optimization).
515. **Font loading strategy:** Is `font-display: swap` or `optional` used? If not, add it.
526. **Critical CSS:** Is it inlined in `<head>` or loaded as a blocking stylesheet? Inline above-fold critical CSS.
53
54### 3. Analyze Cumulative Layout Shift (CLS) — target < 0.1
55
56Identify elements causing layout shifts (images without dimensions, dynamic content, ads, web fonts), then check:
57
581. **Do all `<img>` and `<video>` tags have explicit `width` and `height`?** If not, add them.
592. **Is content injected above existing content after load?** Reserve space with fixed-height containers.
603. **Are web fonts causing FOUT/FOIT shifts?** Use `font-display: swap` and `size-adjust` to match fallback metrics.
614. **Are dynamic ads or embeds reserving space?** Use `aspect-ratio` or fixed container dimensions:
62 ```css
63 .ad-slot { aspect-ratio: 728 / 90; min-height: 90px; }
64 ```
65
66### 4. Analyze Interaction to Next Paint (INP) — target < 200ms
67
68Identify heavy JavaScript on the main thread, then check:
69
701. **Are event handlers doing synchronous layout thrashing?** Batch DOM reads and writes separately.
712. **Long tasks (> 50ms) blocking the main thread?** Break them up with `scheduler.yield()` or `setTimeout`.
723. **Unnecessary re-rendering?** For React: check missing `useMemo`, `React.memo()`, excessive state updates. For Vue: check reactive dependencies. For Svelte: check unnecessary store subscriptions.
734. **Third-party scripts blocking interaction?** (analytics, chat widgets, A/B testing). Load with `async`/`defer`; move tracking to `requestIdleCallback`:
74 ```javascript
75 document.addEventListener('click', (e) => {
76 requestIdleCallback(() => trackClick(e));
77 });
78 ```
795. **Suggest offloading:** `requestIdleCallback`, `scheduler.yield()`, web workers for heavy computation.
80
81### 5. Analyze Bundle and Resource Loading
82
831. Identify total JS bundle size and largest chunks.
842. Check for unused JavaScript (Chrome DevTools Coverage tab).
853. Suggest code splitting opportunities (route-based, component-based, dynamic `import()`).
864. Check image formats: suggest WebP/AVIF for photos, SVG for icons. Note: AVIF has better compression but slower decode — WebP is the safer default.
875. Identify duplicate dependencies in the bundle (check `package-lock.json` or `npm ls`).
886. Verify text compression (gzip/brotli) is enabled on the server.
89
90### 6. Output Format
91
92Produce a structured diagnosis:
93
94```
95Core Web Vitals Diagnosis
96==========================
97LCP: [current] (target: < 2.5s)
98CLS: [current] (target: < 0.1)
99INP: [current] (target: < 200ms)
100
101Root Causes (ranked by impact):
102
1031. [SEVERITY]: [Description]
104 File: [exact file and line]
105 Fix: [specific code change]
106 Expected improvement: [-Xs / -Xms]
107
1082. ...
109
110Quick wins (< 15 min each):
111- [Actionable item]
112- [Actionable item]
113```
114
115Severity levels: CRITICAL, HIGH, MODERATE, LOW.
116
117## Examples
118
119### Example 1: Poor LCP on a marketing site
120
121**Input:**
122```
123Our landing page scores 38 on Lighthouse performance. LCP is 5.2s. Here's the page source.
124```
125
126**Output:**
127```
128Core Web Vitals Diagnosis
129==========================
130LCP: 5.2s (target: < 2.5s)
131CLS: 0.03 (target: < 0.1)
132INP: N/A (no interaction data)
133
134Root Causes (ranked by impact):
135
1361. CRITICAL: Hero image (2.4MB JPEG) is lazy-loaded
137 File: src/components/Hero.tsx:12
138 Fix: Remove loading="lazy" from the above-fold hero image.
139 Add: <link rel="preload" as="image" href="/hero.jpg" fetchpriority="high">
140 Convert to WebP: reduces 2.4MB → 340KB
141 Expected LCP improvement: -2.1s
142
1432. HIGH: 3 render-blocking CSS files (total 180KB uncompressed)
144 Files: fonts.css (89KB), animations.css (52KB), above-fold.css (39KB)
145 Fix: Inline critical CSS (above-fold.css) in <head>.
146 Load fonts.css and animations.css with media="print" onload hack.
147 Expected LCP improvement: -0.8s
148
1493. MODERATE: Google Fonts loaded via @import in CSS (parser-blocking)
150 Fix: Switch to <link rel="preconnect" href="https://fonts.gstatic.com">
151 and <link rel="stylesheet" href="fonts-url"> in HTML <head>
152 Expected LCP improvement: -0.3s
153
154Quick wins (< 15 min each):
155- Add fetchpriority="high" to hero image tag
156- Add width/height to hero image (already good CLS, but defensive)
157- Enable text compression (gzip/brotli) on server — saves 60% on CSS
158```
159
160### Example 2: High INP on a React dashboard
161
162**Input:**
163```
164Our dashboard app has INP of 890ms. Users complain clicks feel laggy. It's a React 18 app with Zustand state management.
165```
166
167**Output:**
168```
169INP Diagnosis: 890ms (target: < 200ms)
170
171Root Causes:
172
1731. CRITICAL: Filter dropdown triggers full data grid re-render (45,000 rows)
174 File: src/components/DataGrid.tsx
175 The grid component re-renders on every filter change because the parent
176 passes a new array reference each time.
177 Fix: Memoize the filtered data with useMemo:
178 const filteredData = useMemo(() => data.filter(filterFn), [data, filters]);
179 Wrap DataGrid in React.memo().
180 Expected INP improvement: -500ms
181
1822. HIGH: Analytics script (third-party) runs 180ms synchronous task on click
183 Source: analytics.min.js:1 event listener on document.body
184 Fix: Load analytics with async/defer. Move tracking to requestIdleCallback:
185 document.addEventListener('click', (e) => {
186 requestIdleCallback(() => trackClick(e));
187 });
188 Expected INP improvement: -180ms
189
1903. MODERATE: Date formatting in table cells using Intl.DateTimeFormat per-render
191 Fix: Create formatter once outside component:
192 const fmt = new Intl.DateTimeFormat('en-US', { dateStyle: 'medium' });
193 Expected INP improvement: -80ms
194```
195
196## Pitfalls
197
198- **Never give generic advice without identifying the specific LCP element.** Always pinpoint what is actually slow.
199- **CLS fixes must include explicit code changes** — dimensions, `aspect-ratio` values, container styles. Do not say "add dimensions" without specifying the values.
200- **For INP, focus on the main thread.** Network latency does not directly affect INP.
201- **Recommend `fetchpriority="high"` only for the single most important above-fold resource.** Adding it to multiple elements defeats the purpose.
202- **AVIF has better compression but slower decode.** WebP is the safer default for broad compatibility.
203- **Do not recommend service workers for performance** unless the use case specifically involves repeat visits.
204- **Account for framework-specific patterns:** Next.js Image component, Nuxt's `useAsyncData`, SvelteKit's load functions. Do not give vanilla JS advice for a framework app.
205- **Do not remove `loading="lazy"` from below-fold images** — only the LCP element and above-fold content.
206- **`font-display: optional` causes fonts to not render at all on slow connections.** Use `swap` for body text, `optional` only for non-critical decorative fonts.
207- **`media="print" onload` CSS hack can cause FOUC.** Test visually after applying.
208
209## Verification
210
211After implementing fixes, verify with:
212
2131. **Re-run Lighthouse** and confirm the performance score improved:
214 ```bash
215 npx lighthouse https://your-site.com --output=json --output-path=./lighthouse-after.json --preset=desktop
216 ```
217 Compare `lighthouse-after.json` to the before report.
218
2192. **Check Core Web Vitals field data** via PageSpeed Insights:
220 ```
221 https://pagespeed.web.dev/analysis?url=https://your-site.com
222 ```
223
2243. **Verify LCP element changed** in Chrome DevTools:
225 - Open DevTools → Performance tab
226 - Record a page load
227 - Check "Largest Contentful Paint" marker identifies the correct (optimized) element
228
2294. **Verify no new layout shifts:**
230 - Open DevTools → Performance → Record page load
231 - Check "Layout Shift" entries — CLS should be < 0.1
232
2335. **Verify INP improvement:**
234 - Open DevTools → Performance → Record interaction
235 - Check total blocking time and long tasks (> 50ms) are reduced
236
2376. **Verify bundle size reduction:**
238 ```bash
239 # Next.js
240 npx @next/bundle-analyzer
241
242 # Webpack
243 npx webpack-bundle-analyzer dist/stats.json
244
245 # Vite
246 npx vite-bundle-visualizer
247 ```
248
2497. **Verify image format conversion:**
250 ```bash
251 # Check file sizes
252 ls -lh public/images/hero.webp public/images/hero.jpg
253 ```
254
255## Related Skills
256
257- **frontend-design-system**: For component-level accessibility, design tokens, and UI state coverage.
258- **accessibility-audit**: For WCAG 2.2 compliance checks that overlap with performance (focus management, reduced motion).
259
260## References
261
262- W3C WCAG 2.2: https://www.w3.org/TR/WCAG22/
263- W3C Understanding WCAG 2.2: https://www.w3.org/WAI/WCAG22/Understanding/intro
264- Apple Human Interface Guidelines: https://developer.apple.com/design/human-interface-guidelines
265- Material accessibility guidance: https://m2.material.io/design/usability/accessibility.html
266- Google Search Central documentation: https://developers.google.com/search/docs
267
268Re-check official/current docs before relying on provider-specific APIs, policy, pricing, security behavior, or platform rules.