webperf-snippets 0.2.0 → 0.4.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 +142 -14
- package/package.json +1 -1
- package/snippets/CoreWebVitals/CLS.js +29 -15
- package/snippets/CoreWebVitals/INP.js +14 -7
- package/snippets/CoreWebVitals/LCP-Image-Entropy.js +29 -11
- package/snippets/CoreWebVitals/LCP-Subparts.js +4 -0
- package/snippets/CoreWebVitals/LCP-Trail.js +14 -3
- package/snippets/CoreWebVitals/LCP-Video-Candidate.js +20 -2
- package/snippets/CoreWebVitals/LCP.js +4 -2
- package/snippets/DevTools-Overrides/Fetch-XHR-Timeline-read.js +9 -2
- package/snippets/Interaction/DOM-Size-and-Depth.js +134 -0
- package/snippets/Interaction/Forced-Synchronous-Layout.js +33 -7
- package/snippets/Interaction/Input-Latency-Breakdown.js +45 -1
- package/snippets/Interaction/Interactions.js +16 -5
- package/snippets/Interaction/Layout-Shift-Loading-and-Interaction.js +44 -13
- package/snippets/Interaction/Long-Animation-Frames-Helpers.js +28 -1
- package/snippets/Interaction/Long-Animation-Frames-Script-Attribution.js +8 -0
- package/snippets/Interaction/Long-Animation-Frames.js +31 -2
- package/snippets/Interaction/LongTask.js +37 -5
- package/snippets/Interaction/Scroll-Performance.js +40 -0
- package/snippets/Interaction/WORKFLOWS.md +9 -0
- package/snippets/Loading/Back-Forward-Cache.js +174 -381
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +76 -44
- package/snippets/Loading/Cache-Strategy-Analysis.js +141 -43
- package/snippets/Loading/Compression-Audit.js +172 -0
- package/snippets/Loading/Content-Visibility.js +8 -3
- package/snippets/Loading/Critical-CSS-Detection.js +8 -6
- package/snippets/Loading/Event-Processing-Time.js +15 -2
- package/snippets/Loading/FCP.js +39 -5
- package/snippets/Loading/Find-Above-The-Fold-Lazy-Loaded-Images.js +1 -1
- package/snippets/Loading/Find-non-Lazy-Loaded-Images-outside-of-the-viewport.js +7 -5
- package/snippets/Loading/First-And-Third-Party-Script-Info.js +58 -20
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +46 -13
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +53 -4
- package/snippets/Loading/Inline-CSS-Info-and-Size.js +8 -6
- package/snippets/Loading/Inline-Script-Info-and-Size.js +14 -7
- package/snippets/Loading/JS-Execution-Time-Breakdown.js +23 -10
- package/snippets/Loading/Priority-Hints-Audit.js +4 -1
- package/snippets/Loading/Resource-Hints-Validation.js +44 -9
- package/snippets/Loading/Resource-Hints.js +29 -8
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +77 -8
- package/snippets/Loading/Script-Loading.js +72 -24
- package/snippets/Loading/Server-Timing-Early-Hints.js +126 -0
- package/snippets/Loading/Service-Worker-Analysis.js +229 -53
- package/snippets/Loading/Speculation-Rules-Inspector.js +134 -0
- package/snippets/Loading/TTFB-Resources.js +86 -18
- package/snippets/Loading/TTFB-Sub-Parts.js +9 -2
- package/snippets/Loading/TTFB.js +3 -2
- package/snippets/Loading/Third-Party-Impact-by-Domain.js +244 -0
- package/snippets/Loading/Validate-Preload-Async-Defer-Scripts.js +21 -3
- package/snippets/Loading/WORKFLOWS.md +47 -1
- package/snippets/Loading/Webfont-Usage-Analyzer.js +332 -0
- package/snippets/Media/Image-Element-Audit.js +74 -11
- package/snippets/Media/Oversized-Images.js +217 -0
- package/snippets/Media/SVG-Embedded-Bitmap-Analysis.js +18 -3
- package/snippets/Media/WORKFLOWS.md +22 -0
- package/snippets/Resources/Network-Bandwidth-Connection-Quality.js +7 -0
- package/snippets/SCHEMA.md +1179 -0
- package/snippets/_shared/helpers.js +61 -0
- package/src/bin.js +72 -45
- package/src/crux.js +84 -0
- package/src/decision-tree.js +55 -7
- package/src/interactions.js +13 -3
- package/src/load-snippet.js +1 -1
- package/src/report-to.js +46 -0
- package/src/reporters/human.js +43 -5
- package/src/reporters/markdown.js +27 -1
- package/src/runner.js +75 -21
- package/src/snippet-names.js +89 -0
- package/src/tracking.js +6 -0
- package/src/workflows/audit.js +17 -0
- package/src/workflows/interaction.js +30 -0
- package/src/workflows/loading.js +9 -0
package/snippets/Loading/TTFB.js
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
(() => {
|
|
5
5
|
new PerformanceObserver((entryList) => {
|
|
6
6
|
const [pageNav] = entryList.getEntriesByType("navigation");
|
|
7
|
-
|
|
7
|
+
// Prerendered pages: measure from activation, not from the start of the prerender
|
|
8
|
+
const ttfb = Math.max(0, pageNav.responseStart - (pageNav.activationStart || 0));
|
|
8
9
|
|
|
9
10
|
let rating, color;
|
|
10
11
|
if (ttfb <= 800) {
|
|
@@ -30,7 +31,7 @@
|
|
|
30
31
|
// Synchronous return for agent
|
|
31
32
|
const [nav] = performance.getEntriesByType("navigation");
|
|
32
33
|
if (!nav) return { script: "TTFB", status: "error", error: "No navigation entry" };
|
|
33
|
-
const value = Math.round(nav.responseStart);
|
|
34
|
+
const value = Math.round(Math.max(0, nav.responseStart - (nav.activationStart || 0)));
|
|
34
35
|
const rating = value <= 800 ? "good" : value <= 1800 ? "needs-improvement" : "poor";
|
|
35
36
|
return { script: "TTFB", status: "ok", metric: "TTFB", value, unit: "ms", rating,
|
|
36
37
|
thresholds: { good: 800, needsImprovement: 1800 } };
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
// Third-Party Impact by Domain
|
|
2
|
+
// Groups third-party requests by root domain and reports requests, transfer size, render-blocking and main-thread time
|
|
3
|
+
// https://webperf-snippets.nucliweb.net
|
|
4
|
+
|
|
5
|
+
(() => {
|
|
6
|
+
const MAX_ITEMS = 50;
|
|
7
|
+
const LOAF_WARNING_MS = 250; // Script time in long animation frames above which one domain is a problem
|
|
8
|
+
const MAX_LISTED_DOMAINS = 5;
|
|
9
|
+
|
|
10
|
+
// Domains that belong to the site but differ from the page's root domain, such as its own CDN.
|
|
11
|
+
// They count as first party. Example: const OWN_DOMAINS = ["bbci.co.uk", "bbc.co.uk"];
|
|
12
|
+
const OWN_DOMAINS = [];
|
|
13
|
+
|
|
14
|
+
// @shared formatBytes
|
|
15
|
+
function formatBytes(bytes) {
|
|
16
|
+
if (bytes === null || bytes === undefined || Number.isNaN(bytes)) return "-";
|
|
17
|
+
if (bytes === 0) return "0 B";
|
|
18
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
19
|
+
const i = Math.max(0, Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1));
|
|
20
|
+
return (bytes / Math.pow(1024, i)).toFixed(1) + " " + units[i];
|
|
21
|
+
}
|
|
22
|
+
// @end-shared formatBytes
|
|
23
|
+
|
|
24
|
+
// A size that misses some resources is a lower bound, and one that misses all of them is unknown
|
|
25
|
+
const sizeLabel = (bytes, unknown, total) => {
|
|
26
|
+
if (total > 0 && unknown === total) return "unknown";
|
|
27
|
+
return (unknown > 0 ? "≥ " : "") + formatBytes(bytes);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// @shared getRootDomain
|
|
31
|
+
function getRootDomain(hostname) {
|
|
32
|
+
const host = hostname.replace(/\.$/, "");
|
|
33
|
+
// An IP address has no registrable domain, so each address is its own root
|
|
34
|
+
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(host) || host.includes(":")) return host;
|
|
35
|
+
const parts = host.split(".");
|
|
36
|
+
if (parts.length <= 2) return host;
|
|
37
|
+
// Country-code domains with a second-level suffix: example.co.uk, example.com.au, example.ac.jp
|
|
38
|
+
const secondLevelSuffixes = ["ac", "co", "com", "edu", "go", "gob", "gouv", "gov", "govt", "mil", "ne", "net", "nom", "or", "org", "sch"];
|
|
39
|
+
const tld = parts[parts.length - 1];
|
|
40
|
+
const sld = parts[parts.length - 2];
|
|
41
|
+
if (tld.length === 2 && secondLevelSuffixes.includes(sld)) return parts.slice(-3).join(".");
|
|
42
|
+
return parts.slice(-2).join(".");
|
|
43
|
+
}
|
|
44
|
+
// @end-shared getRootDomain
|
|
45
|
+
|
|
46
|
+
// @shared isFirstParty
|
|
47
|
+
function isFirstParty(hostname) {
|
|
48
|
+
const root = getRootDomain(hostname);
|
|
49
|
+
if (root === getRootDomain(location.hostname)) return true;
|
|
50
|
+
return OWN_DOMAINS.some((d) => getRootDomain(String(d).trim().toLowerCase().replace(/^[a-z]+:\/\//, "").split("/")[0]) === root);
|
|
51
|
+
}
|
|
52
|
+
// @end-shared isFirstParty
|
|
53
|
+
|
|
54
|
+
// @shared logOwnDomainsHint
|
|
55
|
+
function logOwnDomainsHint(thirdPartyCount) {
|
|
56
|
+
if (OWN_DOMAINS.length === 0 && thirdPartyCount > 0) {
|
|
57
|
+
console.log(
|
|
58
|
+
'%cℹ️ OWN_DOMAINS is empty. If this site serves its own assets from other domains (for example a CDN), add them at the top of the snippet, such as const OWN_DOMAINS = ["cdn.example.net"];, and run it again so they count as first party.',
|
|
59
|
+
"color: #3b82f6;"
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// @end-shared logOwnDomainsHint
|
|
64
|
+
|
|
65
|
+
// Root domain of an http(s) URL, or null for anything else (data:, blob:, empty)
|
|
66
|
+
const rootDomainOf = (url) => {
|
|
67
|
+
try {
|
|
68
|
+
const u = new URL(url);
|
|
69
|
+
return /^https?:$/.test(u.protocol) ? { root: getRootDomain(u.hostname), first: isFirstParty(u.hostname) } : null;
|
|
70
|
+
} catch {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const domains = new Map();
|
|
76
|
+
const domainEntry = (root) => {
|
|
77
|
+
if (!domains.has(root)) {
|
|
78
|
+
domains.set(root, { domain: root, requests: 0, transferBytes: 0, sizeUnknownCount: 0, renderBlocking: false, loafMs: null, loafScripts: 0 });
|
|
79
|
+
}
|
|
80
|
+
return domains.get(root);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
let firstPartyRequests = 0;
|
|
84
|
+
let thirdPartyRequests = 0;
|
|
85
|
+
let thirdPartyBytes = 0;
|
|
86
|
+
let sizeUnknownCount = 0;
|
|
87
|
+
|
|
88
|
+
performance.getEntriesByType("resource").forEach((entry) => {
|
|
89
|
+
const parsed = rootDomainOf(entry.name);
|
|
90
|
+
if (!parsed) return;
|
|
91
|
+
if (parsed.first) {
|
|
92
|
+
firstPartyRequests++;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const d = domainEntry(parsed.root);
|
|
96
|
+
d.requests++;
|
|
97
|
+
thirdPartyRequests++;
|
|
98
|
+
|
|
99
|
+
// Cross-origin resources without Timing-Allow-Origin report every size as zero
|
|
100
|
+
const sizeKnown = entry.transferSize > 0 || entry.encodedBodySize > 0 || entry.decodedBodySize > 0;
|
|
101
|
+
if (sizeKnown) {
|
|
102
|
+
const bytes = entry.transferSize || entry.encodedBodySize || 0;
|
|
103
|
+
d.transferBytes += bytes;
|
|
104
|
+
thirdPartyBytes += bytes;
|
|
105
|
+
} else {
|
|
106
|
+
d.sizeUnknownCount++;
|
|
107
|
+
sizeUnknownCount++;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (entry.renderBlockingStatus === "blocking" && (entry.initiatorType === "script" || entry.initiatorType === "link")) {
|
|
111
|
+
d.renderBlocking = true;
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// Main-thread time: script time inside long animation frames, attributed by the script's source URL
|
|
116
|
+
const loafSupported = !!PerformanceObserver.supportedEntryTypes?.includes("long-animation-frame");
|
|
117
|
+
let thirdPartyLoafMs = null;
|
|
118
|
+
let longFrames = null;
|
|
119
|
+
let longFramesMs = null;
|
|
120
|
+
if (loafSupported) {
|
|
121
|
+
thirdPartyLoafMs = 0;
|
|
122
|
+
domains.forEach((d) => {
|
|
123
|
+
d.loafMs = 0;
|
|
124
|
+
});
|
|
125
|
+
const frames = performance.getEntriesByType("long-animation-frame");
|
|
126
|
+
longFrames = frames.length;
|
|
127
|
+
longFramesMs = frames.reduce((total, frame) => total + frame.duration, 0);
|
|
128
|
+
frames.forEach((frame) => {
|
|
129
|
+
(frame.scripts || []).forEach((script) => {
|
|
130
|
+
const parsed = rootDomainOf(script.sourceURL);
|
|
131
|
+
if (!parsed || parsed.first) return;
|
|
132
|
+
const d = domainEntry(parsed.root);
|
|
133
|
+
d.loafMs = (d.loafMs || 0) + script.duration;
|
|
134
|
+
d.loafScripts++;
|
|
135
|
+
thirdPartyLoafMs += script.duration;
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
domains.forEach((d) => {
|
|
139
|
+
d.loafMs = Math.round(d.loafMs || 0);
|
|
140
|
+
});
|
|
141
|
+
thirdPartyLoafMs = Math.round(thirdPartyLoafMs);
|
|
142
|
+
longFramesMs = Math.round(longFramesMs);
|
|
143
|
+
}
|
|
144
|
+
// Time in long frames that no third-party script accounts for: first-party scripts, scripts under
|
|
145
|
+
// the 5 ms that Long Animation Frames list, and style, layout and rendering work
|
|
146
|
+
const unattributedLoafMs = loafSupported ? Math.max(0, longFramesMs - thirdPartyLoafMs) : null;
|
|
147
|
+
const loafUnattributed = loafSupported && domains.size > 0 && thirdPartyLoafMs === 0 && longFrames > 0;
|
|
148
|
+
|
|
149
|
+
// Render-blocking first, then main-thread time, then transfer size
|
|
150
|
+
const all = [...domains.values()].sort(
|
|
151
|
+
(a, b) =>
|
|
152
|
+
Number(b.renderBlocking) - Number(a.renderBlocking) ||
|
|
153
|
+
(b.loafMs || 0) - (a.loafMs || 0) ||
|
|
154
|
+
b.transferBytes - a.transferBytes ||
|
|
155
|
+
a.domain.localeCompare(b.domain)
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
const blocking = all.filter((d) => d.renderBlocking);
|
|
159
|
+
const heavy = all.filter((d) => (d.loafMs || 0) > LOAF_WARNING_MS);
|
|
160
|
+
const corsLimitedAnalysis = sizeUnknownCount > 0;
|
|
161
|
+
|
|
162
|
+
const listDomains = (list) => {
|
|
163
|
+
const names = list.slice(0, MAX_LISTED_DOMAINS).map((d) => d.domain);
|
|
164
|
+
return names.join(", ") + (list.length > MAX_LISTED_DOMAINS ? ` and ${list.length - MAX_LISTED_DOMAINS} more` : "");
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const issues = [];
|
|
168
|
+
if (blocking.length > 0) {
|
|
169
|
+
issues.push({
|
|
170
|
+
severity: "warning",
|
|
171
|
+
message: `${blocking.length} third-party domain(s) serve render-blocking resources (${listDomains(blocking)}); load them async or defer, or self-host them`,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
heavy.slice(0, MAX_LISTED_DOMAINS).forEach((d) => {
|
|
175
|
+
issues.push({
|
|
176
|
+
severity: "warning",
|
|
177
|
+
message: `${d.domain} scripts take ${d.loafMs} ms of main-thread time in long animation frames (over ${LOAF_WARNING_MS} ms); defer, delay or remove them`,
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
if (loafUnattributed) {
|
|
181
|
+
issues.push({
|
|
182
|
+
severity: "info",
|
|
183
|
+
message: `${longFrames} long animation frame(s) took ${longFramesMs} ms, but no third-party script was attributed in them (Long Animation Frames only list scripts over 5 ms), so 0 ms per domain does not rule out third-party cost`,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
if (corsLimitedAnalysis) {
|
|
187
|
+
issues.push({
|
|
188
|
+
severity: "info",
|
|
189
|
+
message: `${sizeUnknownCount} third-party request(s) without Timing-Allow-Origin report zero sizes, so the transfer size is a lower bound`,
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
console.group("%c🌐 Third-Party Impact by Domain", "font-weight: bold; font-size: 14px;");
|
|
194
|
+
console.log(
|
|
195
|
+
`Third-party domains: ${all.length} | requests: ${thirdPartyRequests} | transfer: ${sizeLabel(thirdPartyBytes, sizeUnknownCount, thirdPartyRequests)} | first-party requests: ${firstPartyRequests}`
|
|
196
|
+
);
|
|
197
|
+
if (all.length > 0) {
|
|
198
|
+
console.table(
|
|
199
|
+
all.slice(0, MAX_ITEMS).map((d) => ({
|
|
200
|
+
Domain: d.domain,
|
|
201
|
+
Requests: d.requests,
|
|
202
|
+
Size: sizeLabel(d.transferBytes, d.sizeUnknownCount, d.requests),
|
|
203
|
+
"Render-blocking": d.renderBlocking ? "yes" : "no",
|
|
204
|
+
"Main thread": d.loafMs === null ? "n/a" : d.loafMs + " ms",
|
|
205
|
+
}))
|
|
206
|
+
);
|
|
207
|
+
} else {
|
|
208
|
+
console.log("%c✅ No third-party requests found", "color: #22c55e;");
|
|
209
|
+
}
|
|
210
|
+
if (!loafSupported) {
|
|
211
|
+
console.log("%cℹ️ Long Animation Frames are not supported; main-thread time is unavailable (Chrome 123+)", "color: #3b82f6;");
|
|
212
|
+
}
|
|
213
|
+
if (loafUnattributed) {
|
|
214
|
+
console.log(
|
|
215
|
+
`%cℹ️ ${longFrames} long animation frame(s) took ${longFramesMs} ms, but no third-party script was attributed in them (Long Animation Frames only list scripts over 5 ms), so 0 ms per domain does not rule out third-party cost`,
|
|
216
|
+
"color: #3b82f6;"
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (corsLimitedAnalysis) {
|
|
220
|
+
console.log("%cℹ️ Some third-party sizes are hidden (no Timing-Allow-Origin); sizes are a lower bound", "color: #3b82f6;");
|
|
221
|
+
}
|
|
222
|
+
logOwnDomainsHint(all.length);
|
|
223
|
+
console.groupEnd();
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
script: "Third-Party-Impact-by-Domain",
|
|
227
|
+
status: "ok",
|
|
228
|
+
count: all.length,
|
|
229
|
+
corsLimitedAnalysis,
|
|
230
|
+
details: {
|
|
231
|
+
thirdPartyRequests,
|
|
232
|
+
thirdPartyBytes,
|
|
233
|
+
thirdPartyLoafMs,
|
|
234
|
+
longFrames,
|
|
235
|
+
unattributedLoafMs,
|
|
236
|
+
firstPartyRequests,
|
|
237
|
+
blockingDomains: blocking.length,
|
|
238
|
+
loafSupported,
|
|
239
|
+
sizeUnknownCount,
|
|
240
|
+
},
|
|
241
|
+
items: all.slice(0, MAX_ITEMS),
|
|
242
|
+
issues,
|
|
243
|
+
};
|
|
244
|
+
})();
|
|
@@ -378,6 +378,23 @@
|
|
|
378
378
|
|
|
379
379
|
console.groupEnd();
|
|
380
380
|
|
|
381
|
+
// One item per script (an issue replaces the valid entry of the same URL), at most 50
|
|
382
|
+
const itemsByUrl = new Map();
|
|
383
|
+
validPreloads.forEach((v) => itemsByUrl.set(v.url, {
|
|
384
|
+
url: v.shortUrl,
|
|
385
|
+
type: v.type === "dynamic" ? "dynamic" : v.type === "mitigated" ? "mitigated" : "blocking",
|
|
386
|
+
strategy: v.attributes || "blocking-script",
|
|
387
|
+
location: v.location || "unknown",
|
|
388
|
+
reviewNote: v.reviewNote || v.note || "",
|
|
389
|
+
}));
|
|
390
|
+
issues.forEach((i) => itemsByUrl.set(i.url, {
|
|
391
|
+
url: i.shortUrl,
|
|
392
|
+
type: "issue",
|
|
393
|
+
strategy: i.attributes || i.type,
|
|
394
|
+
location: i.location || "unknown",
|
|
395
|
+
reviewNote: i.fix || "",
|
|
396
|
+
}));
|
|
397
|
+
|
|
381
398
|
return {
|
|
382
399
|
script: "Validate-Preload-Async-Defer-Scripts",
|
|
383
400
|
status: "ok",
|
|
@@ -386,9 +403,10 @@
|
|
|
386
403
|
validCount: validPreloads.length,
|
|
387
404
|
issueCount: issues.length,
|
|
388
405
|
},
|
|
389
|
-
items:
|
|
390
|
-
|
|
406
|
+
items: Array.from(itemsByUrl.values()).slice(0, 50),
|
|
407
|
+
issues: issues.map(i => ({
|
|
408
|
+
severity: i.severity,
|
|
409
|
+
message: i.fix ? `${i.message.replace(/\.$/, "")}. Fix: ${i.fix}` : i.message,
|
|
391
410
|
})),
|
|
392
|
-
issues: issues.map(i => ({ severity: i.severity, message: i.message })),
|
|
393
411
|
};
|
|
394
412
|
})();
|
|
@@ -19,6 +19,7 @@ When TTFB is slow or the user asks "why is my server slow":
|
|
|
19
19
|
2. **TTFB-Sub-Parts.js** - Break down into DNS, connection, server time
|
|
20
20
|
3. **Service-Worker-Analysis.js** - Check for SW overhead impacting TTFB
|
|
21
21
|
4. **TTFB-Resources.js** - Identify slow third-party or API endpoints
|
|
22
|
+
5. **Server-Timing-Early-Hints.js** - Read backend phases from `Server-Timing` and check 103 Early Hints
|
|
22
23
|
|
|
23
24
|
### Font Loading Optimization
|
|
24
25
|
|
|
@@ -27,6 +28,15 @@ When the user asks about fonts, FOIT, FOUT, or font performance:
|
|
|
27
28
|
1. **Fonts-Preloaded-Loaded-and-used-above-the-fold.js** - Full font audit
|
|
28
29
|
2. **Resource-Hints-Validation.js** - Verify font preloads are correct
|
|
29
30
|
3. **Find-render-blocking-resources.js** - Check if fonts block rendering
|
|
31
|
+
4. **Webfont-Usage-Analyzer.js** - Find font faces that are unused, never loaded, or use a blocking `font-display`
|
|
32
|
+
|
|
33
|
+
### Text Compression Audit
|
|
34
|
+
|
|
35
|
+
When the user asks about compression, gzip, brotli, or large transfer sizes for text resources:
|
|
36
|
+
|
|
37
|
+
1. **Compression-Audit.js** - Find text resources served without compression and estimate savings
|
|
38
|
+
2. **Find-render-blocking-resources.js** - Prioritize uncompressed CSS/JS that block rendering
|
|
39
|
+
3. **TTFB-Resources.js** - Check server response of the uncompressed resources
|
|
30
40
|
|
|
31
41
|
### Script Performance Deep Dive
|
|
32
42
|
|
|
@@ -34,6 +44,7 @@ When scripts are suspected to slow down the page:
|
|
|
34
44
|
|
|
35
45
|
1. **Script-Loading.js** - Identify blocking scripts and loading strategy
|
|
36
46
|
2. **First-And-Third-Party-Script-Info.js** - Separate first vs third-party impact
|
|
47
|
+
- **Third-Party-Impact-by-Domain.js** - Rank third-party domains by render-blocking, main-thread time and size
|
|
37
48
|
3. **First-And-Third-Party-Script-Timings.js** - Diagnose slow script connections
|
|
38
49
|
4. **JS-Execution-Time-Breakdown.js** - Network vs parse/execution time
|
|
39
50
|
5. **Inline-Script-Info-and-Size.js** - Measure inline script overhead
|
|
@@ -48,6 +59,7 @@ When the user wants to optimize resource loading priorities:
|
|
|
48
59
|
3. **Priority-Hints-Audit.js** - Check fetchpriority usage
|
|
49
60
|
4. **Prefetch-Resource-Validation.js** - Validate prefetch strategy
|
|
50
61
|
5. **Validate-Preload-Async-Defer-Scripts.js** - Find conflicts
|
|
62
|
+
6. **Speculation-Rules-Inspector.js** - Inspect prefetch/prerender rules and prerender activation
|
|
51
63
|
|
|
52
64
|
### CSS Optimization Workflow
|
|
53
65
|
|
|
@@ -76,6 +88,13 @@ Use this decision tree to automatically run follow-up snippets based on results:
|
|
|
76
88
|
- **If TTFB > 600ms** → Run **TTFB-Sub-Parts.js** to diagnose where time is spent
|
|
77
89
|
- **If Service Worker detected** → Run **Service-Worker-Analysis.js** to check for SW overhead
|
|
78
90
|
- **If TTFB varies significantly across resources** → Run **TTFB-Resources.js**
|
|
91
|
+
- **If server time dominates TTFB-Sub-Parts.js** → Run **Server-Timing-Early-Hints.js** to see which backend phase is slow
|
|
92
|
+
|
|
93
|
+
### After Server-Timing-Early-Hints.js
|
|
94
|
+
|
|
95
|
+
- **If no `Server-Timing` metrics found** → Recommend adding `Server-Timing` headers for database, cache and render phases
|
|
96
|
+
- **If cross-origin resources look hidden** → Recommend `Timing-Allow-Origin` on those responses
|
|
97
|
+
- **If no 103 Early Hints received and server time is high** → Recommend Early Hints for the main stylesheet and LCP image
|
|
79
98
|
|
|
80
99
|
### After FCP.js
|
|
81
100
|
|
|
@@ -109,6 +128,19 @@ Use this decision tree to automatically run follow-up snippets based on results:
|
|
|
109
128
|
2. **Priority-Hints-Audit.js** (LCP candidate)
|
|
110
129
|
- **If preloads on async/defer scripts** → Run **Validate-Preload-Async-Defer-Scripts.js**
|
|
111
130
|
|
|
131
|
+
### After Compression-Audit.js
|
|
132
|
+
|
|
133
|
+
- **If uncompressed CSS/JS found** → Run **Find-render-blocking-resources.js** to see if they block rendering
|
|
134
|
+
- **If estimated savings > 100KB** → Recommend enabling gzip or brotli on the server or CDN
|
|
135
|
+
- **If corsLimitedAnalysis is true** → Report the result as a lower bound and suggest `Timing-Allow-Origin` on resources you control
|
|
136
|
+
|
|
137
|
+
### After Third-Party-Impact-by-Domain.js
|
|
138
|
+
|
|
139
|
+
- **If a domain is render-blocking** → Recommend `async`, `defer`, lazy loading or self-hosting; run **Script-Loading.js** for the loading strategy
|
|
140
|
+
- **If a domain's main-thread time > 250ms** → Run **Long-Animation-Frames-Script-Attribution.js** (webperf-interaction) to find the functions responsible, and recommend delaying or removing the script
|
|
141
|
+
- **If many domains with few requests each** → Recommend auditing tags and removing unused ones
|
|
142
|
+
- **If corsLimitedAnalysis is true** → Report sizes as a lower bound; use **First-And-Third-Party-Script-Timings.js** for timings of the hidden resources
|
|
143
|
+
|
|
112
144
|
### After Service-Worker-Analysis.js
|
|
113
145
|
|
|
114
146
|
- **If SW overhead > 100ms** → Recommend Navigation Preload API
|
|
@@ -119,7 +151,14 @@ Use this decision tree to automatically run follow-up snippets based on results:
|
|
|
119
151
|
|
|
120
152
|
- **If fonts preloaded but not used above-the-fold** → Recommend removing preloads
|
|
121
153
|
- **If fonts used but not preloaded** → Recommend adding preload
|
|
122
|
-
- **If many font variants loaded** → Suggest subsetting or reducing variants
|
|
154
|
+
- **If many font variants loaded** → Suggest subsetting or reducing variants, and run **Webfont-Usage-Analyzer.js** to find the unused ones
|
|
155
|
+
|
|
156
|
+
### After Webfont-Usage-Analyzer.js
|
|
157
|
+
|
|
158
|
+
- **If fonts loaded but not used** → Recommend removing the `@font-face` declaration or the preload
|
|
159
|
+
- **If faces use `font-display: auto` or `block`** → Recommend `swap` or `optional`, then check **Find-render-blocking-resources.js**
|
|
160
|
+
- **If font bytes are high** → Suggest subsetting with `unicode-range` and WOFF2
|
|
161
|
+
- **If the LCP element is text** → Run **LCP.js** with the **webperf-core-web-vitals** skill
|
|
123
162
|
|
|
124
163
|
### After First-And-Third-Party-Script-Info.js
|
|
125
164
|
|
|
@@ -145,6 +184,13 @@ Use this decision tree to automatically run follow-up snippets based on results:
|
|
|
145
184
|
- **If >10 prefetch hints** → Recommend reducing to critical resources
|
|
146
185
|
- **If individual prefetch > 500KB** → Question necessity
|
|
147
186
|
- **If total prefetch > 2MB** → Flag as mobile bandwidth concern
|
|
187
|
+
- **If few or no prefetch hints for likely next navigations** → Run **Speculation-Rules-Inspector.js** to check for prefetch/prerender rules
|
|
188
|
+
|
|
189
|
+
### After Speculation-Rules-Inspector.js
|
|
190
|
+
|
|
191
|
+
- **If no speculation rules found but the site has predictable navigations** → Recommend adding prefetch or prerender rules
|
|
192
|
+
- **If invalid JSON or wasteful rules (eager prerender of many URLs)** → Recommend fixing or lowering eagerness to `moderate` or `conservative`
|
|
193
|
+
- **If the page was prerendered (`activationStart` > 0)** → Subtract `activationStart` when interpreting **TTFB.js**, **FCP.js** and LCP results
|
|
148
194
|
|
|
149
195
|
### After Critical-CSS-Detection.js
|
|
150
196
|
|