Back to skills
SkillHub ClubShip Full StackFull StackIntegration

webperf-core-web-vitals

Intelligent Core Web Vitals analysis with automated workflows and decision trees. Measures LCP, CLS, INP with guided debugging that automatically determines follow-up analysis based on results. Includes workflows for LCP deep dive (5 phases), CLS investigation (loading vs interaction), INP debugging (latency breakdown + attribution), and cross-skill integration with loading, interaction, and media skills. Use when the user asks about Core Web Vitals, LCP optimization, layout shifts, or interaction responsiveness. Compatible with Chrome DevTools MCP.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
1,417
Hot score
99
Updated
March 20, 2026
Overall rating
C4.6
Composite score
4.6
Best-practice grade
B71.9

Install command

npx @skill-hub/cli install nucliweb-webperf-snippets-webperf-core-web-vitals

Repository

nucliweb/webperf-snippets

Skill path: skills/webperf-core-web-vitals

Intelligent Core Web Vitals analysis with automated workflows and decision trees. Measures LCP, CLS, INP with guided debugging that automatically determines follow-up analysis based on results. Includes workflows for LCP deep dive (5 phases), CLS investigation (loading vs interaction), INP debugging (latency breakdown + attribution), and cross-skill integration with loading, interaction, and media skills. Use when the user asks about Core Web Vitals, LCP optimization, layout shifts, or interaction responsiveness. Compatible with Chrome DevTools MCP.

Open repository

Best for

Primary workflow: Ship Full Stack.

Technical facets: Full Stack, Integration.

Target audience: everyone.

License: MIT.

Original source

Catalog source: SkillHub Club.

Repository owner: nucliweb.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install webperf-core-web-vitals into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/nucliweb/webperf-snippets before adding webperf-core-web-vitals to shared team environments
  • Use webperf-core-web-vitals for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: webperf-core-web-vitals
description: Intelligent Core Web Vitals analysis with automated workflows and decision trees. Measures LCP, CLS, INP with guided debugging that automatically determines follow-up analysis based on results. Includes workflows for LCP deep dive (5 phases), CLS investigation (loading vs interaction), INP debugging (latency breakdown + attribution), and cross-skill integration with loading, interaction, and media skills. Use when the user asks about Core Web Vitals, LCP optimization, layout shifts, or interaction responsiveness. Compatible with Chrome DevTools MCP.
license: MIT
metadata:
  author: Joan Leon | @nucliweb
  version: 1.0.0
  mcp-server: chrome-devtools
  category: web-performance
  repository: https://github.com/nucliweb/webperf-snippets
---

# WebPerf: Core Web Vitals

JavaScript snippets for measuring web performance in Chrome DevTools. Execute with `mcp__chrome-devtools__evaluate_script`, capture output with `mcp__chrome-devtools__get_console_message`.

## Scripts

- `scripts/CLS.js` — Cumulative Layout Shift (CLS)
- `scripts/INP.js` — Interaction to Next Paint (INP)
- `scripts/LCP-Image-Entropy.js` — LCP Image Entropy
- `scripts/LCP-Sub-Parts.js` — LCP Sub-Parts
- `scripts/LCP-Trail.js` — LCP Trail
- `scripts/LCP-Video-Candidate.js` — LCP Video Candidate
- `scripts/LCP.js` — Largest Contentful Paint (LCP)

Descriptions and thresholds: `references/snippets.md`

## Common Workflows

### Complete Core Web Vitals Audit

When the user asks for a comprehensive Core Web Vitals analysis or "audit CWV":

1. **LCP.js** - Measure Largest Contentful Paint
2. **CLS.js** - Measure Cumulative Layout Shift
3. **INP.js** - Measure Interaction to Next Paint
4. **LCP-Sub-Parts.js** - Break down LCP timing phases
5. **LCP-Trail.js** - Track LCP candidate evolution

### LCP Deep Dive

When LCP is slow or the user asks "debug LCP" or "why is LCP slow":

1. **LCP.js** - Establish baseline LCP value
2. **LCP-Sub-Parts.js** - Break down into TTFB, resource load, render delay
3. **LCP-Trail.js** - Identify all LCP candidates and changes
4. **LCP-Image-Entropy.js** - Check if LCP image has visual complexity issues
5. **LCP-Video-Candidate.js** - Detect if LCP is a video (poster or video element)

### CLS Investigation

When layout shifts are detected or the user asks "debug CLS" or "layout shift issues":

1. **CLS.js** - Measure overall CLS score
2. **Layout-Shift-Loading-and-Interaction.js** (from Interaction skill) - Separate loading vs interaction shifts
3. Cross-reference with **webperf-loading** skill:
   - Find-Above-The-Fold-Lazy-Loaded-Images.js (lazy images causing shifts)
   - Fonts-Preloaded-Loaded-and-used-above-the-fold.js (font swap causing shifts)

### INP Debugging

When interactions feel slow or the user asks "debug INP" or "slow interactions":

1. **INP.js** - Measure overall INP value
2. **Interactions.js** (from Interaction skill) - List all interactions with timing
3. **Input-Latency-Breakdown.js** (from Interaction skill) - Break down input delay, processing, presentation
4. **Long-Animation-Frames.js** (from Interaction skill) - Identify blocking animation frames
5. **Long-Animation-Frames-Script-Attribution.js** (from Interaction skill) - Find scripts causing delays

### Video as LCP Investigation

When LCP is a video element (detected by LCP-Video-Candidate.js):

1. **LCP-Video-Candidate.js** - Identify video as LCP candidate
2. **Video-Element-Audit.js** (from Media skill) - Audit video loading strategy
3. **LCP-Sub-Parts.js** - Analyze video loading phases
4. Cross-reference with **webperf-loading** skill:
   - Resource-Hints-Validation.js (check for video preload)
   - Priority-Hints-Audit.js (check for fetchpriority on video)

### Image as LCP Investigation

When LCP is an image (most common case):

1. **LCP.js** - Measure LCP timing
2. **LCP-Sub-Parts.js** - Break down timing phases
3. **LCP-Image-Entropy.js** - Analyze image complexity
4. Cross-reference with **webperf-media** skill:
   - Image-Element-Audit.js (check format, dimensions, lazy loading)
5. Cross-reference with **webperf-loading** skill:
   - Find-Above-The-Fold-Lazy-Loaded-Images.js (check if incorrectly lazy)
   - Priority-Hints-Audit.js (check for fetchpriority="high")
   - Resource-Hints-Validation.js (check for preload)

## Decision Tree

Use this decision tree to automatically run follow-up snippets based on results:

### After LCP.js

- **If LCP > 2.5s** → Run **LCP-Sub-Parts.js** to diagnose which phase is slow
- **If LCP > 4.0s (poor)** → Run full LCP deep dive workflow (5 snippets)
- **If LCP candidate is an image** → Run **LCP-Image-Entropy.js** and **webperf-media:Image-Element-Audit.js**
- **If LCP candidate is a video** → Run **LCP-Video-Candidate.js** and **webperf-media:Video-Element-Audit.js**
- **Always run** → **LCP-Trail.js** to understand candidate evolution

### After LCP-Sub-Parts.js

- **If TTFB phase > 600ms** → Switch to **webperf-loading** skill and run TTFB-Sub-Parts.js
- **If Resource Load Time > 1500ms** → Run:
  1. **webperf-loading:Resource-Hints-Validation.js** (check for preload/preconnect)
  2. **webperf-loading:Priority-Hints-Audit.js** (check fetchpriority)
  3. **webperf-loading:Find-render-blocking-resources.js** (competing resources)
- **If Render Delay > 200ms** → Run:
  1. **webperf-loading:Find-render-blocking-resources.js** (blocking CSS/JS)
  2. **webperf-loading:Script-Loading.js** (parser-blocking scripts)
  3. **webperf-interaction:Long-Animation-Frames.js** (main thread blocking)

### After LCP-Trail.js

- **If many LCP candidate changes (>3)** → This causes visual instability, investigate:
  1. **webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js** (late-loading images)
  2. **webperf-loading:Fonts-Preloaded-Loaded-and-used-above-the-fold.js** (font swaps)
  3. **CLS.js** (layout shifts contributing to LCP changes)
- **If final LCP candidate appears late** → Run **webperf-loading:Resource-Hints-Validation.js**
- **If early candidate was replaced** → Understand why initial content was pushed down (likely CLS issue)

### After LCP-Image-Entropy.js

- **If entropy is very high** → Image is visually complex, recommend:
  - Modern formats (WebP, AVIF)
  - Appropriate compression
  - Potentially a placeholder strategy
- **If entropy is low** → Image may be over-optimized or placeholder-like
- **If large file size detected** → Run **webperf-media:Image-Element-Audit.js** for format/sizing analysis

### After LCP-Video-Candidate.js

- **If video is LCP** → Run:
  1. **webperf-media:Video-Element-Audit.js** (check poster, preload, formats)
  2. **webperf-loading:Priority-Hints-Audit.js** (check fetchpriority on poster)
  3. **LCP-Sub-Parts.js** (analyze video loading phases)
- **If poster image is LCP** → Treat as image LCP (run image workflows)

### After CLS.js

- **If CLS > 0.1** → Run **webperf-interaction:Layout-Shift-Loading-and-Interaction.js** to separate causes
- **If CLS > 0.25 (poor)** → Run comprehensive shift investigation:
  1. **webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js** (images without dimensions)
  2. **webperf-loading:Fonts-Preloaded-Loaded-and-used-above-the-fold.js** (font loading strategy)
  3. **webperf-loading:Critical-CSS-Detection.js** (late-loading styles)
  4. **webperf-media:Image-Element-Audit.js** (missing width/height)
- **If CLS = 0** → Confirm with multiple page loads (might be timing-dependent)

### After INP.js

- **If INP > 200ms** → Run **webperf-interaction:Interactions.js** to identify slow interactions
- **If INP > 500ms (poor)** → Run full INP debugging workflow:
  1. **webperf-interaction:Interactions.js** (list all interactions)
  2. **webperf-interaction:Input-Latency-Breakdown.js** (phase breakdown)
  3. **webperf-interaction:Long-Animation-Frames.js** (blocking frames)
  4. **webperf-interaction:Long-Animation-Frames-Script-Attribution.js** (culprit scripts)
- **If specific interaction type is slow (e.g., keyboard)** → Focus analysis on that interaction type

### Cross-Skill Triggers

These triggers recommend using snippets from other skills:

#### From LCP to Loading Skill

- **If LCP > 2.5s and TTFB phase is dominant** → Use **webperf-loading** skill:
  - TTFB.js, TTFB-Sub-Parts.js, Service-Worker-Analysis.js

- **If LCP image is lazy-loaded** → Use **webperf-loading** skill:
  - Find-Above-The-Fold-Lazy-Loaded-Images.js

- **If LCP has no fetchpriority** → Use **webperf-loading** skill:
  - Priority-Hints-Audit.js

#### From CLS to Loading Skill

- **If CLS caused by fonts** → Use **webperf-loading** skill:
  - Fonts-Preloaded-Loaded-and-used-above-the-fold.js
  - Resource-Hints-Validation.js (for font preload)

- **If CLS caused by images** → Use **webperf-media** skill:
  - Image-Element-Audit.js (check for width/height attributes)

#### From INP to Interaction Skill

- **If INP > 200ms** → Use **webperf-interaction** skill for full debugging:
  - Interactions.js, Input-Latency-Breakdown.js
  - Long-Animation-Frames.js, Long-Animation-Frames-Script-Attribution.js
  - LongTask.js (if pre-interaction blocking suspected)

#### From LCP/INP to Interaction Skill

- **If render delay or interaction delay is high** → Use **webperf-interaction** skill:
  - Long-Animation-Frames.js (main thread blocking)
  - LongTask.js (long tasks delaying rendering)

### Multi-Metric Correlation

When multiple CWV metrics are poor, prioritize investigation:

- **If LCP > 2.5s AND CLS > 0.1** → Likely shared root cause:
  1. Check for late-loading content pushing LCP element
  2. Run LCP-Trail.js to see LCP candidate changes
  3. Run Layout-Shift-Loading-and-Interaction.js to correlate timing

- **If LCP > 2.5s AND INP > 200ms** → Main thread congestion:
  1. Run Long-Animation-Frames.js
  2. Run webperf-loading:Script-Loading.js
  3. Run webperf-loading:JS-Execution-Time-Breakdown.js

- **If CLS > 0.1 AND INP > 200ms** → Layout thrashing or interaction-triggered shifts:
  1. Run Layout-Shift-Loading-and-Interaction.js
  2. Run Interactions.js
  3. Check if shifts occur during/after interactions

## References

- `references/snippets.md` — Descriptions and thresholds for each script
- `references/schema.md` — Return value schema for interpreting script output

> Execute via `mcp__chrome-devtools__evaluate_script` → read with `mcp__chrome-devtools__get_console_message`.

---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/CLS.js

```javascript
// snippets/CoreWebVitals/CLS.js | sha256:489073431f0be946 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/CoreWebVitals/CLS.js
(()=>{let e=0;const t=e=>e<=.1?"good":e<=.25?"needs-improvement":"poor",n=()=>{t(e)},o=new PerformanceObserver(t=>{for(const n of t.getEntries())n.hadRecentInput||(e+=n.value);n()});o.observe({type:"layout-shift",buffered:!0}),document.addEventListener("visibilitychange",()=>{"hidden"===document.visibilityState&&(o.takeRecords(),n())}),window.getCLS=()=>{n();const o=t(e);return{script:"CLS",status:"ok",metric:"CLS",value:Math.round(1e4*e)/1e4,unit:"score",rating:o,thresholds:{good:.1,needsImprovement:.25}}};const r=performance.getEntriesByType("layout-shift").reduce((e,t)=>t.hadRecentInput?e:e+t.value,0);t(r);Math.round(1e4*r)})();
```

### scripts/INP.js

```javascript
// snippets/CoreWebVitals/INP.js | sha256:7d7935e7d6d4cabd | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/CoreWebVitals/INP.js
(()=>{const t=[];let e=0,n=null;const r=t=>t<=200?"good":t<=500?"needs-improvement":"poor",s=()=>{if(0===t.length)return{value:0,entry:null};const e=[...t].sort((t,e)=>e.duration-t.duration),n=t.length<50?0:Math.floor(.02*t.length);return{value:e[n].duration,entry:e[n]}},a=t=>{const e=t.target;if(!e)return t.name;let n=e.tagName.toLowerCase();if(e.id)n+=`#${e.id}`;else if(e.className&&"string"==typeof e.className){const t=e.className.trim().split(/\s+/).slice(0,2).join(".");t&&(n+=`.${t}`)}return`${t.name} → ${n}`},i=t=>{const e={inputDelay:0,processingTime:0,presentationDelay:0};return t.processingStart&&t.processingEnd&&(e.inputDelay=t.processingStart-t.startTime,e.processingTime=t.processingEnd-t.processingStart,e.presentationDelay=t.duration-e.inputDelay-e.processingTime),e},o=new PerformanceObserver(r=>{for(const o of r.getEntries()){if(!o.interactionId)continue;const r=t.find(t=>t.interactionId===o.interactionId);if(!r||o.duration>r.duration){if(r){const e=t.indexOf(r);t.splice(e,1)}t.push({name:o.name,duration:o.duration,startTime:o.startTime,interactionId:o.interactionId,target:o.target,processingStart:o.processingStart,processingEnd:o.processingEnd,formattedName:a(o),phases:i(o),entry:o})}const c=s();e=c.value,n=c.entry}});o.observe({type:"event",buffered:!0,durationThreshold:16});const c=()=>{r(e);if(n){n.target&&(t=>{if(!t)return"";const e=[];let n=t;for(;n&&n!==document.body&&e.length<5;){let t=n.tagName.toLowerCase();if(n.id)t+=`#${n.id}`;else if(n.className&&"string"==typeof n.className){const e=n.className.trim().split(/\s+/).slice(0,2).join(".");e&&(t+=`.${e}`)}e.unshift(t),n=n.parentElement}e.join(" > ")})(n.target);const t=n.phases;if(t.inputDelay>0){const e=n.duration,r=40;"▓".repeat(Math.round(t.inputDelay/e*r)),"█".repeat(Math.round(t.processingTime/e*r)),"░".repeat(Math.round(t.presentationDelay/e*r))}}const s=t.filter(t=>t.duration>200).sort((t,e)=>e.duration-t.duration).slice(0,10);s.length>0&&s.slice(0,3).forEach((t,e)=>{});const a={};t.forEach(t=>{const e=t.name;a[e]||(a[e]={count:0,totalDuration:0,maxDuration:0}),a[e].count++,a[e].totalDuration+=t.duration,a[e].maxDuration=Math.max(a[e].maxDuration,t.duration)}),Object.keys(a)};window.getINP=()=>{const a=s();e=a.value,n=a.entry,c();const i=r(e),o={totalInteractions:t.length};return n&&(o.worstEvent=n.formattedName,o.phases={inputDelay:Math.round(n.phases.inputDelay),processingTime:Math.round(n.phases.processingTime),presentationDelay:Math.round(n.phases.presentationDelay)}),0===t.length?{script:"INP",status:"error",error:"No interactions recorded yet",metric:"INP",value:0,unit:"ms",rating:"good",thresholds:{good:200,needsImprovement:500},details:o}:{script:"INP",status:"ok",metric:"INP",value:Math.round(e),unit:"ms",rating:i,thresholds:{good:200,needsImprovement:500},details:o}},window.getINPDetails=()=>{if(0===t.length)return[];const e=[...t].sort((t,e)=>e.duration-t.duration),n=Math.min(e.length,15);return e.slice(0,n).forEach((t,e)=>{t.target&&(t=>{if(!t)return"";const e=[];let n=t;for(;n&&n!==document.body&&e.length<5;){let t=n.tagName.toLowerCase();if(n.id)t+=`#${n.id}`;else if(n.className&&"string"==typeof n.className){const e=n.className.trim().split(/\s+/).slice(0,2).join(".");e&&(t+=`.${e}`)}e.unshift(t),n=n.parentElement}e.join(" > ")})(t.target)}),e},document.addEventListener("visibilitychange",()=>{if("hidden"===document.visibilityState){o.takeRecords();const t=s();e=t.value,n=t.entry,c()}})})();
```

### scripts/LCP-Image-Entropy.js

```javascript
// snippets/CoreWebVitals/LCP-Image-Entropy.js | sha256:e072b4731bcc7fdf | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/CoreWebVitals/LCP-Image-Entropy.js
(()=>{const e=e=>{if(!e)return"-";const t=Math.floor(Math.log(e)/Math.log(1024));return(e/Math.pow(1024,t)).toFixed(1)+" "+["B","KB","MB"][t]};let t=null,r=null;const i=new PerformanceObserver(e=>{const i=e.getEntries(),n=i[i.length-1];n&&(t=n.element,r=n.url)});i.observe({type:"largest-contentful-paint",buffered:!0}),setTimeout(()=>{i.disconnect();const n=[...document.images].filter(e=>{const t=e.currentSrc||e.src;return t&&!t.startsWith("data:image")}).map(e=>{const i=e.currentSrc||e.src,n=performance.getEntriesByName(i)[0],s=n?.encodedBodySize||0,o=e.naturalWidth*e.naturalHeight,l=o>0?8*s/o:0,a=l>0&&l<.05,p=t===e||r===i;return{element:e,src:i,shortSrc:i.split("/").pop()?.split("?")[0]||i,width:e.naturalWidth,height:e.naturalHeight,fileSize:s,bpp:l,isLowEntropy:a,isLCP:p,lcpEligible:!a&&l>0}}).filter(e=>e.bpp>0);if(0===n.length)return;const s=n.filter(e=>e.isLowEntropy);n.filter(e=>!e.isLowEntropy),n.find(e=>e.isLCP);n.sort((e,t)=>t.bpp-e.bpp).map(t=>({Image:t.shortSrc.length>30?"..."+t.shortSrc.slice(-27):t.shortSrc,Dimensions:`${t.width}×${t.height}`,Size:e(t.fileSize),BPP:t.bpp.toFixed(4),Entropy:t.isLowEntropy?"🔴 Low":"🟢 Normal","LCP Eligible":t.lcpEligible?"✅":"❌","Is LCP":t.isLCP?"👈":""})),s.length>0&&s.forEach(e=>{}),n.forEach((e,t)=>{})},100);const n=performance.getEntriesByType("largest-contentful-paint").at(-1),s=n?.element??null,o=n?.url??null,l=[...document.images].filter(e=>{const t=e.currentSrc||e.src;return t&&!t.startsWith("data:image")}).map(e=>{const t=e.currentSrc||e.src,r=performance.getEntriesByName(t)[0],i=r?.encodedBodySize||0,n=e.naturalWidth*e.naturalHeight,l=n>0?8*i/n:0,a=l>0&&l<.05,p=s===e||o===t;return{url:t.split("/").pop()?.split("?")[0]||t,width:e.naturalWidth,height:e.naturalHeight,fileSizeBytes:i,bpp:Math.round(1e4*l)/1e4,isLowEntropy:a,lcpEligible:!a&&l>0,isLCP:p}}).filter(e=>e.bpp>0),a=l.filter(e=>e.isLowEntropy).length,p=l.find(e=>e.isLCP),c=[];a>0&&c.push({severity:"warning",message:`${a} image(s) have low entropy and are LCP-ineligible in Chrome 112+`}),p?.isLowEntropy&&c.push({severity:"error",message:"Current LCP image has low entropy and may be skipped by Chrome"})})();
```

### scripts/LCP-Sub-Parts.js

```javascript
// snippets/CoreWebVitals/LCP-Sub-Parts.js | sha256:675014efb100840b | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/CoreWebVitals/LCP-Sub-Parts.js
(()=>{const e=e=>e<=2500?"good":e<=4e3?"needs-improvement":"poor",t=[{name:"Time to First Byte",key:"ttfb",target:800},{name:"Resource Load Delay",key:"loadDelay",targetPercent:10},{name:"Resource Load Time",key:"loadTime",targetPercent:40},{name:"Element Render Delay",key:"renderDelay",targetPercent:10}],a=()=>{const e=performance.getEntriesByType("navigation")[0];return e?.responseStart>0&&e.responseStart<performance.now()?e:null},r=new PerformanceObserver(r=>{const n=r.getEntries().at(-1);if(!n)return;const s=a();if(!s)return;const o=performance.getEntriesByType("resource").find(e=>e.name===n.url),l=s.activationStart||0,i=Math.max(0,s.responseStart-l),m=Math.max(i,o?(o.requestStart||o.startTime)-l:0),u=Math.max(m,o?o.responseEnd-l:0),c=Math.max(u,n.startTime-l),p={ttfb:i,loadDelay:m-i,loadTime:u-m,renderDelay:c-u};e(c);if(n.element){const e=n.element;let t=e.tagName.toLowerCase();if(e.id)t=`#${e.id}`;else if(e.className&&"string"==typeof e.className){const a=e.className.trim().split(/\s+/).slice(0,2).join(".");a&&(t=`${e.tagName.toLowerCase()}.${a}`)}n.url&&n.url.split("/").pop()?.split("?"),e.style.outline="3px dashed lime",e.style.outlineOffset="2px"}const d=t.map(e=>({...e,value:p[e.key],percent:p[e.key]/c*100})),y=d.reduce((e,t)=>e.value>t.value?e:t),f=(d.map(e=>{const t=e.target?e.value>e.target:e.percent>e.targetPercent;return{"Sub-part":e.key===y.key?`⚠️ ${e.name}`:e.name,Time:(n=e.value,`${Math.round(n)}ms`),"%":(a=e.value,r=c,`${Math.round(a/r*100)}%`),Status:t?"🔴 Over target":"✅ OK"};var a,r,n}),d.map(e=>{const t=Math.max(1,Math.round(e.value/c*40));return{key:e.key,bar:t}}));"█".repeat(f[0].bar),"▓".repeat(f[1].bar),"▒".repeat(f[2].bar),"░".repeat(f[3].bar),t.forEach(e=>performance.clearMeasures(e.name)),d.forEach(e=>{const t={ttfb:0,loadDelay:i,loadTime:m,renderDelay:u};performance.measure(e.name,{start:t[e.key],end:t[e.key]+e.value})})});r.observe({type:"largest-contentful-paint",buffered:!0});const n=performance.getEntriesByType("largest-contentful-paint").at(-1);if(!n)return{script:"LCP-Sub-Parts",status:"error",error:"No LCP entries yet"};const s=a();if(!s)return{script:"LCP-Sub-Parts",status:"error",error:"No navigation entry"};const o=performance.getEntriesByType("resource").find(e=>e.name===n.url),l=s.activationStart||0,i=Math.max(0,s.responseStart-l),m=Math.max(i,o?(o.requestStart||o.startTime)-l:0),u=Math.max(m,o?o.responseEnd-l:0),c=Math.max(u,n.startTime-l),p=Math.round(c),d=(e(p),Math.round(i)),y=Math.round(m-i),f=Math.round(u-m),g=Math.round(c-u);[{key:"ttfb",value:d},{key:"resourceLoadDelay",value:y},{key:"resourceLoadTime",value:f},{key:"elementRenderDelay",value:g}].reduce((e,t)=>e.value>t.value?e:t);let h=null;if(n.element){const e=n.element;if(h=e.tagName.toLowerCase(),e.id)h=`#${e.id}`;else if(e.className&&"string"==typeof e.className){const t=e.className.trim().split(/\s+/).slice(0,2).join(".");t&&(h=`${e.tagName.toLowerCase()}.${t}`)}}n.url&&n.url.split("/").pop()?.split("?"),Math.round(d/p*100),Math.round(y/p*100),Math.round(f/p*100),Math.round(g/p*100)})();
```

### scripts/LCP-Trail.js

```javascript
// snippets/CoreWebVitals/LCP-Trail.js | sha256:ba3b9a7b4ff63567 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/CoreWebVitals/LCP-Trail.js
(()=>{const e=[{color:"#EF4444",name:"Red"},{color:"#F97316",name:"Orange"},{color:"#22C55E",name:"Green"},{color:"#3B82F6",name:"Blue"},{color:"#A855F7",name:"Purple"},{color:"#EC4899",name:"Pink"}],t=e=>e<=2500?"good":e<=4e3?"needs-improvement":"poor",r=()=>{const e=performance.getEntriesByType("navigation")[0];return e?.activationStart||0},n=e=>{if(e.id)return`#${e.id}`;if(e.className&&"string"==typeof e.className){const t=e.className.trim().split(/\s+/).slice(0,2).join(".");if(t)return`${e.tagName.toLowerCase()}.${t}`}return e.tagName.toLowerCase()},o=(e,t)=>{const r=e.tagName.toLowerCase();return"img"===r?{type:"Image",url:t.url||e.src}:"video"===r?{type:"Video poster",url:t.url||e.poster}:e.style?.backgroundImage?{type:"Background image",url:t.url}:{type:"h1"===r||"p"===r?"Text block":r}},s=[];new PerformanceObserver(a=>{const l=r(),i=new Set(s.map(e=>e.element));for(const t of a.getEntries()){const{element:r}=t;if(!r||i.has(r))continue;const{color:o,name:a}=e[s.length%e.length];r.style.outline=`3px dashed ${o}`,r.style.outlineOffset="2px",s.push({index:s.length+1,element:r,selector:n(r),color:o,name:a,time:Math.max(0,t.startTime-l),entry:t}),i.add(r)}(()=>{const e=s[s.length-1];e&&(t(e.time),o(e.element,e.entry),s.forEach(({})=>{}))})()}).observe({type:"largest-contentful-paint",buffered:!0});const a=performance.getEntriesByType("largest-contentful-paint");if(0===a.length)return{script:"LCP-Trail",status:"error",error:"No LCP entries yet"};const l=r(),i=new Set,c=[];for(const e of a){const t=e.element;if(!t||i.has(t))continue;i.add(t);const r=n(t),s=Math.round(Math.max(0,e.startTime-l)),{type:a,url:m}=o(t,e);c.push({index:c.length+1,selector:r,time:s,elementType:a,...m?{url:m.split("/").pop()?.split("?")[0]||m}:{}})}if(0===c.length)return{script:"LCP-Trail",status:"error",error:"No LCP elements in DOM"};const m=c.at(-1);t(m.time)})();
```

### scripts/LCP-Video-Candidate.js

```javascript
// snippets/CoreWebVitals/LCP-Video-Candidate.js | sha256:1df9db0d6cd61f13 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/CoreWebVitals/LCP-Video-Candidate.js
(()=>{const e=performance.getEntriesByType("largest-contentful-paint");if(0===e.length)return{script:"LCP-Video-Candidate",status:"error",error:"No LCP entries found"};const t=e[e.length-1],r=t.element;function i(e){return e<=2500?"good":e<=4e3?"needs-improvement":"poor"}function n(e){try{return new URL(e,location.origin).href}catch{return e}}if(!r||"VIDEO"!==r.tagName){r&&r.tagName.toLowerCase();const e=i(t.startTime);return{script:"LCP-Video-Candidate",status:"ok",metric:"LCP",value:Math.round(t.startTime),unit:"ms",rating:e,thresholds:{good:2500,needsImprovement:4e3},details:{isVideo:!1},issues:[]}}const o=r.getAttribute("poster")||"",s=o?n(o):"",a=t.url||"",u=(i(t.startTime),function(e){if(!e)return"unknown";const t=e.toLowerCase().split("?")[0].match(/\.(avif|webp|jxl|png|gif|jpg|jpeg|svg)(?:[?#]|$)/);return t?"jpeg"===t[1]?"jpg":t[1]:"unknown"}(a||s)),g=["avif","webp","jxl"].includes(u),l=0===t.renderTime&&t.loadTime>0,d=Array.from(document.querySelectorAll('link[rel="preload"][as="image"]')).find(e=>{const t=e.getAttribute("href");if(!t)return!1;try{return n(t)===s||n(t)===a}catch{return!1}})??null,m=r.getAttribute("preload"),p=r.hasAttribute("autoplay"),h=(r.hasAttribute("muted"),r.hasAttribute("playsinline"),[]);o||h.push({s:"error",msg:"No poster attribute — the browser has no image to use as LCP candidate"}),o&&!d?h.push({s:"warning",msg:'No <link rel="preload" as="image"> for the poster — browser discovers it late'}):d&&"high"!==d.getAttribute("fetchpriority")&&h.push({s:"info",msg:'Preload found but missing fetchpriority="high" — may be deprioritised'}),o&&!g&&"unknown"!==u&&h.push({s:"info",msg:`Poster uses ${u} — AVIF or WebP would reduce file size and LCP load time`}),l&&h.push({s:"info",msg:"renderTime is 0 — poster is cross-origin and the server does not send Timing-Allow-Origin"}),p||"none"!==m||h.push({s:"warning",msg:'preload="none" on a non-autoplay video may delay poster image loading in some browsers'}),d&&d.getAttribute("fetchpriority"),h.length>0&&(h.filter(e=>"error"===e.s),h.filter(e=>"warning"===e.s),h.filter(e=>"info"===e.s),h.forEach(e=>{})),Math.round(t.startTime),d?.getAttribute("fetchpriority"),h.map(e=>({severity:"error"===e.s?"error":"warning"===e.s?"warning":"info",message:e.msg}))})();
```

### scripts/LCP.js

```javascript
// snippets/CoreWebVitals/LCP.js | sha256:5982d7648dc0db34 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/CoreWebVitals/LCP.js
(()=>{const e=e=>e<=2500?"good":e<=4e3?"needs-improvement":"poor",t=()=>{const e=performance.getEntriesByType("navigation")[0];return e?.activationStart||0};new PerformanceObserver(a=>{const s=a.getEntries(),o=s[s.length-1];if(!o)return;const r=t(),n=Math.max(0,o.startTime-r),i=(e(n),o.element);if(i){let e=i.tagName.toLowerCase();if(i.id)e=`#${i.id}`;else if(i.className&&"string"==typeof i.className){const t=i.className.trim().split(/\s+/).slice(0,2).join(".");t&&(e=`${i.tagName.toLowerCase()}.${t}`)}i.tagName.toLowerCase(),i.style.outline="3px dashed lime",i.style.outlineOffset="2px"}}).observe({type:"largest-contentful-paint",buffered:!0});const a=performance.getEntriesByType("largest-contentful-paint").at(-1);if(!a)return{script:"LCP",status:"error",error:"No LCP entries yet"};const s=t(),o=Math.round(Math.max(0,a.startTime-s)),r=(e(o),a.element);let n=null,i=null;if(r){if(n=r.tagName.toLowerCase(),r.id)n=`#${r.id}`;else if(r.className&&"string"==typeof r.className){const e=r.className.trim().split(/\s+/).slice(0,2).join(".");e&&(n=`${r.tagName.toLowerCase()}.${e}`)}const e=r.tagName.toLowerCase();i="img"===e?"Image":"video"===e?"Video poster":r.style?.backgroundImage?"Background image":"h1"===e||"p"===e?"Text block":e}})();
```

### references/snippets.md

```markdown
---
## Cumulative Layout Shift (CLS)

Quick check for Cumulative Layout Shift, a Core Web Vital that measures visual stability. CLS tracks how much the page layout shifts unexpectedly during its lifetime, providing a single score that represents the cumulative impact of all unexpected layout shifts.

**Script:** `scripts/CLS.js`

**Thresholds:**

| Rating | Score | Meaning |
|--------|-------|---------|
| 🟢 Good | ≤ 0.1 | Stable, minimal shifting |
| 🟡 Needs Improvement | ≤ 0.25 | Noticeable shifting |
| 🔴 Poor | > 0.25 | Significant layout instability |
---
## Interaction to Next Paint (INP)

Tracks Interaction to Next Paint, a Core Web Vital that measures responsiveness. INP evaluates how quickly a page responds to user interactions throughout the entire page visit, replacing First Input Delay (FID) as a Core Web Vital in March 2024.

**Script:** `scripts/INP.js`

**Thresholds:**

| Rating | Time | Meaning |
|--------|------|---------|
| 🟢 Good | ≤ 200ms | Responsive, feels instant |
| 🟡 Needs Improvement | ≤ 500ms | Noticeable delay |
| 🔴 Poor | > 500ms | Slow, frustrating experience |
---
## LCP Image Entropy

Checks if images qualify as LCP candidates based on their entropy (bits per pixel). Since Chrome 112, low-entropy images are ignored for LCP measurement.

**Script:** `scripts/LCP-Image-Entropy.js`

**Thresholds:**

| BPP | Entropy | LCP Eligible | Example |
|-----|---------|--------------|---------|
| < 0.05 | 🔴 Low | ❌ No | Solid colors, simple gradients, placeholders |
| ≥ 0.05 | 🟢 Normal | ✅ Yes | Photos, complex graphics |
---
## LCP Sub-Parts

Breaks down Largest Contentful Paint into its four phases to identify optimization opportunities. Understanding which phase is slowest helps you focus your optimization efforts where they'll have the most impact. Based on the Web Vitals Chrome Extension.

**Script:** `scripts/LCP-Sub-Parts.js`
---
## LCP Trail

Tracks every LCP candidate element during page load and highlights each one with a distinct pastel-colored dashed outline — so you can see the full trail from first candidate to final LCP.

**Script:** `scripts/LCP-Trail.js`
---
## LCP Video Candidate

Detects whether the LCP element is a <video> and audits the poster image configuration — the most common source of avoidable LCP delay when video is the hero element.

**Script:** `scripts/LCP-Video-Candidate.js`
---
## Largest Contentful Paint (LCP)

Quick check for Largest Contentful Paint, a Core Web Vital that measures loading performance. LCP marks when the largest content element becomes visible in the viewport.

**Script:** `scripts/LCP.js`

**Thresholds:**

| Rating | Time | Meaning |
|--------|------|---------|
| 🟢 Good | ≤ 2.5s | Fast, content appears quickly |
| 🟡 Needs Improvement | ≤ 4s | Moderate delay |
| 🔴 Poor | > 4s | Slow, users may abandon |

```

### references/schema.md

```markdown
# Script Return Value Schema

All scripts in the skills directory must return a structured JSON object as the IIFE return value. This allows agents using `mcp__chrome-devtools__evaluate_script` to read structured data directly from the return value, rather than parsing human-readable console output.

## Why this matters

`evaluate_script` captures **both** the console output **and** the return value of the evaluated expression. Console output (with `%c` CSS styling, emojis, tables) is meant for humans reading DevTools. The return value is meant for agents.

```
// Agent workflow
result = evaluate_script(scriptCode)  // return value → structured JSON for agent
get_console_message()                  // console output → human debugging only
```

---

## Base Shape

Every script must return an object matching this shape:

```typescript
{
  // Required in all scripts
  script: string;        // Script name, e.g. "LCP", "TTFB", "Script-Loading"
  status: "ok"           // Script ran, has data
       | "tracking"      // Observer active, data accumulates over time
       | "error"         // Failed or no data available
       | "unsupported";  // Browser API not supported

  // Metric scripts (LCP, CLS, INP, TTFB, FCP)
  metric?: string;       // Short metric name: "LCP", "CLS", "INP", "TTFB", "FCP"
  value?: number;        // Always a number, never a formatted string
  unit?: "ms"            // Milliseconds
       | "score"         // Unitless score (CLS)
       | "count"         // Integer count
       | "bytes"         // Raw bytes
       | "bpp"           // Bits per pixel
       | "fps";          // Frames per second
  rating?: "good" | "needs-improvement" | "poor";
  thresholds?: {
    good: number;        // Upper bound for "good"
    needsImprovement: number;  // Upper bound for "needs-improvement"
  };

  // Audit/inspection scripts (render-blocking, images, scripts)
  count?: number;        // Total number of items found
  items?: object[];      // Array of individual findings

  // Script-specific structured data
  details?: object;

  // Issues detected (for audit scripts)
  issues?: Array<{
    severity: "error" | "warning" | "info";
    message: string;
  }>;

  // Tracking scripts (status: "tracking")
  message?: string;      // Human-readable status message
  getDataFn?: string;    // window function name to call for data: evaluate_script(`${getDataFn}()`)

  // Error info (status: "error" or "unsupported")
  error?: string;
}
```

---

## Execution Patterns

### Pattern 1: Fully synchronous

Scripts that read DOM or `performance.getEntriesByType()` directly. Return JSON at the end of the IIFE.

```js
// Example: TTFB.js
(() => {
  const [nav] = performance.getEntriesByType("navigation");
  if (!nav) return { script: "TTFB", status: "error", error: "No navigation entry" };

  const value = Math.round(nav.responseStart);
  const rating = value <= 800 ? "good" : value <= 1800 ? "needs-improvement" : "poor";

  // Human output
  console.log(`TTFB: ${value}ms (${rating})`);

  // Agent output
  return { script: "TTFB", status: "ok", metric: "TTFB", value, unit: "ms", rating,
           thresholds: { good: 800, needsImprovement: 1800 } };
})();
```

**Scripts using this pattern:** TTFB, TTFB-Sub-Parts, FCP, Find-render-blocking-resources, Script-Loading, LCP-Video-Candidate, Resource-Hints, Resource-Hints-Validation, Priority-Hints-Audit, Validate-Preload-Async-Defer-Scripts, Fonts-Preloaded, Service-Worker-Analysis, Back-Forward-Cache, Content-Visibility, Critical-CSS-Detection, Inline-CSS-Info-and-Size, Inline-Script-Info-and-Size, First-And-Third-Party-Script-Info, First-And-Third-Party-Script-Timings, JS-Execution-Time-Breakdown, CSS-Media-Queries-Analysis, Client-Side-Redirect-Detection, SSR-Hydration-Data-Analysis, Network-Bandwidth-Connection-Quality, Find-Above-The-Fold-Lazy-Loaded-Images, Find-Images-With-Lazy-and-Fetchpriority, Find-non-Lazy-Loaded-Images-outside-of-the-viewport, SVG-Embedded-Bitmap-Analysis, Prefetch-Resource-Validation, TTFB-Resources.

### Pattern 2: PerformanceObserver → getEntriesByType

Scripts using `PerformanceObserver` with `buffered: true` can read the same data synchronously via `performance.getEntriesByType()`. The observer stays for human console display; the return value is computed synchronously.

```js
// Example: LCP.js
(() => {
  // Synchronous data for agent (computed at top)
  const entries = performance.getEntriesByType("largest-contentful-paint");
  const lastEntry = entries.at(-1);
  if (!lastEntry) {
    // Still set up the observer for human display
    // observer.observe(...)
    return { script: "LCP", status: "error", error: "No LCP entries yet" };
  }

  const activationStart = performance.getEntriesByType("navigation")[0]?.activationStart ?? 0;
  const value = Math.round(Math.max(0, lastEntry.startTime - activationStart));
  const rating = value <= 2500 ? "good" : value <= 4000 ? "needs-improvement" : "poor";

  // Human output via PerformanceObserver (unchanged)
  const observer = new PerformanceObserver(...);
  observer.observe({ type: "largest-contentful-paint", buffered: true });

  // Agent return value
  return {
    script: "LCP", status: "ok", metric: "LCP", value, unit: "ms", rating,
    thresholds: { good: 2500, needsImprovement: 4000 },
    details: { element: selector, elementType: type, url: lastEntry.url, sizePixels: lastEntry.size }
  };
})();
```

**Scripts using this pattern:** LCP, CLS, LCP-Sub-Parts, LCP-Trail, LCP-Image-Entropy, Event-Processing-Time, Long-Animation-Frames (buffered LoAFs), LongTask (buffered tasks).

### Pattern 3: Tracking observers

Scripts that observe ongoing user interactions cannot return meaningful data synchronously. They return `status: "tracking"` immediately, and expose a `window.getXxx()` function for agents to call later.

```js
// Return at the end of the IIFE:
return {
  script: "INP",
  status: "tracking",
  message: "INP tracking active. Interact with the page then call getINP() for results.",
  getDataFn: "getINP"
};
```

**Agent workflow for tracking scripts:**
```
1. evaluate_script(INP.js)          → { status: "tracking", getDataFn: "getINP" }
2. (user interacts with the page)
3. evaluate_script("getINP()")      → { script: "INP", status: "ok", value: 350, rating: "needs-improvement", ... }
```

**The window function must also return a structured object** matching the same schema.

**Scripts using this pattern:** INP, Interactions, Input-Latency-Breakdown, Layout-Shift-Loading-and-Interaction, Scroll-Performance, Long-Animation-Frames (ongoing tracking), LongTask (ongoing tracking), Long-Animation-Frames-Script-Attribution.

### Pattern 4: Async scripts

Scripts that use `async/await` or `setTimeout`. The IIFE returns a Promise, which `evaluate_script` can await (Chrome DevTools `awaitPromise`).

Keep the existing `async () => {}` wrapper. Add a `return` statement with structured data at the end. The agent receives the resolved value.

**Scripts using this pattern:** Image-Element-Audit (fetches content-type headers), Video-Element-Audit, Long-Animation-Frames-Script-Attribution (should be converted to return buffered data immediately instead of waiting 10s).

---

## Script-Specific Schemas

### Core Web Vitals

#### LCP
```json
{
  "script": "LCP",
  "status": "ok",
  "metric": "LCP",
  "value": 1240,
  "unit": "ms",
  "rating": "good",
  "thresholds": { "good": 2500, "needsImprovement": 4000 },
  "details": {
    "element": "img.hero",
    "elementType": "Image",
    "url": "https://example.com/hero.jpg",
    "sizePixels": 756000
  }
}
```

#### CLS
```json
{
  "script": "CLS",
  "status": "ok",
  "metric": "CLS",
  "value": 0.05,
  "unit": "score",
  "rating": "good",
  "thresholds": { "good": 0.1, "needsImprovement": 0.25 }
}
```

#### INP (tracking)
```json
{
  "script": "INP",
  "status": "tracking",
  "message": "INP tracking active. Interact with the page then call getINP() for results.",
  "getDataFn": "getINP"
}
```
`getINP()` returns:
```json
{
  "script": "INP",
  "status": "ok",
  "metric": "INP",
  "value": 350,
  "unit": "ms",
  "rating": "needs-improvement",
  "thresholds": { "good": 200, "needsImprovement": 500 },
  "details": {
    "totalInteractions": 5,
    "worstEvent": "click -> button.submit",
    "phases": { "inputDelay": 120, "processingTime": 180, "presentationDelay": 50 }
  }
}
```

#### LCP-Sub-Parts
```json
{
  "script": "LCP-Sub-Parts",
  "status": "ok",
  "metric": "LCP",
  "value": 2100,
  "unit": "ms",
  "rating": "needs-improvement",
  "thresholds": { "good": 2500, "needsImprovement": 4000 },
  "details": {
    "element": "img.hero",
    "url": "hero.jpg",
    "subParts": {
      "ttfb": { "value": 450, "percent": 21, "overTarget": false },
      "resourceLoadDelay": { "value": 120, "percent": 6, "overTarget": false },
      "resourceLoadTime": { "value": 1200, "percent": 57, "overTarget": true },
      "elementRenderDelay": { "value": 330, "percent": 16, "overTarget": true }
    },
    "slowestPhase": "resourceLoadTime"
  }
}
```

#### LCP-Trail
```json
{
  "script": "LCP-Trail",
  "status": "ok",
  "metric": "LCP",
  "value": 1240,
  "unit": "ms",
  "rating": "good",
  "thresholds": { "good": 2500, "needsImprovement": 4000 },
  "details": {
    "candidateCount": 2,
    "finalElement": "img.hero",
    "candidates": [
      { "index": 1, "selector": "h1", "time": 800, "elementType": "Text block" },
      { "index": 2, "selector": "img.hero", "time": 1240, "elementType": "Image", "url": "hero.jpg" }
    ]
  }
}
```

#### LCP-Image-Entropy
```json
{
  "script": "LCP-Image-Entropy",
  "status": "ok",
  "count": 5,
  "details": {
    "totalImages": 5,
    "lowEntropyCount": 1,
    "lcpImageEligible": true,
    "lcpImage": {
      "url": "hero.jpg",
      "bpp": 1.65,
      "isLowEntropy": false
    }
  },
  "items": [
    { "url": "hero.jpg", "width": 1200, "height": 630, "fileSizeBytes": 156000, "bpp": 1.65, "isLowEntropy": false, "lcpEligible": true, "isLCP": true }
  ],
  "issues": []
}
```

#### LCP-Video-Candidate
```json
{
  "script": "LCP-Video-Candidate",
  "status": "ok",
  "metric": "LCP",
  "value": 1800,
  "unit": "ms",
  "rating": "good",
  "thresholds": { "good": 2500, "needsImprovement": 4000 },
  "details": {
    "isVideo": true,
    "posterUrl": "https://example.com/hero.avif",
    "posterFormat": "avif",
    "posterPreloaded": true,
    "fetchpriorityOnPreload": "high",
    "isCrossOrigin": false,
    "videoAttributes": { "autoplay": true, "muted": true, "playsinline": true, "preload": "auto" }
  },
  "issues": []
}
```

### Loading

#### TTFB
```json
{
  "script": "TTFB",
  "status": "ok",
  "metric": "TTFB",
  "value": 245,
  "unit": "ms",
  "rating": "good",
  "thresholds": { "good": 800, "needsImprovement": 1800 }
}
```

#### TTFB-Sub-Parts
```json
{
  "script": "TTFB-Sub-Parts",
  "status": "ok",
  "metric": "TTFB",
  "value": 245,
  "unit": "ms",
  "rating": "good",
  "thresholds": { "good": 800, "needsImprovement": 1800 },
  "details": {
    "subParts": {
      "redirectWait": { "value": 0, "unit": "ms" },
      "serviceWorkerCache": { "value": 0, "unit": "ms" },
      "dnsLookup": { "value": 5, "unit": "ms" },
      "tcpConnection": { "value": 30, "unit": "ms" },
      "sslTls": { "value": 45, "unit": "ms" },
      "serverResponse": { "value": 165, "unit": "ms" }
    },
    "slowestPhase": "serverResponse"
  }
}
```

#### Find-render-blocking-resources
```json
{
  "script": "Find-render-blocking-resources",
  "status": "ok",
  "count": 3,
  "details": {
    "totalBlockingUntilMs": 450,
    "totalSizeBytes": 135000,
    "byType": { "link": 2, "script": 1 }
  },
  "items": [
    { "type": "link", "url": "https://example.com/style.css", "shortName": "style.css", "responseEndMs": 450, "durationMs": 200, "sizeBytes": 45000 }
  ]
}
```

#### Script-Loading
```json
{
  "script": "Script-Loading",
  "status": "ok",
  "count": 8,
  "rating": "needs-improvement",
  "details": {
    "totalSizeBytes": 245000,
    "byStrategy": { "blocking": 2, "async": 4, "defer": 1, "module": 1 },
    "byParty": { "firstParty": 5, "thirdParty": 3 },
    "thirdPartyBlockingCount": 1
  },
  "items": [
    { "url": "https://example.com/app.js", "shortName": "app.js", "strategy": "blocking", "location": "head", "party": "first", "sizeBytes": 85000, "durationMs": 120 }
  ],
  "issues": [
    { "severity": "error", "message": "2 blocking scripts in <head>" },
    { "severity": "error", "message": "1 third-party blocking script" }
  ]
}
```

### Interaction

#### Interactions (tracking)
```json
{
  "script": "Interactions",
  "status": "tracking",
  "message": "Tracking interactions. Interact with the page then call getInteractionSummary() for results.",
  "getDataFn": "getInteractionSummary"
}
```

#### Input-Latency-Breakdown (tracking)
```json
{
  "script": "Input-Latency-Breakdown",
  "status": "tracking",
  "message": "Tracking input latency by event type. Interact with the page then call getInputLatencyBreakdown().",
  "getDataFn": "getInputLatencyBreakdown"
}
```

#### Layout-Shift-Loading-and-Interaction
Immediately returns buffered CLS data, plus exposes summary function for ongoing tracking.
```json
{
  "script": "Layout-Shift-Loading-and-Interaction",
  "status": "tracking",
  "metric": "CLS",
  "value": 0.08,
  "unit": "score",
  "rating": "good",
  "thresholds": { "good": 0.1, "needsImprovement": 0.25 },
  "details": {
    "currentCLS": 0.08,
    "shiftCount": 3,
    "countedShifts": 3,
    "excludedShifts": 0
  },
  "message": "Layout shift tracking active. Call getLayoutShiftSummary() for full element attribution.",
  "getDataFn": "getLayoutShiftSummary"
}
```

#### Long-Animation-Frames
Returns buffered LoAF data immediately. Ongoing tracking continues.
```json
{
  "script": "Long-Animation-Frames",
  "status": "tracking",
  "count": 3,
  "details": {
    "totalLoAFs": 3,
    "withBlockingTime": 2,
    "totalBlockingTimeMs": 280,
    "worstBlockingMs": 180
  },
  "message": "Tracking long animation frames. Call getLoAFSummary() for full script attribution.",
  "getDataFn": "getLoAFSummary"
}
```

#### Long-Animation-Frames-Script-Attribution
Returns buffered LoAF data immediately (do not wait for a timer):
```json
{
  "script": "Long-Animation-Frames-Script-Attribution",
  "status": "ok",
  "details": {
    "frameCount": 5,
    "totalBlockingMs": 420,
    "byCategory": {
      "first-party": { "durationMs": 180, "count": 3 },
      "third-party": { "durationMs": 210, "count": 2 },
      "framework": { "durationMs": 30, "count": 1 }
    }
  },
  "items": [
    { "file": "app.js", "category": "first-party", "durationMs": 180, "count": 3 }
  ]
}
```

#### Scroll-Performance (tracking)
```json
{
  "script": "Scroll-Performance",
  "status": "tracking",
  "details": {
    "nonPassiveListeners": 2,
    "cssAudit": {
      "smoothScrollElements": 1,
      "willChangeElements": 0,
      "contentVisibilityElements": 3
    }
  },
  "message": "Scroll performance tracking active. Scroll the page then call getScrollSummary() for FPS data.",
  "getDataFn": "getScrollSummary"
}
```

#### LongTask
Returns buffered long tasks immediately. Ongoing tracking continues.
```json
{
  "script": "LongTask",
  "status": "tracking",
  "count": 4,
  "details": {
    "totalBlockingTimeMs": 380,
    "worstTaskMs": 220,
    "bySeverity": { "critical": 1, "high": 1, "medium": 2, "low": 0 }
  },
  "message": "Tracking long tasks. Call getLongTaskSummary() for statistics.",
  "getDataFn": "getLongTaskSummary"
}
```

### Media

#### Image-Element-Audit (async)
```json
{
  "script": "Image-Element-Audit",
  "status": "ok",
  "count": 8,
  "details": {
    "totalImages": 8,
    "inViewport": 3,
    "offViewport": 5,
    "totalErrors": 2,
    "totalWarnings": 3,
    "totalInfos": 1,
    "lcpCandidate": {
      "selector": "img.hero",
      "format": "avif",
      "fetchpriority": "high",
      "loading": "(not set)",
      "preloaded": true
    }
  },
  "items": [
    {
      "selector": "img.hero",
      "url": "hero.avif",
      "format": "avif",
      "inViewport": true,
      "isLCP": true,
      "loading": "(not set)",
      "decoding": "sync",
      "fetchpriority": "high",
      "hasDimensions": true,
      "hasSrcset": false,
      "hasSizes": false,
      "inPicture": false,
      "issues": []
    }
  ],
  "issues": [
    { "severity": "warning", "message": "img.thumbnail: Missing width/height attributes (CLS risk)" }
  ]
}
```

#### Video-Element-Audit
Same shape as Image-Element-Audit but for video elements.

#### SVG-Embedded-Bitmap-Analysis
```json
{
  "script": "SVG-Embedded-Bitmap-Analysis",
  "status": "ok",
  "count": 2,
  "items": [
    { "url": "icon.svg", "hasBitmap": true, "bitmapType": "image/png", "sizeBytes": 4500 }
  ],
  "issues": [
    { "severity": "warning", "message": "2 SVG files contain embedded bitmaps" }
  ]
}
```

### Resources

#### Network-Bandwidth-Connection-Quality
```json
{
  "script": "Network-Bandwidth-Connection-Quality",
  "status": "ok",
  "details": {
    "effectiveType": "4g",
    "downlink": 10,
    "rtt": 50,
    "saveData": false
  }
}
```

---

## Guidelines for Agents

### Reading results

```
// Prefer return value over console output
result = evaluate_script(scriptCode)
if result.status == "ok" → use result.value, result.rating, result.details, result.items
if result.status == "tracking" → call evaluate_script(`${result.getDataFn}()`) after user interaction
if result.status == "error" → check result.error, the browser may not have loaded the page yet
if result.status == "unsupported" → browser does not support the required API (check: Chrome 107+?)
```

### Tracking scripts workflow

```
// 1. Start tracking
result = evaluate_script(INP_js)
// result = { status: "tracking", getDataFn: "getINP" }

// 2. Wait for/trigger user interactions

// 3. Collect data
data = evaluate_script("getINP()")
// data = { status: "ok", value: 350, rating: "needs-improvement", ... }
```

### Making decisions from return values

- `rating === "good"` → no action needed for this metric
- `rating === "needs-improvement"` → investigate, check `details` and `issues`
- `rating === "poor"` → high priority fix, check `issues` for specific problems
- `count > 0` and `issues.length > 0` → audit found actionable problems
- `count === 0` → nothing to audit (no render-blocking resources, no images, etc.)

---

## Implementation Rules

1. **Numbers are numbers** — never `"245ms"`, always `245`. The agent formats as needed.
2. **Consistent field names** — `value` for the metric, `unit` for its unit, `rating` for the threshold assessment.
3. **Issues are actionable** — each issue message describes what to fix, not what was found.
4. **Items are homogeneous** — all objects in `items[]` have the same fields.
5. **No DOM references in return value** — elements can't be serialized to JSON.
6. **Keep console output unchanged** — the return value is additive, not a replacement.
7. **Window functions match the schema** — `getINP()`, `getLoAFSummary()`, etc. return the same structured shape.

```

webperf-core-web-vitals | SkillHub