webperf-snippets 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +139 -0
- package/package.json +54 -0
- package/snippets/CoreWebVitals/CLS.js +88 -0
- package/snippets/CoreWebVitals/INP.js +432 -0
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +198 -0
- package/snippets/CoreWebVitals/LCP-Subparts.js +286 -0
- package/snippets/CoreWebVitals/LCP-Trail.js +184 -0
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +228 -0
- package/snippets/CoreWebVitals/LCP.js +135 -0
- package/snippets/CoreWebVitals/WORKFLOWS.md +203 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-inject.js +73 -0
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +191 -0
- package/snippets/Interaction/Forced-Synchronous-Layout.js +286 -0
- package/snippets/Interaction/Input-Latency-Breakdown.js +199 -0
- package/snippets/Interaction/Interactions.js +220 -0
- package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +266 -0
- package/snippets/Interaction/Long-Animation-Frames-Helpers.js +397 -0
- package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +137 -0
- package/snippets/Interaction/Long-Animation-Frames.js +298 -0
- package/snippets/Interaction/LongTask.js +203 -0
- package/snippets/Interaction/Scroll-Performance.js +359 -0
- package/snippets/Interaction/WORKFLOWS.md +205 -0
- package/snippets/Loading/Back-Forward-Cache.js +473 -0
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +512 -0
- package/snippets/Loading/Cache-Strategy-Analysis.js +808 -0
- package/snippets/Loading/Client-Side-Redirect-Detection.js +337 -0
- package/snippets/Loading/Content-Visibility.js +352 -0
- package/snippets/Loading/Critical-CSS-Detection.js +329 -0
- package/snippets/Loading/Event-Processing-Time.js +179 -0
- package/snippets/Loading/FCP.js +101 -0
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +264 -0
- package/snippets/Loading/Find-Images-With-Lazy-and-Fetchpriority.js +155 -0
- package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +300 -0
- package/snippets/Loading/Find-render-blocking-resources.js +161 -0
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +269 -0
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +295 -0
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +317 -0
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +234 -0
- package/snippets/Loading/Inline-Script-Info-and-Size.js +323 -0
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +336 -0
- package/snippets/Loading/Prefetch-Resource-Validation.js +456 -0
- package/snippets/Loading/Priority-Hints-Audit.js +336 -0
- package/snippets/Loading/Resource-Hints-Validation.js +503 -0
- package/snippets/Loading/Resource-Hints.js +400 -0
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +364 -0
- package/snippets/Loading/Script-Loading.js +332 -0
- package/snippets/Loading/Service-Worker-Analysis.js +192 -0
- package/snippets/Loading/TTFB-Resources.js +115 -0
- package/snippets/Loading/TTFB-Sub-Parts.js +117 -0
- package/snippets/Loading/TTFB.js +37 -0
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +394 -0
- package/snippets/Loading/WORKFLOWS.md +163 -0
- package/snippets/Media/Image-Element-Audit.js +350 -0
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +225 -0
- package/snippets/Media/Video-Element-Audit.js +282 -0
- package/snippets/Media/WORKFLOWS.md +190 -0
- package/snippets/README.md +98 -0
- package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +103 -0
- package/snippets/Resources/WORKFLOWS.md +173 -0
- package/src/bin.js +229 -0
- package/src/decision-tree.js +19 -0
- package/src/load-snippet.js +23 -0
- package/src/reporters/human.js +179 -0
- package/src/reporters/json.js +3 -0
- package/src/runner.js +61 -0
- package/src/workflows/audit.js +21 -0
- package/src/workflows/cwv.js +10 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
// Video Element Audit
|
|
2
|
+
// https://webperf-snippets.nucliweb.net
|
|
3
|
+
|
|
4
|
+
(() => {
|
|
5
|
+
function isInViewport(el) {
|
|
6
|
+
const rect = el.getBoundingClientRect();
|
|
7
|
+
return (
|
|
8
|
+
rect.top < window.innerHeight &&
|
|
9
|
+
rect.bottom > 0 &&
|
|
10
|
+
rect.left < window.innerWidth &&
|
|
11
|
+
rect.right > 0 &&
|
|
12
|
+
rect.width > 0 &&
|
|
13
|
+
rect.height > 0
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function detectBestCodec(video) {
|
|
18
|
+
const sources = Array.from(video.querySelectorAll("source"));
|
|
19
|
+
const types = sources
|
|
20
|
+
.map((s) => s.getAttribute("type") || "")
|
|
21
|
+
.filter(Boolean)
|
|
22
|
+
.map((t) => t.toLowerCase());
|
|
23
|
+
|
|
24
|
+
const allSrcs = [
|
|
25
|
+
(video.currentSrc || "").toLowerCase(),
|
|
26
|
+
(video.getAttribute("src") || "").toLowerCase(),
|
|
27
|
+
...sources.map((s) => (s.getAttribute("src") || "").toLowerCase()),
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
if (types.some((t) => t.includes("av01") || t.includes("av1"))) return "AV1";
|
|
31
|
+
if (types.some((t) => t.includes("vp9"))) return "VP9";
|
|
32
|
+
if (types.some((t) => t.includes("webm")) || allSrcs.some((s) => s.endsWith(".webm")))
|
|
33
|
+
return "WebM";
|
|
34
|
+
if (
|
|
35
|
+
types.some((t) => t.includes("mp4")) ||
|
|
36
|
+
allSrcs.some((s) => s.endsWith(".mp4") || s.endsWith(".mov"))
|
|
37
|
+
)
|
|
38
|
+
return "MP4";
|
|
39
|
+
if (types.some((t) => t.includes("ogg")) || allSrcs.some((s) => s.endsWith(".ogv")))
|
|
40
|
+
return "OGG";
|
|
41
|
+
|
|
42
|
+
return sources.length > 0 || video.getAttribute("src") ? "unknown" : "no source";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function isModernCodec(codec) {
|
|
46
|
+
return ["AV1", "VP9", "WebM"].includes(codec);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function shortSrc(url) {
|
|
50
|
+
if (!url) return "";
|
|
51
|
+
return url.split("/").pop()?.split("?")[0]?.slice(0, 40) || url.slice(-40);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const videos = Array.from(document.querySelectorAll("video"));
|
|
55
|
+
|
|
56
|
+
if (videos.length === 0) {
|
|
57
|
+
console.log("No <video> elements found on this page.");
|
|
58
|
+
return { script: "Video-Element-Audit", status: "ok", count: 0, items: [], issues: [] };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const audited = videos.map((video) => {
|
|
62
|
+
const inViewport = isInViewport(video);
|
|
63
|
+
const rect = video.getBoundingClientRect();
|
|
64
|
+
const src = video.currentSrc || video.getAttribute("src") || "";
|
|
65
|
+
|
|
66
|
+
const preload = video.getAttribute("preload");
|
|
67
|
+
const autoplay = video.hasAttribute("autoplay");
|
|
68
|
+
const muted = video.hasAttribute("muted") || video.muted;
|
|
69
|
+
const playsinline = video.hasAttribute("playsinline");
|
|
70
|
+
const loop = video.hasAttribute("loop");
|
|
71
|
+
const controls = video.hasAttribute("controls");
|
|
72
|
+
const poster = video.getAttribute("poster");
|
|
73
|
+
const hasDimensions = video.hasAttribute("width") && video.hasAttribute("height");
|
|
74
|
+
const sources = Array.from(video.querySelectorAll("source"));
|
|
75
|
+
const codec = detectBestCodec(video);
|
|
76
|
+
|
|
77
|
+
const issues = [];
|
|
78
|
+
|
|
79
|
+
if (preload === "auto" && !autoplay) {
|
|
80
|
+
issues.push({
|
|
81
|
+
s: "error",
|
|
82
|
+
msg: 'preload="auto" loads the full video eagerly ā use preload="none" or preload="metadata"',
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!autoplay && !inViewport && preload !== "none") {
|
|
87
|
+
issues.push({
|
|
88
|
+
s: "warning",
|
|
89
|
+
msg: 'Off-viewport video without preload="none" ā set preload="none" to avoid unnecessary loading',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (autoplay && !muted) {
|
|
94
|
+
issues.push({
|
|
95
|
+
s: "error",
|
|
96
|
+
msg: "autoplay without muted ā browsers block unmuted autoplay",
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (autoplay && !playsinline) {
|
|
101
|
+
issues.push({
|
|
102
|
+
s: "warning",
|
|
103
|
+
msg: "autoplay without playsinline ā iOS Safari forces fullscreen mode",
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!poster) {
|
|
108
|
+
issues.push({
|
|
109
|
+
s: "warning",
|
|
110
|
+
msg: "Missing poster ā no preview frame shown before playback starts",
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (!hasDimensions) {
|
|
115
|
+
issues.push({
|
|
116
|
+
s: "warning",
|
|
117
|
+
msg: "Missing width/height attributes (CLS risk)",
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!isModernCodec(codec) && codec !== "no source") {
|
|
122
|
+
issues.push({
|
|
123
|
+
s: "info",
|
|
124
|
+
msg: "No modern codec source detected ā consider adding a WebM source (VP9 or AV1) for better compression",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (!controls && !autoplay) {
|
|
129
|
+
issues.push({
|
|
130
|
+
s: "info",
|
|
131
|
+
msg: "No controls attribute and no autoplay ā users may not be able to play the video",
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
video,
|
|
137
|
+
inViewport,
|
|
138
|
+
src,
|
|
139
|
+
codec,
|
|
140
|
+
preload: preload ?? "(not set)",
|
|
141
|
+
autoplay,
|
|
142
|
+
muted,
|
|
143
|
+
playsinline,
|
|
144
|
+
loop,
|
|
145
|
+
controls,
|
|
146
|
+
poster: poster ? "ā" : "(not set)",
|
|
147
|
+
hasDimensions,
|
|
148
|
+
sourceCount: sources.length,
|
|
149
|
+
dimensions: `${Math.round(rect.width)}Ć${Math.round(rect.height)}`,
|
|
150
|
+
issues,
|
|
151
|
+
};
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
const withIssues = audited.filter((r) => r.issues.length > 0);
|
|
155
|
+
const totalErrors = audited.flatMap((r) => r.issues.filter((i) => i.s === "error")).length;
|
|
156
|
+
const totalWarnings = audited.flatMap((r) => r.issues.filter((i) => i.s === "warning")).length;
|
|
157
|
+
const totalInfos = audited.flatMap((r) => r.issues.filter((i) => i.s === "info")).length;
|
|
158
|
+
|
|
159
|
+
console.group("%cš¬ Video Element Audit", "font-weight: bold; font-size: 14px;");
|
|
160
|
+
|
|
161
|
+
// Summary
|
|
162
|
+
console.log("");
|
|
163
|
+
console.log("%cSummary", "font-weight: bold;");
|
|
164
|
+
console.log(` Total videos : ${videos.length}`);
|
|
165
|
+
console.log(` In viewport : ${audited.filter((r) => r.inViewport).length}`);
|
|
166
|
+
console.log(` Off viewport : ${audited.filter((r) => !r.inViewport).length}`);
|
|
167
|
+
console.log(
|
|
168
|
+
` Issues : ${totalErrors} errors Ā· ${totalWarnings} warnings Ā· ${totalInfos} info`,
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
// Full table
|
|
172
|
+
console.log("");
|
|
173
|
+
console.group(`%cš All Videos (${videos.length})`, "font-weight: bold;");
|
|
174
|
+
console.table(
|
|
175
|
+
audited.map((r) => ({
|
|
176
|
+
src: shortSrc(r.src),
|
|
177
|
+
codec: r.codec,
|
|
178
|
+
viewport: r.inViewport ? "ā" : "",
|
|
179
|
+
preload: r.preload,
|
|
180
|
+
autoplay: r.autoplay ? "ā" : "",
|
|
181
|
+
muted: r.muted ? "ā" : "",
|
|
182
|
+
playsinline: r.playsinline ? "ā" : "",
|
|
183
|
+
controls: r.controls ? "ā" : "",
|
|
184
|
+
poster: r.poster,
|
|
185
|
+
"w/h": r.hasDimensions ? "ā" : "ā ļø",
|
|
186
|
+
sources: r.sourceCount,
|
|
187
|
+
issues:
|
|
188
|
+
r.issues.length === 0
|
|
189
|
+
? "ā
"
|
|
190
|
+
: r.issues
|
|
191
|
+
.map((i) => (i.s === "error" ? "š“" : i.s === "warning" ? "ā ļø" : "ā¹ļø"))
|
|
192
|
+
.join(" "),
|
|
193
|
+
})),
|
|
194
|
+
);
|
|
195
|
+
console.groupEnd();
|
|
196
|
+
|
|
197
|
+
// Issues detail
|
|
198
|
+
if (withIssues.length > 0) {
|
|
199
|
+
console.log("");
|
|
200
|
+
console.group(
|
|
201
|
+
`%cā ļø Issues Detail (${totalErrors} errors Ā· ${totalWarnings} warnings Ā· ${totalInfos} info)`,
|
|
202
|
+
"color: #ef4444; font-weight: bold;",
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
withIssues.forEach((r) => {
|
|
206
|
+
const hasError = r.issues.some((i) => i.s === "error");
|
|
207
|
+
const icon = hasError ? "š“" : "ā ļø";
|
|
208
|
+
console.log("");
|
|
209
|
+
console.log(`%c${icon} ${shortSrc(r.src) || "(no src)"}`, "font-weight: bold;");
|
|
210
|
+
r.issues.forEach((issue) => {
|
|
211
|
+
const prefix = issue.s === "error" ? " š“" : issue.s === "warning" ? " ā ļø" : " ā¹ļø";
|
|
212
|
+
console.log(`${prefix} ${issue.msg}`);
|
|
213
|
+
});
|
|
214
|
+
console.log(" Element:", r.video);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
console.groupEnd();
|
|
218
|
+
} else {
|
|
219
|
+
console.log("");
|
|
220
|
+
console.log("%cā
No issues found.", "color: #22c55e; font-weight: bold;");
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Quick reference
|
|
224
|
+
console.log("");
|
|
225
|
+
console.group("%cš Quick Reference", "color: #3b82f6; font-weight: bold;");
|
|
226
|
+
console.log("");
|
|
227
|
+
console.log("%c š¬ Autoplay hero/background video:", "color: #22c55e;");
|
|
228
|
+
console.log(
|
|
229
|
+
'%c <video autoplay muted playsinline loop poster="preview.avif" width="1280" height="720">\n <source src="hero.av1.webm" type="video/webm; codecs=av01.0.04M.08">\n <source src="hero.vp9.webm" type="video/webm; codecs=vp9">\n <source src="hero.mp4" type="video/mp4">\n </video>',
|
|
230
|
+
"font-family: monospace;",
|
|
231
|
+
);
|
|
232
|
+
console.log("");
|
|
233
|
+
console.log("%c ā
User-controlled video:", "color: #22c55e;");
|
|
234
|
+
console.log(
|
|
235
|
+
'%c <video controls preload="metadata" poster="thumbnail.avif" width="800" height="450">\n <source src="video.vp9.webm" type="video/webm; codecs=vp9">\n <source src="video.mp4" type="video/mp4">\n </video>',
|
|
236
|
+
"font-family: monospace;",
|
|
237
|
+
);
|
|
238
|
+
console.log("");
|
|
239
|
+
console.log("%c ā
Below-fold video:", "color: #22c55e;");
|
|
240
|
+
console.log(
|
|
241
|
+
'%c <video controls preload="none" poster="thumbnail.avif" width="800" height="450">\n <source src="video.vp9.webm" type="video/webm; codecs=vp9">\n <source src="video.mp4" type="video/mp4">\n </video>',
|
|
242
|
+
"font-family: monospace;",
|
|
243
|
+
);
|
|
244
|
+
console.groupEnd();
|
|
245
|
+
|
|
246
|
+
console.groupEnd();
|
|
247
|
+
|
|
248
|
+
return {
|
|
249
|
+
script: "Video-Element-Audit",
|
|
250
|
+
status: "ok",
|
|
251
|
+
count: videos.length,
|
|
252
|
+
details: {
|
|
253
|
+
totalVideos: videos.length,
|
|
254
|
+
inViewport: audited.filter((r) => r.inViewport).length,
|
|
255
|
+
offViewport: audited.filter((r) => !r.inViewport).length,
|
|
256
|
+
totalErrors,
|
|
257
|
+
totalWarnings,
|
|
258
|
+
totalInfos,
|
|
259
|
+
},
|
|
260
|
+
items: audited.map((r) => ({
|
|
261
|
+
src: r.src,
|
|
262
|
+
codec: r.codec,
|
|
263
|
+
inViewport: r.inViewport,
|
|
264
|
+
preload: r.preload,
|
|
265
|
+
autoplay: r.autoplay,
|
|
266
|
+
muted: r.muted,
|
|
267
|
+
playsinline: r.playsinline,
|
|
268
|
+
loop: r.loop,
|
|
269
|
+
controls: r.controls,
|
|
270
|
+
hasPoster: r.poster !== "(not set)",
|
|
271
|
+
hasDimensions: r.hasDimensions,
|
|
272
|
+
sourceCount: r.sourceCount,
|
|
273
|
+
issues: r.issues.map((i) => ({ severity: i.s, message: i.msg })),
|
|
274
|
+
})),
|
|
275
|
+
issues: audited.flatMap((r) =>
|
|
276
|
+
r.issues.map((i) => ({
|
|
277
|
+
severity: i.s,
|
|
278
|
+
message: `${shortSrc(r.src) || "(no src)"}: ${i.msg}`,
|
|
279
|
+
}))
|
|
280
|
+
),
|
|
281
|
+
};
|
|
282
|
+
})();
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
## Common Workflows
|
|
2
|
+
|
|
3
|
+
### Complete Media Audit
|
|
4
|
+
|
|
5
|
+
When the user asks for media optimization or "audit images and videos":
|
|
6
|
+
|
|
7
|
+
1. **Image-Element-Audit.js** - Analyze all images (format, lazy loading, sizing, fetchpriority)
|
|
8
|
+
2. **Video-Element-Audit.js** - Analyze all videos (poster, preload, formats, autoplay)
|
|
9
|
+
3. **SVG-Embedded-Bitmap-Analysis.js** - Detect inefficient bitmap images embedded in SVGs
|
|
10
|
+
|
|
11
|
+
### Image Optimization Workflow
|
|
12
|
+
|
|
13
|
+
When the user asks "optimize images" or "check image performance":
|
|
14
|
+
|
|
15
|
+
1. **Image-Element-Audit.js** - Full image audit
|
|
16
|
+
2. Cross-reference with **webperf-loading** skill:
|
|
17
|
+
- Find-Above-The-Fold-Lazy-Loaded-Images.js (incorrectly lazy-loaded images)
|
|
18
|
+
- Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js (missing lazy loading)
|
|
19
|
+
- Find-Images-With-Lazy-and-Fetchpriority.js (contradictory attributes)
|
|
20
|
+
- Priority-Hints-Audit.js (LCP image should have fetchpriority="high")
|
|
21
|
+
|
|
22
|
+
### Video Performance Audit
|
|
23
|
+
|
|
24
|
+
When the user asks "optimize videos" or "check video performance":
|
|
25
|
+
|
|
26
|
+
1. **Video-Element-Audit.js** - Full video audit
|
|
27
|
+
2. Cross-reference with **webperf-core-web-vitals** skill:
|
|
28
|
+
- LCP-Video-Candidate.js (check if video/poster is LCP)
|
|
29
|
+
3. Cross-reference with **webperf-loading** skill:
|
|
30
|
+
- Priority-Hints-Audit.js (video poster priority)
|
|
31
|
+
- Resource-Hints-Validation.js (video preload)
|
|
32
|
+
|
|
33
|
+
### LCP Image Investigation
|
|
34
|
+
|
|
35
|
+
When LCP is an image and needs optimization:
|
|
36
|
+
|
|
37
|
+
1. Cross-reference with **webperf-core-web-vitals** skill:
|
|
38
|
+
- LCP.js (measure LCP)
|
|
39
|
+
- LCP-Image-Entropy.js (analyze image complexity)
|
|
40
|
+
2. **Image-Element-Audit.js** - Check format, dimensions, lazy loading
|
|
41
|
+
3. Cross-reference with **webperf-loading** skill:
|
|
42
|
+
- Find-Above-The-Fold-Lazy-Loaded-Images.js (should NOT be lazy)
|
|
43
|
+
- Priority-Hints-Audit.js (should have fetchpriority="high")
|
|
44
|
+
- Resource-Hints-Validation.js (consider preload)
|
|
45
|
+
|
|
46
|
+
### Layout Shift from Images
|
|
47
|
+
|
|
48
|
+
When CLS is caused by images without dimensions:
|
|
49
|
+
|
|
50
|
+
1. **Image-Element-Audit.js** - Check for missing width/height attributes
|
|
51
|
+
2. Cross-reference with **webperf-core-web-vitals** skill:
|
|
52
|
+
- CLS.js (measure total CLS)
|
|
53
|
+
3. Cross-reference with **webperf-interaction** skill:
|
|
54
|
+
- Layout-Shift-Loading-and-Interaction.js (when shifts occur)
|
|
55
|
+
|
|
56
|
+
### SVG Optimization Audit
|
|
57
|
+
|
|
58
|
+
When the user asks about SVG performance or file sizes are large:
|
|
59
|
+
|
|
60
|
+
1. **SVG-Embedded-Bitmap-Analysis.js** - Detect raster images embedded in vector SVGs
|
|
61
|
+
2. Recommend SVGO optimization for SVGs without embedded bitmaps
|
|
62
|
+
3. Recommend extracting bitmaps to separate image files with proper formats
|
|
63
|
+
|
|
64
|
+
## Decision Tree
|
|
65
|
+
|
|
66
|
+
Use this decision tree to automatically run follow-up snippets based on results:
|
|
67
|
+
|
|
68
|
+
### After Image-Element-Audit.js
|
|
69
|
+
|
|
70
|
+
- **If images missing width/height attributes** ā Layout shift risk, run:
|
|
71
|
+
1. **webperf-core-web-vitals:CLS.js** (measure CLS impact)
|
|
72
|
+
2. **webperf-interaction:Layout-Shift-Loading-and-Interaction.js** (timing of shifts)
|
|
73
|
+
3. Recommend adding explicit dimensions to all images
|
|
74
|
+
|
|
75
|
+
- **If images using wrong format (JPEG for graphics, PNG for photos)** ā Recommend:
|
|
76
|
+
- Modern formats: WebP, AVIF
|
|
77
|
+
- Appropriate format for content type
|
|
78
|
+
- Format-specific compression settings
|
|
79
|
+
|
|
80
|
+
- **If images much larger than display size** ā Recommend:
|
|
81
|
+
- Responsive images with srcset
|
|
82
|
+
- Appropriate image CDN sizing
|
|
83
|
+
- srcset with multiple sizes for different viewports
|
|
84
|
+
|
|
85
|
+
- **If above-the-fold images are lazy-loaded** ā Run:
|
|
86
|
+
1. **webperf-loading:Find-Above-The-Fold-Lazy-Loaded-Images.js** (confirm)
|
|
87
|
+
2. **webperf-core-web-vitals:LCP.js** (measure LCP impact)
|
|
88
|
+
3. Recommend removing loading="lazy" from above-fold images
|
|
89
|
+
|
|
90
|
+
- **If LCP image lacks fetchpriority="high"** ā Run:
|
|
91
|
+
1. **webperf-core-web-vitals:LCP.js** (measure current LCP)
|
|
92
|
+
2. **webperf-loading:Priority-Hints-Audit.js** (full priority audit)
|
|
93
|
+
3. Recommend adding fetchpriority="high" to LCP image
|
|
94
|
+
|
|
95
|
+
- **If below-the-fold images are NOT lazy-loaded** ā Run:
|
|
96
|
+
1. **webperf-loading:Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js** (confirm)
|
|
97
|
+
2. Recommend adding loading="lazy" to offscreen images
|
|
98
|
+
|
|
99
|
+
- **If images have both loading="lazy" AND fetchpriority="high"** ā Run:
|
|
100
|
+
1. **webperf-loading:Find-Images-With-Lazy-and-Fetchpriority.js** (confirm contradiction)
|
|
101
|
+
2. Recommend removing one of the conflicting attributes
|
|
102
|
+
|
|
103
|
+
- **If images competing with critical resources** ā Run:
|
|
104
|
+
1. **webperf-loading:Find-render-blocking-resources.js** (resource priority conflicts)
|
|
105
|
+
2. **webperf-loading:TTFB-Resources.js** (identify slow image CDN)
|
|
106
|
+
|
|
107
|
+
- **If images missing alt text** ā Accessibility issue, recommend adding descriptive alt text
|
|
108
|
+
|
|
109
|
+
### After Video-Element-Audit.js
|
|
110
|
+
|
|
111
|
+
- **If video is LCP candidate** ā Run:
|
|
112
|
+
1. **webperf-core-web-vitals:LCP-Video-Candidate.js** (confirm)
|
|
113
|
+
2. **webperf-core-web-vitals:LCP.js** (measure LCP)
|
|
114
|
+
3. **webperf-core-web-vitals:LCP-Subparts.js** (break down timing)
|
|
115
|
+
4. Optimize video poster image or consider image alternative
|
|
116
|
+
|
|
117
|
+
- **If video missing poster** ā Recommend:
|
|
118
|
+
- Adding poster image for better perceived performance
|
|
119
|
+
- Using first frame or custom thumbnail
|
|
120
|
+
- Optimizing poster as you would an image
|
|
121
|
+
|
|
122
|
+
- **If video uses preload="auto"** ā Bandwidth concern, evaluate:
|
|
123
|
+
- Is video above-the-fold? Keep preload="auto"
|
|
124
|
+
- Is video below-the-fold? Change to preload="metadata" or "none"
|
|
125
|
+
- Is autoplay intended? Verify preload matches intent
|
|
126
|
+
|
|
127
|
+
- **If autoplay video without muted** ā Browser will block, recommend:
|
|
128
|
+
- Adding muted attribute
|
|
129
|
+
- Or removing autoplay
|
|
130
|
+
|
|
131
|
+
- **If video missing multiple formats** ā Recommend:
|
|
132
|
+
- WebM for Chrome/Firefox
|
|
133
|
+
- MP4 as fallback for Safari
|
|
134
|
+
- Order sources by efficiency (WebM first)
|
|
135
|
+
|
|
136
|
+
- **If large video files (>5MB)** ā Recommend:
|
|
137
|
+
- Compression/transcoding
|
|
138
|
+
- Adaptive bitrate streaming (HLS, DASH)
|
|
139
|
+
- Loading strategy optimization
|
|
140
|
+
|
|
141
|
+
### After SVG-Embedded-Bitmap-Analysis.js
|
|
142
|
+
|
|
143
|
+
- **If bitmap images found in SVGs** ā Recommend:
|
|
144
|
+
1. Extract bitmaps to separate files
|
|
145
|
+
2. Use WebP/AVIF format for extracted images
|
|
146
|
+
3. Reference images from SVG with <image> element
|
|
147
|
+
4. Or convert to pure vector if possible
|
|
148
|
+
|
|
149
|
+
- **If large embedded bitmaps (>100KB)** ā Critical inefficiency:
|
|
150
|
+
- SVG parsing overhead + large bitmap = worst of both worlds
|
|
151
|
+
- Urgently recommend extraction
|
|
152
|
+
|
|
153
|
+
- **If multiple small bitmaps in SVG** ā Consider:
|
|
154
|
+
- CSS sprites for small icons
|
|
155
|
+
- SVG symbols for reusable graphics
|
|
156
|
+
- Extracting to individual optimized images
|
|
157
|
+
|
|
158
|
+
### Performance Budget Thresholds
|
|
159
|
+
|
|
160
|
+
Use these thresholds to trigger recommendations:
|
|
161
|
+
|
|
162
|
+
**Image File Sizes:**
|
|
163
|
+
- **Warning**: Individual image > 500KB ā Check format and compression
|
|
164
|
+
- **Critical**: Individual image > 1MB ā Urgent optimization needed
|
|
165
|
+
- **Total images**: > 5MB on initial load ā Implement lazy loading
|
|
166
|
+
|
|
167
|
+
**Image Formats:**
|
|
168
|
+
- **JPEG for graphics/icons** ā Recommend PNG or SVG
|
|
169
|
+
- **PNG for photos** ā Recommend JPEG, WebP, or AVIF
|
|
170
|
+
- **GIF for animations** ā Recommend video (MP4/WebM) or animated WebP
|
|
171
|
+
- **No modern formats (WebP/AVIF)** ā Recommend upgrading
|
|
172
|
+
|
|
173
|
+
**Image Dimensions:**
|
|
174
|
+
- **Intrinsic size > 2x display size** ā Recommend responsive images
|
|
175
|
+
- **Intrinsic size < display size** ā Upscaling = blurry, provide larger source
|
|
176
|
+
|
|
177
|
+
**Video File Sizes:**
|
|
178
|
+
- **Warning**: Video > 10MB ā Consider compression or streaming
|
|
179
|
+
- **Critical**: Video > 50MB ā Urgent optimization or streaming needed
|
|
180
|
+
|
|
181
|
+
**Lazy Loading:**
|
|
182
|
+
- **Above-fold images lazy-loaded** ā Critical LCP impact, fix immediately
|
|
183
|
+
- **Below-fold images NOT lazy-loaded** ā Wasted bandwidth, implement lazy loading
|
|
184
|
+
- **>10 images eager-loaded** ā Excessive, implement lazy loading
|
|
185
|
+
|
|
186
|
+
**Priority Hints:**
|
|
187
|
+
- **LCP image without fetchpriority="high"** ā Add for 10-30% LCP improvement
|
|
188
|
+
- **Non-LCP images with fetchpriority="high"** ā Remove, wasting browser hints
|
|
189
|
+
- **Lazy + fetchpriority="high" conflict** ā Fix contradiction
|
|
190
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Snippets Directory
|
|
2
|
+
|
|
3
|
+
This directory contains the source JavaScript files for all web performance measurement snippets, organized by category.
|
|
4
|
+
|
|
5
|
+
## Structure
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
snippets/
|
|
9
|
+
āāā CoreWebVitals/ # LCP, CLS, INP metrics
|
|
10
|
+
ā āāā *.js # Snippet source files
|
|
11
|
+
ā āāā WORKFLOWS.md # Optional workflows & decision trees (if applicable)
|
|
12
|
+
āāā Loading/ # TTFB, FCP, scripts, fonts, resource hints
|
|
13
|
+
ā āāā *.js
|
|
14
|
+
ā āāā WORKFLOWS.md # Workflows & decision trees for loading optimization
|
|
15
|
+
āāā Interaction/ # Long tasks, animation frames, scroll performance
|
|
16
|
+
ā āāā *.js
|
|
17
|
+
ā āāā WORKFLOWS.md # Optional workflows & decision trees (if applicable)
|
|
18
|
+
āāā Media/ # Images, videos, SVGs
|
|
19
|
+
ā āāā *.js
|
|
20
|
+
ā āāā WORKFLOWS.md # Optional workflows & decision trees (if applicable)
|
|
21
|
+
āāā Resources/ # Network bandwidth, connection quality
|
|
22
|
+
āāā *.js
|
|
23
|
+
āāā WORKFLOWS.md # Optional workflows & decision trees (if applicable)
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Adding Workflows & Decision Trees
|
|
27
|
+
|
|
28
|
+
To add intelligent workflows and decision trees to a skill category:
|
|
29
|
+
|
|
30
|
+
1. **Create `WORKFLOWS.md`** in the category directory (e.g., `snippets/Loading/WORKFLOWS.md`)
|
|
31
|
+
|
|
32
|
+
2. **Structure the file** with two main sections:
|
|
33
|
+
|
|
34
|
+
```markdown
|
|
35
|
+
## Common Workflows
|
|
36
|
+
|
|
37
|
+
### Workflow Name
|
|
38
|
+
|
|
39
|
+
When the user asks about [scenario]:
|
|
40
|
+
|
|
41
|
+
1. **Snippet1.js** - Brief description
|
|
42
|
+
2. **Snippet2.js** - Brief description
|
|
43
|
+
|
|
44
|
+
## Decision Tree
|
|
45
|
+
|
|
46
|
+
Use this decision tree to automatically run follow-up snippets based on results:
|
|
47
|
+
|
|
48
|
+
### After Snippet1.js
|
|
49
|
+
|
|
50
|
+
- **If condition** ā Run **Snippet2.js**
|
|
51
|
+
- **If other condition** ā Run **Snippet3.js**
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
3. **Run the generator** to inject workflows into skills:
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
node scripts/generate-skills.js
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The generator will:
|
|
61
|
+
- Read `WORKFLOWS.md` from each category
|
|
62
|
+
- Inject the content into the generated `SKILL.md`
|
|
63
|
+
- Copy to both `/skills/` (for distribution) and `/.claude/skills/` (for local use)
|
|
64
|
+
|
|
65
|
+
## Example: Loading Workflows
|
|
66
|
+
|
|
67
|
+
See `snippets/Loading/WORKFLOWS.md` for a complete example with:
|
|
68
|
+
|
|
69
|
+
- 8 common workflow scenarios (Complete Audit, Server Investigation, Font Optimization, etc.)
|
|
70
|
+
- 16 decision trees with conditional logic for follow-up snippets
|
|
71
|
+
- Threshold-based triggers (e.g., "If TTFB > 600ms, run TTFB-Sub-Parts.js")
|
|
72
|
+
|
|
73
|
+
## Benefits
|
|
74
|
+
|
|
75
|
+
**For agents:**
|
|
76
|
+
- Autonomous multi-snippet execution based on results
|
|
77
|
+
- Context-aware snippet selection
|
|
78
|
+
- Progressive diagnostic workflows
|
|
79
|
+
|
|
80
|
+
**For maintainability:**
|
|
81
|
+
- Workflows persist across regenerations
|
|
82
|
+
- Single source of truth in `snippets/` directory
|
|
83
|
+
- Version-controlled expertise and best practices
|
|
84
|
+
|
|
85
|
+
## Build System
|
|
86
|
+
|
|
87
|
+
The `generate-skills.js` script:
|
|
88
|
+
|
|
89
|
+
1. Reads all `.js` files from each category
|
|
90
|
+
2. Extracts metadata from corresponding MDX documentation in `/pages/`
|
|
91
|
+
3. Injects `WORKFLOWS.md` content (if exists)
|
|
92
|
+
4. Generates complete `SKILL.md` files with:
|
|
93
|
+
- Snippet table
|
|
94
|
+
- Execution instructions
|
|
95
|
+
- Workflows & decision trees
|
|
96
|
+
- Individual snippet documentation with thresholds
|
|
97
|
+
|
|
98
|
+
**Note:** Never edit generated files directly. Always modify source files in `/snippets/` and regenerate.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Network Bandwidth & Connection Quality Analysis
|
|
2
|
+
// https://webperf-snippets.nucliweb.net
|
|
3
|
+
|
|
4
|
+
(() => {
|
|
5
|
+
const connection =
|
|
6
|
+
navigator.connection ||
|
|
7
|
+
navigator.mozConnection ||
|
|
8
|
+
navigator.webkitConnection;
|
|
9
|
+
|
|
10
|
+
if (!connection) {
|
|
11
|
+
console.warn(
|
|
12
|
+
"%cā ļø Network Information API not supported in this browser",
|
|
13
|
+
"color: #f59e0b; font-weight: bold;"
|
|
14
|
+
);
|
|
15
|
+
console.log("Available in Chrome and Android browsers.");
|
|
16
|
+
return { script: "Network-Bandwidth-Connection-Quality", status: "unsupported", error: "Network Information API not supported" };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const effectiveTypeRating = {
|
|
20
|
+
"slow-2g": { label: "Slow 2G", color: "#ef4444" },
|
|
21
|
+
"2g": { label: "2G", color: "#f97316" },
|
|
22
|
+
"3g": { label: "3G", color: "#f59e0b" },
|
|
23
|
+
"4g": { label: "4G", color: "#22c55e" },
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const effectiveType = connection.effectiveType || "unknown";
|
|
27
|
+
const rating = effectiveTypeRating[effectiveType] || {
|
|
28
|
+
label: effectiveType,
|
|
29
|
+
color: "#6b7280",
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
console.group(
|
|
33
|
+
`%cš” Network Quality: ${rating.label}`,
|
|
34
|
+
`color: ${rating.color}; font-weight: bold; font-size: 14px;`
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
console.log("");
|
|
38
|
+
console.log("%cConnection details:", "font-weight: bold;");
|
|
39
|
+
console.table({
|
|
40
|
+
"Connection Type": connection.type || "unknown",
|
|
41
|
+
"Effective Type": effectiveType,
|
|
42
|
+
"Downlink (Mbps)":
|
|
43
|
+
connection.downlink !== undefined ? connection.downlink : "N/A",
|
|
44
|
+
"RTT (ms)": connection.rtt !== undefined ? connection.rtt : "N/A",
|
|
45
|
+
"Save-Data": connection.saveData ? "Enabled" : "Disabled",
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Recommendations
|
|
49
|
+
console.log("");
|
|
50
|
+
if (connection.saveData) {
|
|
51
|
+
console.log(
|
|
52
|
+
"%cš” Save-Data is enabled ā consider serving reduced assets:",
|
|
53
|
+
"color: #3b82f6; font-weight: bold;"
|
|
54
|
+
);
|
|
55
|
+
console.log(" - Omit non-essential images and videos");
|
|
56
|
+
console.log(" - Disable autoplay and animations");
|
|
57
|
+
console.log(" - Defer non-critical resources");
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (effectiveType === "slow-2g" || effectiveType === "2g") {
|
|
61
|
+
console.log(
|
|
62
|
+
"%cš” Slow connection detected ā performance tips:",
|
|
63
|
+
"color: #3b82f6; font-weight: bold;"
|
|
64
|
+
);
|
|
65
|
+
console.log(" - Prioritize critical resources with fetchpriority=high");
|
|
66
|
+
console.log(" - Minimize render-blocking scripts and stylesheets");
|
|
67
|
+
console.log(" - Consider skeleton screens over layout shifts");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Monitor connection changes
|
|
71
|
+
const onChange = () => {
|
|
72
|
+
const updated = connection.effectiveType || "unknown";
|
|
73
|
+
const updatedRating = effectiveTypeRating[updated] || {
|
|
74
|
+
label: updated,
|
|
75
|
+
color: "#6b7280",
|
|
76
|
+
};
|
|
77
|
+
console.log(
|
|
78
|
+
`%cš Connection changed ā ${updatedRating.label} (${updated})`,
|
|
79
|
+
`color: ${updatedRating.color}; font-weight: bold;`
|
|
80
|
+
);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
connection.addEventListener("change", onChange);
|
|
84
|
+
console.log("");
|
|
85
|
+
console.log(
|
|
86
|
+
"%cš Listening for connection changes... (navigate to trigger)",
|
|
87
|
+
"color: #6b7280;"
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
console.groupEnd();
|
|
91
|
+
|
|
92
|
+
return {
|
|
93
|
+
script: "Network-Bandwidth-Connection-Quality",
|
|
94
|
+
status: "ok",
|
|
95
|
+
details: {
|
|
96
|
+
effectiveType,
|
|
97
|
+
downlink: connection.downlink,
|
|
98
|
+
rtt: connection.rtt,
|
|
99
|
+
saveData: connection.saveData,
|
|
100
|
+
type: connection.type || "unknown",
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
})();
|