Skip to content

Commit 7773e96

Browse files
committed
feat: add webperf-core-web-vitals references (snippets and schema)
1 parent 4466cb1 commit 7773e96

2 files changed

Lines changed: 258 additions & 0 deletions

File tree

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# Script Return Value Schema
2+
3+
All scripts return a structured JSON object as the IIFE return value. This allows agents using `evaluate_script` to read structured data directly from the return value, rather than parsing human-readable console output.
4+
5+
## Base Shape
6+
7+
```typescript
8+
{
9+
script: string; // Script name, e.g. "LCP", "CLS", "INP"
10+
status: "ok" // Script ran, has data
11+
| "tracking" // Observer active, data accumulates over time
12+
| "error" // Failed or no data available
13+
| "unsupported"; // Browser API not supported
14+
15+
// Metric scripts (LCP, CLS, INP)
16+
metric?: string;
17+
value?: number; // Always a number, never a formatted string
18+
unit?: "ms" | "score" | "count" | "bytes" | "bpp" | "fps";
19+
rating?: "good" | "needs-improvement" | "poor";
20+
thresholds?: { good: number; needsImprovement: number };
21+
22+
// Audit scripts
23+
count?: number;
24+
items?: object[];
25+
details?: object;
26+
issues?: Array<{ severity: "error" | "warning" | "info"; message: string }>;
27+
28+
// Tracking scripts
29+
message?: string;
30+
getDataFn?: string; // window function name: evaluate_script(`${getDataFn}()`)
31+
32+
// Error info
33+
error?: string;
34+
}
35+
```
36+
37+
## Agent Workflow
38+
39+
```
40+
// Synchronous scripts (LCP, CLS, LCP-Sub-Parts, LCP-Trail, LCP-Image-Entropy, LCP-Video-Candidate)
41+
result = evaluate_script(scriptCode)
42+
// → { status: "ok", value: 1240, rating: "good", ... }
43+
44+
// Tracking scripts (INP)
45+
result = evaluate_script(INP_js)
46+
// → { status: "tracking", getDataFn: "getINP" }
47+
// (user interacts with the page)
48+
data = evaluate_script("getINP()")
49+
// → { status: "ok", value: 350, rating: "needs-improvement", ... }
50+
51+
// CLS (hybrid: returns current value immediately, keeps tracking)
52+
result = evaluate_script(CLS_js)
53+
// → { status: "ok", value: 0.05, rating: "good", message: "Call getCLS() for updated value" }
54+
// (after more page interactions)
55+
data = evaluate_script("getCLS()")
56+
// → { status: "ok", value: 0.08, rating: "good", ... }
57+
```
58+
59+
## Making Decisions from Return Values
60+
61+
- `rating === "good"` → no action needed for this metric
62+
- `rating === "needs-improvement"` → investigate, check `details` and `issues`
63+
- `rating === "poor"` → high priority fix, check `issues` for specific problems
64+
- `status === "error"` → page may not have loaded yet, or metric has no data
65+
- `status === "tracking"` → call `evaluate_script(result.getDataFn + "()")` after interaction
66+
67+
## Script-Specific Schemas
68+
69+
### LCP
70+
```json
71+
{
72+
"script": "LCP", "status": "ok", "metric": "LCP",
73+
"value": 1240, "unit": "ms", "rating": "good",
74+
"thresholds": { "good": 2500, "needsImprovement": 4000 },
75+
"details": { "element": "img.hero", "elementType": "Image", "url": "hero.jpg", "sizePixels": 756000 }
76+
}
77+
```
78+
79+
### CLS
80+
```json
81+
{
82+
"script": "CLS", "status": "ok", "metric": "CLS",
83+
"value": 0.05, "unit": "score", "rating": "good",
84+
"thresholds": { "good": 0.1, "needsImprovement": 0.25 },
85+
"message": "CLS tracking active. Call getCLS() for updated value after page interactions."
86+
}
87+
```
88+
89+
### INP (initial — tracking)
90+
```json
91+
{ "script": "INP", "status": "tracking", "message": "INP tracking active. Interact with the page then call getINP() for results.", "getDataFn": "getINP" }
92+
```
93+
`getINP()` returns:
94+
```json
95+
{
96+
"script": "INP", "status": "ok", "metric": "INP",
97+
"value": 350, "unit": "ms", "rating": "needs-improvement",
98+
"thresholds": { "good": 200, "needsImprovement": 500 },
99+
"details": { "totalInteractions": 5, "worstEvent": "click → button.submit", "phases": { "inputDelay": 120, "processingTime": 180, "presentationDelay": 50 } }
100+
}
101+
```
102+
103+
### LCP-Sub-Parts
104+
```json
105+
{
106+
"script": "LCP-Sub-Parts", "status": "ok", "metric": "LCP",
107+
"value": 2100, "unit": "ms", "rating": "needs-improvement",
108+
"thresholds": { "good": 2500, "needsImprovement": 4000 },
109+
"details": {
110+
"element": "img.hero", "url": "hero.jpg",
111+
"subParts": {
112+
"ttfb": { "value": 450, "percent": 21, "overTarget": false },
113+
"resourceLoadDelay": { "value": 120, "percent": 6, "overTarget": false },
114+
"resourceLoadTime": { "value": 1200, "percent": 57, "overTarget": true },
115+
"elementRenderDelay": { "value": 330, "percent": 16, "overTarget": true }
116+
},
117+
"slowestPhase": "resourceLoadTime"
118+
}
119+
}
120+
```
121+
122+
### LCP-Trail
123+
```json
124+
{
125+
"script": "LCP-Trail", "status": "ok", "metric": "LCP",
126+
"value": 1240, "unit": "ms", "rating": "good",
127+
"thresholds": { "good": 2500, "needsImprovement": 4000 },
128+
"details": {
129+
"candidateCount": 2, "finalElement": "img.hero",
130+
"candidates": [
131+
{ "index": 1, "selector": "h1", "time": 800, "elementType": "Text block" },
132+
{ "index": 2, "selector": "img.hero", "time": 1240, "elementType": "Image", "url": "hero.jpg" }
133+
]
134+
}
135+
}
136+
```
137+
138+
### LCP-Image-Entropy
139+
```json
140+
{
141+
"script": "LCP-Image-Entropy", "status": "ok", "count": 5,
142+
"details": { "totalImages": 5, "lowEntropyCount": 1, "lcpImageEligible": true, "lcpImage": { "url": "hero.jpg", "bpp": 1.65, "isLowEntropy": false } },
143+
"items": [
144+
{ "url": "hero.jpg", "width": 1200, "height": 630, "fileSizeBytes": 156000, "bpp": 1.65, "isLowEntropy": false, "lcpEligible": true, "isLCP": true }
145+
],
146+
"issues": []
147+
}
148+
```
149+
150+
### LCP-Video-Candidate
151+
```json
152+
{
153+
"script": "LCP-Video-Candidate", "status": "ok", "metric": "LCP",
154+
"value": 1800, "unit": "ms", "rating": "good",
155+
"thresholds": { "good": 2500, "needsImprovement": 4000 },
156+
"details": {
157+
"isVideo": true, "posterUrl": "https://example.com/hero.avif", "posterFormat": "avif",
158+
"posterPreloaded": true, "fetchpriorityOnPreload": "high", "isCrossOrigin": false,
159+
"videoAttributes": { "autoplay": true, "muted": true, "playsinline": true, "preload": "auto" }
160+
},
161+
"issues": []
162+
}
163+
```
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
---
2+
## Largest Contentful Paint (LCP)
3+
4+
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.
5+
6+
**Script:** `scripts/LCP.js`
7+
8+
**Thresholds:**
9+
10+
| Rating | Time | Meaning |
11+
|--------|------|---------|
12+
| 🟢 Good | ≤ 2.5s | Fast, content appears quickly |
13+
| 🟡 Needs Improvement | ≤ 4s | Moderate delay |
14+
| 🔴 Poor | > 4s | Slow, users may abandon |
15+
---
16+
## Cumulative Layout Shift (CLS)
17+
18+
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.
19+
20+
**Script:** `scripts/CLS.js`
21+
22+
**Usage:** Run `CLS.js` once on page load. It returns the current score immediately and keeps tracking. Call `getCLS()` later to get an updated value after further page interactions.
23+
24+
**Thresholds:**
25+
26+
| Rating | Score | Meaning |
27+
|--------|-------|---------|
28+
| 🟢 Good | ≤ 0.1 | Stable, minimal shifting |
29+
| 🟡 Needs Improvement | ≤ 0.25 | Noticeable shifting |
30+
| 🔴 Poor | > 0.25 | Significant layout instability |
31+
---
32+
## Interaction to Next Paint (INP)
33+
34+
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.
35+
36+
**Script:** `scripts/INP.js`
37+
38+
**Usage:** Run `INP.js` once to start tracking. It returns `status: "tracking"` immediately. After the user interacts with the page, call `getINP()` to retrieve the current INP value.
39+
40+
**Thresholds:**
41+
42+
| Rating | Time | Meaning |
43+
|--------|------|---------|
44+
| 🟢 Good | ≤ 200ms | Responsive, feels instant |
45+
| 🟡 Needs Improvement | ≤ 500ms | Noticeable delay |
46+
| 🔴 Poor | > 500ms | Slow, frustrating experience |
47+
---
48+
## LCP Sub-Parts
49+
50+
Breaks down Largest Contentful Paint into its four phases to identify optimization opportunities. Understanding which phase is slowest helps focus optimization efforts where they'll have the most impact.
51+
52+
**Script:** `scripts/LCP-Sub-Parts.js`
53+
54+
**Sub-parts:**
55+
56+
| Phase | Target | Description |
57+
|-------|--------|-------------|
58+
| Time to First Byte (TTFB) | ≤ 800ms | Navigation start → first HTML byte |
59+
| Resource Load Delay | < 10% of LCP | TTFB → browser starts loading LCP resource |
60+
| Resource Load Time | ~40% of LCP | Time to download the LCP resource |
61+
| Element Render Delay | < 10% of LCP | Resource downloaded → LCP element rendered |
62+
---
63+
## LCP Trail
64+
65+
Tracks every LCP candidate element during page load and highlights each one with a distinct colored dashed outline — so you can see the full trail from first candidate to final LCP.
66+
67+
**Script:** `scripts/LCP-Trail.js`
68+
69+
**Returns:** Array of all LCP candidates in order, with selector, time, element type, and URL (if applicable). The last entry is the final LCP element.
70+
---
71+
## LCP Image Entropy
72+
73+
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.
74+
75+
**Script:** `scripts/LCP-Image-Entropy.js`
76+
77+
**Thresholds:**
78+
79+
| BPP | Entropy | LCP Eligible | Example |
80+
|-----|---------|--------------|---------|
81+
| < 0.05 | 🔴 Low | ❌ No | Solid colors, simple gradients, placeholders |
82+
| ≥ 0.05 | 🟢 Normal | ✅ Yes | Photos, complex graphics |
83+
---
84+
## LCP Video Candidate
85+
86+
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.
87+
88+
**Script:** `scripts/LCP-Video-Candidate.js`
89+
90+
**Checks:**
91+
- Whether a `poster` attribute exists
92+
- Whether the poster is preloaded with `<link rel="preload" as="image">`
93+
- Whether `fetchpriority="high"` is set on the preload
94+
- Whether the poster uses a modern format (AVIF, WebP)
95+
- Whether cross-origin timing is obscured (`renderTime = 0`)

0 commit comments

Comments
 (0)