
Webperf Media
- 189 installs
- 1.4k repo stars
- Updated August 2, 2026
- nucliweb/webperf-snippets
Helps with ai & agent building tasks.
About
webperf-media is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- webperf-media
- AI & Agent Building
- AI-coding skill
Webperf Media by the numbers
- 189 all-time installs (skills.sh)
- +4 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #2,965 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/nucliweb/webperf-snippets --skill webperf-mediaAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 189 |
|---|---|
| repo stars | ★ 1.4k |
| Last updated | August 2, 2026 |
| Repository | nucliweb/webperf-snippets ↗ |
What it does
Helps with ai & agent building tasks.
Files
WebPerf: Media Performance
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/Image-Element-Audit.js— Image Element Auditscripts/SVG-Embedded-Bitmap-Analysis.js— SVG Embedded Bitmap Analysisscripts/Video-Element-Audit.js— Video Element Audit
Common Workflows
Complete Media Audit
When the user asks for media optimization or "audit images and videos":
1. Image-Element-Audit.js - Analyze all images (format, lazy loading, sizing, fetchpriority) 2. Video-Element-Audit.js - Analyze all videos (poster, preload, formats, autoplay) 3. SVG-Embedded-Bitmap-Analysis.js - Detect inefficient bitmap images embedded in SVGs
Image Optimization Workflow
When the user asks "optimize images" or "check image performance":
1. Image-Element-Audit.js - Full image audit 2. Cross-reference with webperf-loading skill:
- Find-Above-The-Fold-Lazy-Loaded-Images.js (incorrectly lazy-loaded images)
- Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js (missing lazy loading)
- Find-Images-With-Lazy-and-Fetchpriority.js (contradictory attributes)
- Priority-Hints-Audit.js (LCP image should have fetchpriority="high")
Video Performance Audit
When the user asks "optimize videos" or "check video performance":
1. Video-Element-Audit.js - Full video audit 2. Cross-reference with webperf-core-web-vitals skill:
- LCP-Video-Candidate.js (check if video/poster is LCP)
3. Cross-reference with webperf-loading skill:
- Priority-Hints-Audit.js (video poster priority)
- Resource-Hints-Validation.js (video preload)
LCP Image Investigation
When LCP is an image and needs optimization:
1. Cross-reference with webperf-core-web-vitals skill:
- LCP.js (measure LCP)
- LCP-Image-Entropy.js (analyze image complexity)
2. Image-Element-Audit.js - Check format, dimensions, lazy loading 3. Cross-reference with webperf-loading skill:
- Find-Above-The-Fold-Lazy-Loaded-Images.js (should NOT be lazy)
- Priority-Hints-Audit.js (should have fetchpriority="high")
- Resource-Hints-Validation.js (consider preload)
Layout Shift from Images
When CLS is caused by images without dimensions:
1. Image-Element-Audit.js - Check for missing width/height attributes 2. Cross-reference with webperf-core-web-vitals skill:
- CLS.js (measure total CLS)
3. Cross-reference with webperf-interaction skill:
- Layout-Shift-Loading-and-Interaction.js (when shifts occur)
SVG Optimization Audit
When the user asks about SVG performance or file sizes are large:
1. SVG-Embedded-Bitmap-Analysis.js - Detect raster images embedded in vector SVGs 2. Recommend SVGO optimization for SVGs without embedded bitmaps 3. Recommend extracting bitmaps to separate image files with proper formats
Decision Tree
Use this decision tree to automatically run follow-up snippets based on results:
After Image-Element-Audit.js
- If images missing width/height attributes → Layout shift risk, run:
1. webperf-core-web-vitals:CLS.js (measure CLS impact) 2. webperf-interaction:Layout-Shift-Loading-and-Interaction.js (timing of shifts) 3. Recommend adding explicit dimensions to all images
- If images using wrong format (JPEG for graphics, PNG for photos) → Recommend:
- Modern formats: WebP, AVIF
- Appropriate format for content type
- Format-specific compression settings
- If images much larger than display size → Recommend:
- Responsive images with srcset
- Appropriate image CDN sizing
- srcset with multiple sizes for different viewports
- If above-the-fold images are lazy-loaded → Run:
1. webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js (confirm) 2. webperf-core-web-vitals:LCP.js (measure LCP impact) 3. Recommend removing loading="lazy" from above-fold images
- If LCP image lacks fetchpriority="high" → Run:
1. webperf-core-web-vitals:LCP.js (measure current LCP) 2. webperf-loading:Priority-Hints-Audit.js (full priority audit) 3. Recommend adding fetchpriority="high" to LCP image
- If below-the-fold images are NOT lazy-loaded → Run:
1. webperf-loading:Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js (confirm) 2. Recommend adding loading="lazy" to offscreen images
- If images have both loading="lazy" AND fetchpriority="high" → Run:
1. webperf-loading:Find-Images-With-Lazy-and-Fetchpriority.js (confirm contradiction) 2. Recommend removing one of the conflicting attributes
- If images competing with critical resources → Run:
1. webperf-loading:Find-render-blocking-resources.js (resource priority conflicts) 2. webperf-loading:TTFB-Resources.js (identify slow image CDN)
- If images missing alt text → Accessibility issue, recommend adding descriptive alt text
After Video-Element-Audit.js
- If video is LCP candidate → Run:
1. webperf-core-web-vitals:LCP-Video-Candidate.js (confirm) 2. webperf-core-web-vitals:LCP.js (measure LCP) 3. webperf-core-web-vitals:LCP-Subparts.js (break down timing) 4. Optimize video poster image or consider image alternative
- If video missing poster → Recommend:
- Adding poster image for better perceived performance
- Using first frame or custom thumbnail
- Optimizing poster as you would an image
- If video uses preload="auto" → Bandwidth concern, evaluate:
- Is video above-the-fold? Keep preload="auto"
- Is video below-the-fold? Change to preload="metadata" or "none"
- Is autoplay intended? Verify preload matches intent
- If autoplay video without muted → Browser will block, recommend:
- Adding muted attribute
- Or removing autoplay
- If video missing multiple formats → Recommend:
- WebM for Chrome/Firefox
- MP4 as fallback for Safari
- Order sources by efficiency (WebM first)
- If large video files (>5MB) → Recommend:
- Compression/transcoding
- Adaptive bitrate streaming (HLS, DASH)
- Loading strategy optimization
After SVG-Embedded-Bitmap-Analysis.js
- If bitmap images found in SVGs → Recommend:
1. Extract bitmaps to separate files 2. Use WebP/AVIF format for extracted images 3. Reference images from SVG with <image> element 4. Or convert to pure vector if possible
- If large embedded bitmaps (>100KB) → Critical inefficiency:
- SVG parsing overhead + large bitmap = worst of both worlds
- Urgently recommend extraction
- If multiple small bitmaps in SVG → Consider:
- CSS sprites for small icons
- SVG symbols for reusable graphics
- Extracting to individual optimized images
Performance Budget Thresholds
Use these thresholds to trigger recommendations:
Image File Sizes:
- Warning: Individual image > 500KB → Check format and compression
- Critical: Individual image > 1MB → Urgent optimization needed
- Total images: > 5MB on initial load → Implement lazy loading
Image Formats:
- JPEG for graphics/icons → Recommend PNG or SVG
- PNG for photos → Recommend JPEG, WebP, or AVIF
- GIF for animations → Recommend video (MP4/WebM) or animated WebP
- No modern formats (WebP/AVIF) → Recommend upgrading
Image Dimensions:
- Intrinsic size > 2x display size → Recommend responsive images
- Intrinsic size < display size → Upscaling = blurry, provide larger source
Video File Sizes:
- Warning: Video > 10MB → Consider compression or streaming
- Critical: Video > 50MB → Urgent optimization or streaming needed
Lazy Loading:
- Above-fold images lazy-loaded → Critical LCP impact, fix immediately
- Below-fold images NOT lazy-loaded → Wasted bandwidth, implement lazy loading
- >10 images eager-loaded → Excessive, implement lazy loading
Priority Hints:
- LCP image without fetchpriority="high" → Add for 10-30% LCP improvement
- Non-LCP images with fetchpriority="high" → Remove, wasting browser hints
- Lazy + fetchpriority="high" conflict → Fix contradiction
References
references/snippets.md— Descriptions and thresholds for each scriptreferences/schema.md— Return value schema for interpreting script output
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:
{
// 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.
// 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.
// 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-Subparts, 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.
// 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
{
"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://web.dev/hero.jpg",
"sizePixels": 756000
}
}CLS
Returns buffered CLS immediately and keeps tracking. Always call getCLS() after interactions to get an updated value.
{
"script": "CLS",
"status": "ok",
"metric": "CLS",
"value": 0.05,
"unit": "score",
"rating": "good",
"thresholds": { "good": 0.1, "needsImprovement": 0.25 },
"message": "CLS tracking active. Call getCLS() for updated value after page interactions.",
"getDataFn": "getCLS"
}getCLS() returns the same shape with the latest accumulated value.
INP (tracking)
{
"script": "INP",
"status": "tracking",
"message": "INP tracking active. Interact with the page then call getINP() for results.",
"getDataFn": "getINP"
}getINP() returns:
{
"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 }
}
}If no interactions yet, getINP() returns status: "error" with getDataFn: "getINP" — retry after user interaction.
getINPDetails() returns the full sorted interaction list (array of up to 15 entries). Use when getINP() shows poor INP and you need to identify patterns across multiple slow interactions:
[
{
"formattedName": "click → button.submit",
"duration": 450,
"startTime": 1200,
"phases": { "inputDelay": 120, "processingTime": 280, "presentationDelay": 50 }
}
]LCP-Subparts
{
"script": "LCP-Subparts",
"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
{
"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
{
"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
{
"script": "LCP-Video-Candidate",
"status": "ok",
"metric": "LCP",
"value": 1800,
"unit": "ms",
"rating": "good",
"thresholds": { "good": 2500, "needsImprovement": 4000 },
"details": {
"isVideo": true,
"posterUrl": "https://web.dev/hero.avif",
"posterFormat": "avif",
"posterPreloaded": true,
"fetchpriorityOnPreload": "high",
"isCrossOrigin": false,
"videoAttributes": { "autoplay": true, "muted": true, "playsinline": true, "preload": "auto" }
},
"issues": []
}Loading
TTFB
{
"script": "TTFB",
"status": "ok",
"metric": "TTFB",
"value": 245,
"unit": "ms",
"rating": "good",
"thresholds": { "good": 800, "needsImprovement": 1800 }
}TTFB-Sub-Parts
{
"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
{
"script": "Find-render-blocking-resources",
"status": "ok",
"count": 3,
"details": {
"totalBlockingUntilMs": 450,
"totalSizeBytes": 135000,
"byType": { "link": 2, "script": 1 }
},
"items": [
{
"type": "link",
"url": "https://web.dev/style.css",
"shortName": "style.css",
"responseEndMs": 450,
"durationMs": 200,
"sizeBytes": 45000
}
]
}Script-Loading
{
"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://web.dev/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)
{
"script": "Interactions",
"status": "tracking",
"message": "Tracking interactions. Interact with the page then call getInteractionSummary() for results.",
"getDataFn": "getInteractionSummary"
}Input-Latency-Breakdown (tracking)
{
"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.
{
"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.
{
"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):
{
"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)
{
"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.
{
"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)
{
"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
{
"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
{
"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 metricrating === "needs-improvement"→ investigate, checkdetailsandissuesrating === "poor"→ high priority fix, checkissuesfor specific problemscount > 0andissues.length > 0→ audit found actionable problemscount === 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.
SVG Embedded Bitmap Analysis
Scans all SVG resources on the page — both external files and inline <svg> elements — and flags any that contain embedded bitmap images, reporting name, transfer size, compression encoding, and embedded bitmap details.
Script: scripts/SVG-Embedded-Bitmap-Analysis.js ---
Video Element Audit
Audits all <video> elements on the page against video performance best practices — covering preload strategy, autoplay configuration, format modernisation, CLS prevention, and playback accessibility.
Script: scripts/Video-Element-Audit.js
// snippets/Media/Image-Element-Audit.js | sha256:18880771ce56c1db | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/Media/Image-Element-Audit.js
(async()=>{function t(t){const e=t.getBoundingClientRect();return e.top<window.innerHeight&&e.bottom>0&&e.left<window.innerWidth&&e.right>0&&e.width>0&&e.height>0}function e(t){if(!t)return"unknown";const e=t.toLowerCase(),i=e.split("?")[0],s=e.includes("?")?e.split("?")[1]:"",r=i.match(/\/f_(auto|avif|webp|jxl|png|jpg|jpeg|gif|svg)[,/]/);if(r){const t=r[1];return"auto"===t?"auto (cdn)":"jpeg"===t?"jpg":t}const n=s.match(/(?:^|&)(?:fm|format)=(avif|webp|jxl|png|jpg|jpeg|gif|svg)(?:&|$)/);if(n)return"jpeg"===n[1]?"jpg":n[1];const o=i.match(/\.(avif|webp|jxl|png|gif|svg|jpg|jpeg)(?:[?#]|$)/);return o?"jpeg"===o[1]?"jpg":o[1]:(i.split("/").pop()||"").includes(".")?"unknown":"auto (cdn?)"}function i(t){return t?t.split("/").pop()?.split("?")[0]?.slice(0,40)||t.slice(-40):""}function s(t){try{return new URL(t,location.origin).href}catch{return t}}const r=Array.from(document.querySelectorAll("img"));if(0===r.length)return{script:"Image-Element-Audit",status:"ok",count:0,items:[],issues:[]};const n=function(e){let i=null,s=0;return e.filter(t).forEach(t=>{const{width:e,height:r}=t.getBoundingClientRect(),n=e*r;n>s&&(s=n,i=t)}),i}(r),o=Array.from(document.querySelectorAll('link[rel="preload"][as="image"]')),a=await Promise.all(r.map(t=>async function(t){if(!t)return e(t);try{const e=await fetch(t,{cache:"force-cache"}),i=e.headers.get("content-type")?.split(";")[0]?.trim()||"";if(i.includes("avif"))return"avif";if(i.includes("webp"))return"webp";if(i.includes("jxl"))return"jxl";if(i.includes("png"))return"png";if(i.includes("gif"))return"gif";if(i.includes("svg"))return"svg";if(i.includes("jpeg"))return"jpg"}catch{}return e(t)}(t.currentSrc||t.src))),c=r.map((e,i)=>{const r=t(e),c=e===n,g=e.getBoundingClientRect(),u=e.currentSrc||e.src||"",l=a[i],h="PICTURE"===e.parentElement?.tagName,p=e.getAttribute("loading"),f=e.getAttribute("decoding"),d=e.getAttribute("fetchpriority"),m=e.hasAttribute("width")&&e.hasAttribute("height"),w=[];let y=null;return c?("high"!==d&&w.push({s:"error",msg:'Add fetchpriority="high" to the LCP image'}),"lazy"===p&&w.push({s:"error",msg:'Remove loading="lazy" from the LCP image'}),"sync"!==f&&w.push({s:"warning",msg:'Consider decoding="sync" for the LCP image'}),y=function(t,e){const i=s(t.currentSrc||t.src||"");return e.find(t=>{const e=t.getAttribute("href"),r=t.getAttribute("imagesrcset")||"";return!(!e||s(e)!==i)||!!r&&r.split(",").map(t=>s(t.trim().split(/\s+/)[0])).includes(i)})??null}(e,o),y?"high"!==y.getAttribute("fetchpriority")&&w.push({s:"info",msg:'LCP image preload is missing fetchpriority="high"'}):w.push({s:"warning",msg:'LCP image has no <link rel="preload" as="image">'})):r?"lazy"===p&&w.push({s:"warning",msg:'Remove loading="lazy" (image is above the fold)'}):("lazy"!==p&&w.push({s:"warning",msg:'Add loading="lazy" (image is off-viewport)'}),"high"===d&&w.push({s:"error",msg:'Remove fetchpriority="high" (image is off-viewport)'})),"lazy"===p&&"high"===d&&w.push({s:"error",msg:'Conflict: loading="lazy" + fetchpriority="high"'}),m||w.push({s:"warning",msg:"Missing width/height attributes (CLS risk)"}),function(t){return["avif","webp","jxl","auto (cdn)","auto (cdn?)"].includes(t)}(l)||h||"svg"===l||w.push({s:"info",msg:"No modern format detected (WebP / AVIF / JXL)"}),{img:e,inViewport:r,isLcp:c,src:u,format:l,loading:p??"(not set)",decoding:f??"(not set)",fetchpriority:d??"(not set)",hasDimensions:m,hasSrcset:e.hasAttribute("srcset"),hasSizes:e.hasAttribute("sizes"),inPicture:h,dimensions:`${Math.round(g.width)}×${Math.round(g.height)}`,lcpPreload:y,issues:w}}),g=c.filter(t=>t.issues.length>0);c.flatMap(t=>t.issues.filter(t=>"error"===t.s)),c.flatMap(t=>t.issues.filter(t=>"warning"===t.s)),c.flatMap(t=>t.issues.filter(t=>"info"===t.s));if(n){const t=c.find(t=>t.isLcp);t.lcpPreload&&t.lcpPreload.getAttribute("fetchpriority")}g.length>0&&g.forEach(t=>{t.issues.some(t=>"error"===t.s),t.issues.forEach(t=>{})});const u=n?c.find(t=>t.isLcp):null;c.filter(t=>t.inViewport),c.filter(t=>!t.inViewport),u&&i(u.src),c.map(t=>({url:t.src,format:t.format,inViewport:t.inViewport,isLCP:t.isLcp,loading:t.loading,decoding:t.decoding,fetchpriority:t.fetchpriority,hasDimensions:t.hasDimensions,hasSrcset:t.hasSrcset,hasSizes:t.hasSizes,inPicture:t.inPicture,issues:t.issues.map(t=>({severity:t.s,message:t.msg}))})),c.flatMap(t=>t.issues.map(e=>({severity:e.s,message:`${i(t.src)||"(no src)"}: ${e.msg}`})))})();// snippets/Media/SVG-Embedded-Bitmap-Analysis.js | sha256:0e14a49f0b51e083 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/Media/SVG-Embedded-Bitmap-Analysis.js
(async()=>{function e(e){try{const t=new URL(e).pathname,n=t.split("/").pop()||t;return n.length>45?"…"+n.slice(-42):n}catch{return e.slice(-45)}}function t(e){const t=[],n=/data:image\/(png|jpe?g|gif|webp|avif|bmp|tiff?|ico);base64,([A-Za-z0-9+/=]*)/gi;let r;for(;null!==(r=n.exec(e));)t.push({kind:"inline",format:r[1].replace("jpeg","jpg"),estimatedBytes:Math.floor(3*r[2].length/4)});const a=/(?:xlink:)?href=["']([^"']*\.(?:png|jpe?g|gif|webp|avif|bmp|tiff?|ico))["']/gi;for(;null!==(r=a.exec(e));)t.push({kind:"external",format:r[1].split(".").pop().toLowerCase().replace("jpeg","jpg"),url:r[1]});return t}const n=performance.getEntriesByType("resource").filter(e=>e.name.split("?")[0].toLowerCase().endsWith(".svg")),r=await Promise.all(n.map(async n=>{let r=function(e){return 0===e.transferSize&&0===e.encodedBodySize?"cached":0===e.encodedBodySize?"unknown":e.encodedBodySize<e.decodedBodySize?"compressed":"none"}(n),a=[];try{const e=await fetch(n.name,{cache:"force-cache"}),i=e.headers.get("content-encoding");i&&(r=i),a=t(await e.text())}catch{}const i=n.transferSize>0?n.transferSize:n.encodedBodySize;return{name:e(n.name),url:n.name,transferSize:i,compression:r,bitmaps:a}})),a=Array.from(document.querySelectorAll("svg")),i=a.length,o=(a.filter(e=>e.querySelector("use")),Array.from(document.querySelectorAll("svg")).map((e,n)=>{const r=e.outerHTML,a=t(r);return a.length?{name:e.id?`#${e.id}`:`inline-svg[${n+1}]`,transferSize:new Blob([r]).size,compression:"N/A",bitmaps:a}:null}).filter(Boolean)),s=[...r.filter(e=>e.bitmaps.length>0),...o];if(0===n.length&&0===i)return{script:"SVG-Embedded-Bitmap-Analysis",status:"ok",count:0,items:[],issues:[]};s.length>0&&s.forEach(e=>{e.bitmaps.forEach(e=>{})}),s.map(e=>({url:e.url||e.name,name:e.name,hasBitmap:!0,bitmapCount:e.bitmaps.length,bitmapTypes:e.bitmaps.map(e=>e.format).join(", "),sizeBytes:e.transferSize}))})();// snippets/Media/Video-Element-Audit.js | sha256:b94c8ac50c2247b9 | https://github.com/nucliweb/webperf-snippets/blob/main/snippets/Media/Video-Element-Audit.js
(()=>{const e=Array.from(document.querySelectorAll("video"));if(0===e.length)return{script:"Video-Element-Audit",status:"ok",count:0,items:[],issues:[]};const t=e.map(e=>{const t=function(e){const t=e.getBoundingClientRect();return t.top<window.innerHeight&&t.bottom>0&&t.left<window.innerWidth&&t.right>0&&t.width>0&&t.height>0}(e),s=e.getBoundingClientRect(),o=e.currentSrc||e.getAttribute("src")||"",r=e.getAttribute("preload"),i=e.hasAttribute("autoplay"),n=e.hasAttribute("muted")||e.muted,a=e.hasAttribute("playsinline"),u=e.hasAttribute("loop"),l=e.hasAttribute("controls"),c=e.getAttribute("poster"),p=e.hasAttribute("width")&&e.hasAttribute("height"),d=Array.from(e.querySelectorAll("source")),m=function(e){const t=Array.from(e.querySelectorAll("source")),s=t.map(e=>e.getAttribute("type")||"").filter(Boolean).map(e=>e.toLowerCase()),o=[(e.currentSrc||"").toLowerCase(),(e.getAttribute("src")||"").toLowerCase(),...t.map(e=>(e.getAttribute("src")||"").toLowerCase())];return s.some(e=>e.includes("av01")||e.includes("av1"))?"AV1":s.some(e=>e.includes("vp9"))?"VP9":s.some(e=>e.includes("webm"))||o.some(e=>e.endsWith(".webm"))?"WebM":s.some(e=>e.includes("mp4"))||o.some(e=>e.endsWith(".mp4")||e.endsWith(".mov"))?"MP4":s.some(e=>e.includes("ogg"))||o.some(e=>e.endsWith(".ogv"))?"OGG":t.length>0||e.getAttribute("src")?"unknown":"no source"}(e),h=[];return"auto"!==r||i||h.push({s:"error",msg:'preload="auto" loads the full video eagerly — use preload="none" or preload="metadata"'}),i||t||"none"===r||h.push({s:"warning",msg:'Off-viewport video without preload="none" — set preload="none" to avoid unnecessary loading'}),i&&!n&&h.push({s:"error",msg:"autoplay without muted — browsers block unmuted autoplay"}),i&&!a&&h.push({s:"warning",msg:"autoplay without playsinline — iOS Safari forces fullscreen mode"}),c||h.push({s:"warning",msg:"Missing poster — no preview frame shown before playback starts"}),p||h.push({s:"warning",msg:"Missing width/height attributes (CLS risk)"}),function(e){return["AV1","VP9","WebM"].includes(e)}(m)||"no source"===m||h.push({s:"info",msg:"No modern codec source detected — consider adding a WebM source (VP9 or AV1) for better compression"}),l||i||h.push({s:"info",msg:"No controls attribute and no autoplay — users may not be able to play the video"}),{video:e,inViewport:t,src:o,codec:m,preload:r??"(not set)",autoplay:i,muted:n,playsinline:a,loop:u,controls:l,poster:c?"✓":"(not set)",hasDimensions:p,sourceCount:d.length,dimensions:`${Math.round(s.width)}×${Math.round(s.height)}`,issues:h}}),s=t.filter(e=>e.issues.length>0);t.flatMap(e=>e.issues.filter(e=>"error"===e.s)),t.flatMap(e=>e.issues.filter(e=>"warning"===e.s)),t.flatMap(e=>e.issues.filter(e=>"info"===e.s));s.length>0&&s.forEach(e=>{e.issues.some(e=>"error"===e.s),e.issues.forEach(e=>{})}),t.filter(e=>e.inViewport),t.filter(e=>!e.inViewport),t.map(e=>({src:e.src,codec:e.codec,inViewport:e.inViewport,preload:e.preload,autoplay:e.autoplay,muted:e.muted,playsinline:e.playsinline,loop:e.loop,controls:e.controls,hasPoster:"(not set)"!==e.poster,hasDimensions:e.hasDimensions,sourceCount:e.sourceCount,issues:e.issues.map(e=>({severity:e.s,message:e.msg}))})),t.flatMap(e=>e.issues.map(t=>{return{severity:t.s,message:`${s=e.src,(s?s.split("/").pop()?.split("?")[0]?.slice(0,40)||s.slice(-40):"")||"(no src)"}: ${t.msg}`};var s}))})();