webperf-snippets 0.2.0 → 0.3.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 +120 -5
- 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 +52 -30
- 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 +40 -20
- package/snippets/Loading/First-And-Third-Party-Script-Timings.js +21 -13
- package/snippets/Loading/Fonts-Preloaded-Loaded-and-used-above-the-fold.js +35 -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 +15 -0
- package/snippets/Loading/Resource-Hints.js +29 -8
- package/snippets/Loading/SSR-Hydration-Data-Analysis.js +77 -8
- package/snippets/Loading/Script-Loading.js +55 -25
- 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 +59 -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 +216 -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 +22 -3
- 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 +1176 -0
- package/snippets/_shared/helpers.js +44 -0
- package/src/bin.js +55 -3
- package/src/crux.js +84 -0
- package/src/report-to.js +46 -0
- package/src/reporters/human.js +20 -1
- package/src/reporters/markdown.js +22 -1
- package/src/runner.js +4 -2
|
@@ -179,7 +179,9 @@
|
|
|
179
179
|
script: "Forced-Synchronous-Layout",
|
|
180
180
|
status: "ok",
|
|
181
181
|
count: 0,
|
|
182
|
-
details: {
|
|
182
|
+
details: {},
|
|
183
|
+
items: [],
|
|
184
|
+
issues: [],
|
|
183
185
|
};
|
|
184
186
|
}
|
|
185
187
|
|
|
@@ -235,13 +237,37 @@
|
|
|
235
237
|
fastestSinceLastMutationMs: Math.min(
|
|
236
238
|
...fslEvents.map((e) => e.sinceLastMutationMs)
|
|
237
239
|
),
|
|
238
|
-
fslEvents: fslEvents.map(({ property, accessType, element, sinceLastMutationMs }) => ({
|
|
239
|
-
property,
|
|
240
|
-
accessType,
|
|
241
|
-
element,
|
|
242
|
-
sinceLastMutationMs,
|
|
243
|
-
})),
|
|
244
240
|
},
|
|
241
|
+
// One item per property and element, most frequent first (at most 50)
|
|
242
|
+
items: Object.values(
|
|
243
|
+
fslEvents.reduce((acc, e) => {
|
|
244
|
+
const key = `${e.property}|${e.accessType}|${e.element}`;
|
|
245
|
+
if (!acc[key]) {
|
|
246
|
+
acc[key] = {
|
|
247
|
+
property: e.property,
|
|
248
|
+
accessType: e.accessType,
|
|
249
|
+
element: e.element,
|
|
250
|
+
count: 0,
|
|
251
|
+
fastestSinceLastMutationMs: e.sinceLastMutationMs,
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
acc[key].count++;
|
|
255
|
+
acc[key].fastestSinceLastMutationMs = Math.min(
|
|
256
|
+
acc[key].fastestSinceLastMutationMs,
|
|
257
|
+
e.sinceLastMutationMs
|
|
258
|
+
);
|
|
259
|
+
return acc;
|
|
260
|
+
}, {})
|
|
261
|
+
)
|
|
262
|
+
.sort((a, b) => b.count - a.count)
|
|
263
|
+
.slice(0, 50),
|
|
264
|
+
issues: Object.entries(byProperty)
|
|
265
|
+
.sort((a, b) => b[1] - a[1])
|
|
266
|
+
.slice(0, 10)
|
|
267
|
+
.map(([property, count]) => ({
|
|
268
|
+
severity: count > 5 ? "error" : "warning",
|
|
269
|
+
message: `${property} triggered ${count} forced synchronous layout${count > 1 ? "s" : ""}. Read layout properties before writing to the DOM.`,
|
|
270
|
+
})),
|
|
245
271
|
};
|
|
246
272
|
};
|
|
247
273
|
|
|
@@ -81,7 +81,12 @@
|
|
|
81
81
|
console.log(
|
|
82
82
|
" Interact with the page (click, type, etc.) and call this again."
|
|
83
83
|
);
|
|
84
|
-
return
|
|
84
|
+
return {
|
|
85
|
+
script: "Input-Latency-Breakdown",
|
|
86
|
+
status: "error",
|
|
87
|
+
error: "No interactions recorded yet. Interact with the page (click, type, etc.) and call getInputLatencyBreakdown() again.",
|
|
88
|
+
getDataFn: "getInputLatencyBreakdown",
|
|
89
|
+
};
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
console.group(
|
|
@@ -171,11 +176,50 @@
|
|
|
171
176
|
};
|
|
172
177
|
}
|
|
173
178
|
console.groupEnd();
|
|
179
|
+
|
|
180
|
+
const BOTTLENECK_FIXES = {
|
|
181
|
+
"Input Delay": "break up the long tasks that block the main thread (scheduler.yield(), setTimeout)",
|
|
182
|
+
Processing: "optimize the event handlers or debounce them",
|
|
183
|
+
Presentation: "reduce the DOM changes after the handler and avoid layout thrashing",
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const items = types.map((type) => {
|
|
187
|
+
const b = byEventType[type];
|
|
188
|
+
return {
|
|
189
|
+
type,
|
|
190
|
+
count: b.count,
|
|
191
|
+
p75Ms: Math.round(p75(b.durations)),
|
|
192
|
+
inputDelayMs: Math.round(p75(b.inputDelays)),
|
|
193
|
+
processingMs: Math.round(p75(b.processingTimes)),
|
|
194
|
+
presentationMs: Math.round(p75(b.presentationDelays)),
|
|
195
|
+
rating: valueToRating(p75(b.durations)),
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
const issues = types
|
|
200
|
+
.filter((type) => valueToRating(p75(byEventType[type].durations)) !== "good")
|
|
201
|
+
.map((type) => {
|
|
202
|
+
const b = byEventType[type];
|
|
203
|
+
const phases = [
|
|
204
|
+
{ name: "Input Delay", value: p75(b.inputDelays) },
|
|
205
|
+
{ name: "Processing", value: p75(b.processingTimes) },
|
|
206
|
+
{ name: "Presentation", value: p75(b.presentationDelays) },
|
|
207
|
+
];
|
|
208
|
+
const bottleneck = phases.reduce((a, c) => (a.value > c.value ? a : c));
|
|
209
|
+
const rating = valueToRating(p75(b.durations));
|
|
210
|
+
return {
|
|
211
|
+
severity: rating === "poor" ? "error" : "warning",
|
|
212
|
+
message: `${type} P75 is ${Math.round(p75(b.durations))}ms (${rating}). Bottleneck: ${bottleneck.name}. Fix: ${BOTTLENECK_FIXES[bottleneck.name]}.`,
|
|
213
|
+
};
|
|
214
|
+
});
|
|
215
|
+
|
|
174
216
|
return {
|
|
175
217
|
script: "Input-Latency-Breakdown",
|
|
176
218
|
status: "ok",
|
|
177
219
|
count: types.length,
|
|
178
220
|
details: { eventTypes: typeSummary },
|
|
221
|
+
items,
|
|
222
|
+
issues,
|
|
179
223
|
};
|
|
180
224
|
};
|
|
181
225
|
|
|
@@ -199,11 +199,22 @@
|
|
|
199
199
|
p75Ms: Math.round(p75),
|
|
200
200
|
byRating,
|
|
201
201
|
},
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
202
|
+
// Slowest first, at most 50
|
|
203
|
+
items: [...allInteractions]
|
|
204
|
+
.sort((a, b) => b.duration - a.duration)
|
|
205
|
+
.slice(0, 50)
|
|
206
|
+
.map((i) => ({
|
|
207
|
+
type: i.type,
|
|
208
|
+
durationMs: Math.round(i.duration),
|
|
209
|
+
rating: i.rating,
|
|
210
|
+
})),
|
|
211
|
+
issues: [...slowInteractions]
|
|
212
|
+
.sort((a, b) => b.duration - a.duration)
|
|
213
|
+
.slice(0, 10)
|
|
214
|
+
.map((i) => ({
|
|
215
|
+
severity: i.rating === "poor" ? "error" : "warning",
|
|
216
|
+
message: `${i.type} interaction took ${Math.round(i.duration)}ms (${i.rating}), above the ${i.rating === "poor" ? "500ms" : "200ms"} threshold`,
|
|
217
|
+
})),
|
|
207
218
|
};
|
|
208
219
|
};
|
|
209
220
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// Layout Shift Tracking
|
|
2
2
|
// https://webperf-snippets.nucliweb.net
|
|
3
3
|
|
|
4
|
-
(() => {
|
|
4
|
+
(async () => {
|
|
5
|
+
if (!PerformanceObserver.supportedEntryTypes?.includes("layout-shift")) {
|
|
6
|
+
console.warn("⚠️ layout-shift entries are not supported in this browser.");
|
|
7
|
+
return { script: "Layout-Shift-Loading-and-Interaction", status: "unsupported", error: "layout-shift entries not supported in this browser" };
|
|
8
|
+
}
|
|
5
9
|
// CLS thresholds
|
|
6
10
|
const valueToRating = (score) =>
|
|
7
11
|
score <= 0.1 ? "good" : score <= 0.25 ? "needs-improvement" : "poor";
|
|
@@ -18,11 +22,26 @@
|
|
|
18
22
|
poor: "🔴",
|
|
19
23
|
};
|
|
20
24
|
|
|
21
|
-
// Track all shifts
|
|
25
|
+
// Track all shifts. CLS is the largest session window: shifts less than 1s apart,
|
|
26
|
+
// in a window of at most 5s.
|
|
22
27
|
let totalCLS = 0;
|
|
28
|
+
let sessionValue = 0;
|
|
29
|
+
let sessionFirst = null;
|
|
30
|
+
let sessionLast = null;
|
|
23
31
|
const allShifts = [];
|
|
24
32
|
const elementShifts = new Map(); // Track shifts per element
|
|
25
33
|
|
|
34
|
+
// The five elements that shifted the most (same value at start and in the summary)
|
|
35
|
+
const getTopElements = () =>
|
|
36
|
+
Array.from(elementShifts.entries())
|
|
37
|
+
.sort((a, b) => b[1].totalShift - a[1].totalShift)
|
|
38
|
+
.slice(0, 5)
|
|
39
|
+
.map(([selector, data]) => ({
|
|
40
|
+
selector,
|
|
41
|
+
shiftCount: data.count,
|
|
42
|
+
totalImpact: Math.round(data.totalShift * 10000) / 10000,
|
|
43
|
+
}));
|
|
44
|
+
|
|
26
45
|
const getElementSelector = (element) => {
|
|
27
46
|
if (!element) return "(unknown)";
|
|
28
47
|
if (element.id) return `#${element.id}`;
|
|
@@ -39,7 +58,18 @@
|
|
|
39
58
|
const countedForCLS = !entry.hadRecentInput;
|
|
40
59
|
|
|
41
60
|
if (countedForCLS) {
|
|
42
|
-
|
|
61
|
+
if (
|
|
62
|
+
sessionLast &&
|
|
63
|
+
entry.startTime - sessionLast.startTime < 1000 &&
|
|
64
|
+
entry.startTime - sessionFirst.startTime < 5000
|
|
65
|
+
) {
|
|
66
|
+
sessionValue += entry.value;
|
|
67
|
+
} else {
|
|
68
|
+
sessionValue = entry.value;
|
|
69
|
+
sessionFirst = entry;
|
|
70
|
+
}
|
|
71
|
+
sessionLast = entry;
|
|
72
|
+
totalCLS = Math.max(totalCLS, sessionValue);
|
|
43
73
|
}
|
|
44
74
|
|
|
45
75
|
// Get affected elements
|
|
@@ -81,7 +111,7 @@
|
|
|
81
111
|
`font-weight: bold; color: ${countedForCLS ? clsColor : "#888"};`
|
|
82
112
|
);
|
|
83
113
|
|
|
84
|
-
console.log(`%
|
|
114
|
+
console.log(`%cCurrent CLS: ${totalCLS.toFixed(4)}`, `color: ${clsColor}; font-weight: bold;`);
|
|
85
115
|
console.log(` Time: ${Math.round(entry.startTime)}ms`);
|
|
86
116
|
console.log(` Had recent input: ${entry.hadRecentInput ? "Yes (excluded from CLS)" : "No"}`);
|
|
87
117
|
|
|
@@ -205,10 +235,7 @@
|
|
|
205
235
|
|
|
206
236
|
console.groupEnd();
|
|
207
237
|
|
|
208
|
-
const topElementsData =
|
|
209
|
-
.sort((a, b) => b[1].totalShift - a[1].totalShift)
|
|
210
|
-
.slice(0, 5)
|
|
211
|
-
.map(([selector, data]) => ({ selector, shiftCount: data.count, totalImpact: Math.round(data.totalShift * 10000) / 10000 }));
|
|
238
|
+
const topElementsData = getTopElements();
|
|
212
239
|
return {
|
|
213
240
|
script: "Layout-Shift-Loading-and-Interaction",
|
|
214
241
|
status: "ok",
|
|
@@ -227,6 +254,10 @@
|
|
|
227
254
|
};
|
|
228
255
|
};
|
|
229
256
|
|
|
257
|
+
// Buffered entries reach the observer callback asynchronously; wait for them so the
|
|
258
|
+
// summary below (and the return value) reflect shifts that happened before this ran.
|
|
259
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
260
|
+
|
|
230
261
|
// Initial message
|
|
231
262
|
const rating = valueToRating(totalCLS);
|
|
232
263
|
const icon = RATING_ICONS[rating];
|
|
@@ -240,11 +271,10 @@
|
|
|
240
271
|
""
|
|
241
272
|
);
|
|
242
273
|
|
|
243
|
-
//
|
|
244
|
-
const clsBufferedSync =
|
|
245
|
-
|
|
246
|
-
const
|
|
247
|
-
const excludedSync = performance.getEntriesByType("layout-shift").filter((e) => e.hadRecentInput).length;
|
|
274
|
+
// Return for agent (shifts collected by the buffered observer above)
|
|
275
|
+
const clsBufferedSync = totalCLS;
|
|
276
|
+
const countedSync = allShifts.filter((s) => s.countedForCLS).length;
|
|
277
|
+
const excludedSync = allShifts.length - countedSync;
|
|
248
278
|
const clsRatingSync = valueToRating(clsBufferedSync);
|
|
249
279
|
return {
|
|
250
280
|
script: "Layout-Shift-Loading-and-Interaction",
|
|
@@ -259,6 +289,7 @@
|
|
|
259
289
|
shiftCount: countedSync + excludedSync,
|
|
260
290
|
countedShifts: countedSync,
|
|
261
291
|
excludedShifts: excludedSync,
|
|
292
|
+
topElements: getTopElements(),
|
|
262
293
|
},
|
|
263
294
|
message: "Layout shift tracking active. Call getLayoutShiftSummary() for full element attribution.",
|
|
264
295
|
getDataFn: "getLayoutShiftSummary",
|
|
@@ -354,6 +354,33 @@
|
|
|
354
354
|
return capturedFrames;
|
|
355
355
|
},
|
|
356
356
|
|
|
357
|
+
/**
|
|
358
|
+
* Get a structured summary (the value agents read through getDataFn)
|
|
359
|
+
*/
|
|
360
|
+
getData() {
|
|
361
|
+
const blocking = capturedFrames.filter((f) => f.blockingDuration > 0);
|
|
362
|
+
const worst = capturedFrames.reduce((max, f) => Math.max(max, f.blockingDuration), 0);
|
|
363
|
+
return {
|
|
364
|
+
script: "Long-Animation-Frames-Helpers",
|
|
365
|
+
status: "ok",
|
|
366
|
+
count: capturedFrames.length,
|
|
367
|
+
details: {
|
|
368
|
+
withBlockingTime: blocking.length,
|
|
369
|
+
totalBlockingTimeMs: Math.round(blocking.reduce((sum, f) => sum + f.blockingDuration, 0)),
|
|
370
|
+
worstBlockingMs: Math.round(worst),
|
|
371
|
+
},
|
|
372
|
+
items: [...capturedFrames]
|
|
373
|
+
.sort((a, b) => b.duration - a.duration)
|
|
374
|
+
.slice(0, 20)
|
|
375
|
+
.map((f) => ({
|
|
376
|
+
startTimeMs: Math.round(f.startTime),
|
|
377
|
+
durationMs: Math.round(f.duration),
|
|
378
|
+
blockingDurationMs: Math.round(f.blockingDuration),
|
|
379
|
+
scriptCount: f.scripts.length,
|
|
380
|
+
})),
|
|
381
|
+
};
|
|
382
|
+
},
|
|
383
|
+
|
|
357
384
|
/**
|
|
358
385
|
* Clear all captured data
|
|
359
386
|
*/
|
|
@@ -392,6 +419,6 @@
|
|
|
392
419
|
script: "Long-Animation-Frames-Helpers",
|
|
393
420
|
status: "tracking",
|
|
394
421
|
message: "LoAF Helpers loaded. Use loafHelpers.summary(), loafHelpers.topScripts(), etc. Call loafHelpers.getRawData() to get raw frame data.",
|
|
395
|
-
getDataFn: "loafHelpers.
|
|
422
|
+
getDataFn: "loafHelpers.getData",
|
|
396
423
|
};
|
|
397
424
|
})();
|
|
@@ -132,6 +132,14 @@
|
|
|
132
132
|
category: data.category,
|
|
133
133
|
durationMs: Math.round(data.duration),
|
|
134
134
|
count: data.count,
|
|
135
|
+
functions: [...data.functions]
|
|
136
|
+
.sort((a, b) => b.duration - a.duration)
|
|
137
|
+
.slice(0, 5)
|
|
138
|
+
.map((fn) => ({
|
|
139
|
+
name: fn.name,
|
|
140
|
+
invoker: fn.invoker,
|
|
141
|
+
durationMs: Math.round(fn.duration),
|
|
142
|
+
})),
|
|
135
143
|
})),
|
|
136
144
|
};
|
|
137
145
|
})();
|
|
@@ -77,6 +77,27 @@
|
|
|
77
77
|
};
|
|
78
78
|
};
|
|
79
79
|
|
|
80
|
+
// Item returned for agents: the same shape at start and in the summary
|
|
81
|
+
const toItem = (frame) => ({
|
|
82
|
+
startTimeMs: frame.startTime,
|
|
83
|
+
durationMs: frame.duration,
|
|
84
|
+
blockingDurationMs: frame.blockingDuration,
|
|
85
|
+
scripts: [...frame.scripts]
|
|
86
|
+
.sort((a, b) => b.duration - a.duration)
|
|
87
|
+
.slice(0, 10)
|
|
88
|
+
.map((s) => ({
|
|
89
|
+
invoker: s.invoker,
|
|
90
|
+
source: s.source,
|
|
91
|
+
durationMs: s.duration,
|
|
92
|
+
forcedStyleAndLayoutMs: s.forcedStyleAndLayout,
|
|
93
|
+
})),
|
|
94
|
+
});
|
|
95
|
+
const topFrames = (frames) =>
|
|
96
|
+
[...frames]
|
|
97
|
+
.sort((a, b) => b.blockingDuration - a.blockingDuration)
|
|
98
|
+
.slice(0, 20)
|
|
99
|
+
.map(toItem);
|
|
100
|
+
|
|
80
101
|
const overlap = (e1, e2) =>
|
|
81
102
|
e1.startTime < e2.startTime + e2.duration &&
|
|
82
103
|
e2.startTime < e1.startTime + e1.duration;
|
|
@@ -164,7 +185,7 @@
|
|
|
164
185
|
});
|
|
165
186
|
|
|
166
187
|
loafObserver.observe({ type: "long-animation-frame", buffered: true });
|
|
167
|
-
eventObserver.observe({ type: "event", buffered: true });
|
|
188
|
+
eventObserver.observe({ type: "event", buffered: true, durationThreshold: 16 });
|
|
168
189
|
|
|
169
190
|
// Summary function
|
|
170
191
|
window.getLoAFSummary = () => {
|
|
@@ -173,7 +194,13 @@
|
|
|
173
194
|
if (allLoAFs.length === 0) {
|
|
174
195
|
console.log(" No long animation frames recorded.");
|
|
175
196
|
console.groupEnd();
|
|
176
|
-
return
|
|
197
|
+
return {
|
|
198
|
+
script: "Long-Animation-Frames",
|
|
199
|
+
status: "ok",
|
|
200
|
+
count: 0,
|
|
201
|
+
details: { totalLoAFs: 0, withBlockingTime: 0, totalBlockingTimeMs: 0, worstBlockingMs: 0, topScripts: [] },
|
|
202
|
+
items: [],
|
|
203
|
+
};
|
|
177
204
|
}
|
|
178
205
|
|
|
179
206
|
const blocking = allLoAFs.filter((l) => l.blockingDuration > 0);
|
|
@@ -266,6 +293,7 @@
|
|
|
266
293
|
.slice(0, 5)
|
|
267
294
|
.map((s) => ({ invoker: s.invoker, source: s.source, totalDurationMs: Math.round(s.totalDuration), count: s.count })),
|
|
268
295
|
},
|
|
296
|
+
items: topFrames(allLoAFs),
|
|
269
297
|
};
|
|
270
298
|
};
|
|
271
299
|
|
|
@@ -292,6 +320,7 @@
|
|
|
292
320
|
totalBlockingTimeMs: Math.round(totalBlockingSync),
|
|
293
321
|
worstBlockingMs: Math.round(worstBlockingSync),
|
|
294
322
|
},
|
|
323
|
+
items: topFrames(loafBuffered.map(processLoAF)),
|
|
295
324
|
message: "Tracking long animation frames. Call getLoAFSummary() for full script attribution.",
|
|
296
325
|
getDataFn: "getLoAFSummary",
|
|
297
326
|
};
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
// Long Tasks Tracking
|
|
2
2
|
// https://webperf-snippets.nucliweb.net
|
|
3
3
|
|
|
4
|
-
(() => {
|
|
4
|
+
(async () => {
|
|
5
|
+
if (!PerformanceObserver.supportedEntryTypes?.includes("longtask")) {
|
|
6
|
+
console.warn("⚠️ longtask entries are not supported in this browser.");
|
|
7
|
+
return { script: "LongTask", status: "unsupported", error: "longtask entries not supported in this browser" };
|
|
8
|
+
}
|
|
5
9
|
const formatMs = (ms) => `${Math.round(ms)}ms`;
|
|
6
10
|
|
|
7
11
|
// Severity helpers
|
|
@@ -14,6 +18,19 @@
|
|
|
14
18
|
|
|
15
19
|
// Track all long tasks
|
|
16
20
|
const allTasks = [];
|
|
21
|
+
|
|
22
|
+
// Item returned for agents: the same shape at start and in the summary. Slowest first, at most 50.
|
|
23
|
+
const toItems = (tasks) =>
|
|
24
|
+
[...tasks]
|
|
25
|
+
.sort((a, b) => b.duration - a.duration)
|
|
26
|
+
.slice(0, 50)
|
|
27
|
+
.map((t) => ({
|
|
28
|
+
startTimeMs: Math.round(t.startTime),
|
|
29
|
+
durationMs: Math.round(t.duration),
|
|
30
|
+
blockingTimeMs: Math.round(t.blockingTime),
|
|
31
|
+
severity: t.severity,
|
|
32
|
+
attribution: t.attribution,
|
|
33
|
+
}));
|
|
17
34
|
let totalBlockingTime = 0;
|
|
18
35
|
|
|
19
36
|
try {
|
|
@@ -79,7 +96,18 @@
|
|
|
79
96
|
console.log(" No long tasks recorded.");
|
|
80
97
|
console.log(" This is good! The main thread has been responsive.");
|
|
81
98
|
console.groupEnd();
|
|
82
|
-
return
|
|
99
|
+
return {
|
|
100
|
+
script: "LongTask",
|
|
101
|
+
status: "ok",
|
|
102
|
+
count: 0,
|
|
103
|
+
details: {
|
|
104
|
+
totalBlockingTimeMs: 0,
|
|
105
|
+
worstTaskMs: 0,
|
|
106
|
+
avgDurationMs: 0,
|
|
107
|
+
bySeverity: { critical: 0, high: 0, medium: 0, low: 0 },
|
|
108
|
+
},
|
|
109
|
+
items: [],
|
|
110
|
+
};
|
|
83
111
|
}
|
|
84
112
|
|
|
85
113
|
const durations = allTasks.map((t) => t.duration);
|
|
@@ -119,7 +147,7 @@
|
|
|
119
147
|
console.log("");
|
|
120
148
|
console.log("%c⏱️ Timeline:", "font-weight: bold;");
|
|
121
149
|
|
|
122
|
-
const tableData = allTasks
|
|
150
|
+
const tableData = [...allTasks]
|
|
123
151
|
.sort((a, b) => b.duration - a.duration)
|
|
124
152
|
.slice(0, 10)
|
|
125
153
|
.map((t) => {
|
|
@@ -160,6 +188,7 @@
|
|
|
160
188
|
avgDurationMs: Math.round(avg),
|
|
161
189
|
bySeverity,
|
|
162
190
|
},
|
|
191
|
+
items: toItems(allTasks),
|
|
163
192
|
};
|
|
164
193
|
};
|
|
165
194
|
|
|
@@ -172,8 +201,10 @@
|
|
|
172
201
|
""
|
|
173
202
|
);
|
|
174
203
|
|
|
175
|
-
//
|
|
176
|
-
|
|
204
|
+
// Return for agent. Chrome only exposes longtask entries through a PerformanceObserver,
|
|
205
|
+
// so wait for the buffered observer above to deliver them.
|
|
206
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
207
|
+
const longtaskBuffered = allTasks;
|
|
177
208
|
const totalBlockingSync = longtaskBuffered.reduce((sum, t) => sum + Math.max(0, t.duration - 50), 0);
|
|
178
209
|
const worstTaskSync = longtaskBuffered.length > 0 ? Math.max(...longtaskBuffered.map((t) => t.duration)) : 0;
|
|
179
210
|
const bySeveritySync = {
|
|
@@ -191,6 +222,7 @@
|
|
|
191
222
|
worstTaskMs: Math.round(worstTaskSync),
|
|
192
223
|
bySeverity: bySeveritySync,
|
|
193
224
|
},
|
|
225
|
+
items: toItems(allTasks),
|
|
194
226
|
message: "Tracking long tasks. Call getLongTaskSummary() for statistics.",
|
|
195
227
|
getDataFn: "getLongTaskSummary",
|
|
196
228
|
};
|
|
@@ -188,6 +188,38 @@
|
|
|
188
188
|
return results;
|
|
189
189
|
};
|
|
190
190
|
|
|
191
|
+
// Findings as homogeneous items and actionable issues (same value at start and in the summary)
|
|
192
|
+
const buildFindings = (css) => {
|
|
193
|
+
const seen = new Set();
|
|
194
|
+
const items = [];
|
|
195
|
+
const add = (kind, target, detail) => {
|
|
196
|
+
const key = `${kind}|${target}|${detail}`;
|
|
197
|
+
if (seen.has(key)) return;
|
|
198
|
+
seen.add(key);
|
|
199
|
+
items.push({ kind, target, detail });
|
|
200
|
+
};
|
|
201
|
+
nonPassiveListeners.forEach((l) =>
|
|
202
|
+
add("non-passive-listener", `${l.element}${l.id ? `#${l.id}` : ""}`, l.type)
|
|
203
|
+
);
|
|
204
|
+
css.willChangeElements.forEach((e) => add("will-change", e.element, e.value));
|
|
205
|
+
css.overscrollElements.forEach((e) => add("overscroll", e.element, e.value));
|
|
206
|
+
|
|
207
|
+
const issues = [];
|
|
208
|
+
if (nonPassiveListeners.length > 0) {
|
|
209
|
+
issues.push({
|
|
210
|
+
severity: "warning",
|
|
211
|
+
message: `${nonPassiveListeners.length} non-passive scroll or touch listener(s). Add { passive: true } so scrolling does not wait for JavaScript.`,
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
if (css.willChangeElements.length > 0) {
|
|
215
|
+
issues.push({
|
|
216
|
+
severity: "info",
|
|
217
|
+
message: `will-change is set on ${css.willChangeElements.length} element(s). Remove it from elements that do not animate, since each one keeps a compositor layer.`,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
return { items: items.slice(0, 50), issues };
|
|
221
|
+
};
|
|
222
|
+
|
|
191
223
|
// ─── 4. SUMMARY ─────────────────────────────────────────────────────────────
|
|
192
224
|
|
|
193
225
|
window.getScrollSummary = () => {
|
|
@@ -306,6 +338,7 @@
|
|
|
306
338
|
console.groupEnd();
|
|
307
339
|
|
|
308
340
|
const cssAuditResult = auditScrollCSS();
|
|
341
|
+
const findings = buildFindings(cssAuditResult);
|
|
309
342
|
return {
|
|
310
343
|
script: "Scroll-Performance",
|
|
311
344
|
status: "ok",
|
|
@@ -315,6 +348,7 @@
|
|
|
315
348
|
smoothScrollElements: cssAuditResult.smoothScrollElements.length,
|
|
316
349
|
willChangeElements: cssAuditResult.willChangeElements.length,
|
|
317
350
|
contentVisibilityElements: cssAuditResult.contentVisibilityElements.length,
|
|
351
|
+
overscrollElements: cssAuditResult.overscrollElements.length,
|
|
318
352
|
},
|
|
319
353
|
sessionCount: sessions.length,
|
|
320
354
|
...(sessions.length > 0 ? {
|
|
@@ -323,6 +357,8 @@
|
|
|
323
357
|
totalDrops: sessions.reduce((a, s) => a + s.drops, 0),
|
|
324
358
|
} : {}),
|
|
325
359
|
},
|
|
360
|
+
items: findings.items,
|
|
361
|
+
issues: findings.issues,
|
|
326
362
|
};
|
|
327
363
|
};
|
|
328
364
|
|
|
@@ -342,6 +378,7 @@
|
|
|
342
378
|
);
|
|
343
379
|
|
|
344
380
|
const cssSnapshot = auditScrollCSS();
|
|
381
|
+
const initialFindings = buildFindings(cssSnapshot);
|
|
345
382
|
return {
|
|
346
383
|
script: "Scroll-Performance",
|
|
347
384
|
status: "tracking",
|
|
@@ -351,8 +388,11 @@
|
|
|
351
388
|
smoothScrollElements: cssSnapshot.smoothScrollElements.length,
|
|
352
389
|
willChangeElements: cssSnapshot.willChangeElements.length,
|
|
353
390
|
contentVisibilityElements: cssSnapshot.contentVisibilityElements.length,
|
|
391
|
+
overscrollElements: cssSnapshot.overscrollElements.length,
|
|
354
392
|
},
|
|
355
393
|
},
|
|
394
|
+
items: initialFindings.items,
|
|
395
|
+
issues: initialFindings.issues,
|
|
356
396
|
message: "Scroll performance tracking active. Scroll the page then call getScrollSummary() for FPS data.",
|
|
357
397
|
getDataFn: "getScrollSummary",
|
|
358
398
|
};
|
|
@@ -58,6 +58,7 @@ When animations feel sluggish or the user asks "debug animation performance":
|
|
|
58
58
|
2. **Long-Animation-Frames-Helpers.js** - Detailed frame timing analysis
|
|
59
59
|
3. **Long-Animation-Frames-Script-Attribution.js** - Find scripts delaying frames
|
|
60
60
|
4. **Scroll-Performance.js** - If scroll animations are involved
|
|
61
|
+
5. **DOM-Size-and-Depth.js** - Check whether a large or deep DOM makes each style and layout pass expensive
|
|
61
62
|
|
|
62
63
|
### Third-Party Script Impact on Interactions
|
|
63
64
|
|
|
@@ -117,6 +118,7 @@ Use this decision tree to automatically run follow-up snippets based on results:
|
|
|
117
118
|
- Debouncing/throttling event handlers
|
|
118
119
|
- Web Workers for heavy computation
|
|
119
120
|
- **If forced reflow/layout detected** → Analyze DOM manipulation patterns
|
|
121
|
+
- **If time goes to style recalculation or layout** → Run **DOM-Size-and-Depth.js** to check the size of the tree
|
|
120
122
|
|
|
121
123
|
### After LongTask.js
|
|
122
124
|
|
|
@@ -130,6 +132,13 @@ Use this decision tree to automatically run follow-up snippets based on results:
|
|
|
130
132
|
- Excessive DOM manipulation
|
|
131
133
|
- **If long tasks correlate with interactions** → Run **Interactions.js** to see impact
|
|
132
134
|
|
|
135
|
+
### After DOM-Size-and-Depth.js
|
|
136
|
+
|
|
137
|
+
- **If total elements > 1400** → Recommend virtualizing long lists and loading off-screen content on demand
|
|
138
|
+
- **If depth > 32** → Recommend flattening wrapper elements added by components
|
|
139
|
+
- **If a single parent has > 60 children** → Recommend pagination or virtualization for that container
|
|
140
|
+
- **If the DOM is within limits but interactions are still slow** → Run **Long-Animation-Frames-Script-Attribution.js** to find the scripts responsible
|
|
141
|
+
|
|
133
142
|
### After Scroll-Performance.js
|
|
134
143
|
|
|
135
144
|
- **If scroll FPS < 30** → Severe jank, run:
|