webperf-snippets 1.15.0 ā 1.17.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 +150 -12
- package/package.json +3 -2
- package/snippets/Loading/CSS-Media-Queries-Analysis.js +190 -188
- package/snippets/Loading/Content-Visibility.js +273 -264
- package/snippets/SCHEMA.md +13 -0
- package/snippets/_view/index.js +1 -0
- package/snippets/_view/report.js +60 -0
- package/src/baseline.js +21 -6
- package/src/bin.js +143 -41
- package/src/blocking.js +29 -8
- package/src/interactions.js +63 -6
- package/src/live-baseline.js +76 -0
- package/src/mcp/server.js +29 -17
- package/src/mcp/steps.js +12 -0
- package/src/overview.js +20 -0
- package/src/reporters/baseline-view.js +39 -9
- package/src/reporters/human.js +12 -3
- package/src/reporters/markdown.js +44 -11
- package/src/view.js +1 -1
|
@@ -1,320 +1,329 @@
|
|
|
1
1
|
// Detect elements with content-visibility: auto and analyze optimization opportunities
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
3
|
+
(() => {
|
|
4
|
+
// @shared uniqueSelector
|
|
5
|
+
// A CSS selector that matches only el in its document (or shadow root): its id when it is unique,
|
|
6
|
+
// otherwise a path of tag, classes and :nth-of-type() that goes up to the nearest ancestor with a
|
|
7
|
+
// unique id, or to the root, and stops as soon as it matches one element.
|
|
8
|
+
function uniqueSelector(el) {
|
|
9
|
+
const root = el.getRootNode();
|
|
10
|
+
const matchesOne = (selector) => {
|
|
11
|
+
try {
|
|
12
|
+
return root.querySelectorAll(selector).length === 1;
|
|
13
|
+
} catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
const parts = [];
|
|
18
|
+
let node = el;
|
|
19
|
+
while (node && node.nodeType === 1) {
|
|
20
|
+
if (node.id && matchesOne(`#${window.CSS.escape(node.id)}`)) {
|
|
21
|
+
parts.unshift(`#${window.CSS.escape(node.id)}`);
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
const tag = node.localName;
|
|
25
|
+
let part = tag + Array.from(node.classList).slice(0, 2).map((c) => `.${window.CSS.escape(c)}`).join("");
|
|
26
|
+
const siblings = node.parentNode ? Array.from(node.parentNode.children) : [node];
|
|
27
|
+
if (siblings.filter((s) => s.matches(part)).length > 1) {
|
|
28
|
+
part += `:nth-of-type(${siblings.filter((s) => s.localName === tag).indexOf(node) + 1})`;
|
|
29
|
+
}
|
|
30
|
+
parts.unshift(part);
|
|
31
|
+
if (matchesOne(parts.join(" > "))) break;
|
|
32
|
+
node = node.parentElement;
|
|
28
33
|
}
|
|
29
|
-
parts.
|
|
30
|
-
if (matchesOne(parts.join(" > "))) break;
|
|
31
|
-
node = node.parentElement;
|
|
34
|
+
return parts.join(" > ");
|
|
32
35
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
function lacksIntrinsicSize(value) {
|
|
42
|
-
return !/\d/.test(value);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function detectContentVisibility() {
|
|
46
|
-
// Create an object to store the results
|
|
47
|
-
const results = {
|
|
48
|
-
autoElements: [],
|
|
49
|
-
hiddenElements: [],
|
|
50
|
-
visibleElements: [],
|
|
51
|
-
nodeArray: [],
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
// Check if element is in viewport
|
|
55
|
-
function isInViewport(element) {
|
|
56
|
-
const rect = element.getBoundingClientRect();
|
|
57
|
-
return (
|
|
58
|
-
rect.top < window.innerHeight &&
|
|
59
|
-
rect.bottom > 0 &&
|
|
60
|
-
rect.left < window.innerWidth &&
|
|
61
|
-
rect.right > 0
|
|
62
|
-
);
|
|
36
|
+
// @end-shared uniqueSelector
|
|
37
|
+
|
|
38
|
+
// An element with content-visibility: auto has no size before it first renders unless
|
|
39
|
+
// contain-intrinsic-size gives one, so the page moves when it does. content-visibility: auto adds auto
|
|
40
|
+
// to contain-intrinsic-size, so Chrome reports an element with no size as "auto none", also when the CSS
|
|
41
|
+
// says none: what counts is whether the value has a length, as in "auto 500px".
|
|
42
|
+
function lacksIntrinsicSize(value) {
|
|
43
|
+
return !/\d/.test(value);
|
|
63
44
|
}
|
|
64
45
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
contentVisibility: cs["content-visibility"],
|
|
73
|
-
containIntrinsicSize: cs["contain-intrinsic-size"] || "not set",
|
|
74
|
-
width: Math.round(rect.width),
|
|
75
|
-
height: Math.round(rect.height),
|
|
76
|
-
top: Math.round(rect.top + window.scrollY),
|
|
77
|
-
inViewport: isInViewport(node),
|
|
46
|
+
function detectContentVisibility() {
|
|
47
|
+
// Create an object to store the results
|
|
48
|
+
const results = {
|
|
49
|
+
autoElements: [],
|
|
50
|
+
hiddenElements: [],
|
|
51
|
+
visibleElements: [],
|
|
52
|
+
nodeArray: [],
|
|
78
53
|
};
|
|
79
|
-
}
|
|
80
54
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
results.autoElements.push(info);
|
|
91
|
-
results.nodeArray.push(node);
|
|
92
|
-
} else if (cv === "hidden") {
|
|
93
|
-
results.hiddenElements.push(info);
|
|
94
|
-
}
|
|
55
|
+
// Check if element is in viewport
|
|
56
|
+
function isInViewport(element) {
|
|
57
|
+
const rect = element.getBoundingClientRect();
|
|
58
|
+
return (
|
|
59
|
+
rect.top < window.innerHeight &&
|
|
60
|
+
rect.bottom > 0 &&
|
|
61
|
+
rect.left < window.innerWidth &&
|
|
62
|
+
rect.right > 0
|
|
63
|
+
);
|
|
95
64
|
}
|
|
96
65
|
|
|
97
|
-
|
|
98
|
-
|
|
66
|
+
// Get element dimensions and position
|
|
67
|
+
function getElementInfo(node) {
|
|
68
|
+
const rect = node.getBoundingClientRect();
|
|
69
|
+
const cs = window.getComputedStyle(node);
|
|
70
|
+
|
|
71
|
+
return {
|
|
72
|
+
selector: uniqueSelector(node),
|
|
73
|
+
contentVisibility: cs["content-visibility"],
|
|
74
|
+
containIntrinsicSize: cs["contain-intrinsic-size"] || "not set",
|
|
75
|
+
width: Math.round(rect.width),
|
|
76
|
+
height: Math.round(rect.height),
|
|
77
|
+
top: Math.round(rect.top + window.scrollY),
|
|
78
|
+
inViewport: isInViewport(node),
|
|
79
|
+
};
|
|
99
80
|
}
|
|
100
|
-
}
|
|
101
81
|
|
|
102
|
-
|
|
103
|
-
|
|
82
|
+
// Recursively find all elements with content-visibility
|
|
83
|
+
function findContentVisibilityElements(node) {
|
|
84
|
+
const cs = window.getComputedStyle(node);
|
|
85
|
+
const cv = cs["content-visibility"];
|
|
104
86
|
|
|
105
|
-
|
|
106
|
-
|
|
87
|
+
if (cv && cv !== "visible") {
|
|
88
|
+
const info = getElementInfo(node);
|
|
107
89
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
console.log(" ⢠Tab content that is not initially visible");
|
|
116
|
-
console.log(" ⢠Accordion/collapsible content");
|
|
117
|
-
} else {
|
|
118
|
-
// Auto elements
|
|
119
|
-
if (results.autoElements.length > 0) {
|
|
120
|
-
console.group("ā
content-visibility: auto");
|
|
121
|
-
console.log(`Found ${results.autoElements.length} element(s)`);
|
|
122
|
-
console.table(results.autoElements);
|
|
123
|
-
console.groupEnd();
|
|
124
|
-
}
|
|
90
|
+
if (cv === "auto") {
|
|
91
|
+
results.autoElements.push(info);
|
|
92
|
+
results.nodeArray.push(node);
|
|
93
|
+
} else if (cv === "hidden") {
|
|
94
|
+
results.hiddenElements.push(info);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
125
97
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
console.log(`Found ${results.hiddenElements.length} element(s)`);
|
|
130
|
-
console.table(results.hiddenElements);
|
|
131
|
-
console.groupEnd();
|
|
98
|
+
for (let i = 0; i < node.children.length; i++) {
|
|
99
|
+
findContentVisibilityElements(node.children[i]);
|
|
100
|
+
}
|
|
132
101
|
}
|
|
133
102
|
|
|
134
|
-
//
|
|
135
|
-
|
|
136
|
-
(el) => lacksIntrinsicSize(el.containIntrinsicSize),
|
|
137
|
-
);
|
|
103
|
+
// Run the detection
|
|
104
|
+
findContentVisibilityElements(document.body);
|
|
138
105
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
);
|
|
145
|
-
console.table(
|
|
146
|
-
missingIntrinsicSize.map((el) => ({
|
|
147
|
-
selector: el.selector,
|
|
148
|
-
height: el.height + "px",
|
|
149
|
-
})),
|
|
150
|
-
);
|
|
106
|
+
// Display results
|
|
107
|
+
console.group("š Content-Visibility Detection");
|
|
108
|
+
|
|
109
|
+
if (results.autoElements.length === 0 && results.hiddenElements.length === 0) {
|
|
110
|
+
console.log("%cNo content-visibility usage found.", "color: orange; font-weight: bold;");
|
|
151
111
|
console.log("");
|
|
152
|
-
console.log("š”
|
|
153
|
-
console.log("
|
|
154
|
-
console.
|
|
155
|
-
|
|
112
|
+
console.log("š” Consider applying content-visibility: auto to:");
|
|
113
|
+
console.log(" ⢠Footer sections");
|
|
114
|
+
console.log(" ⢠Below-the-fold content");
|
|
115
|
+
console.log(" ⢠Long lists or card grids");
|
|
116
|
+
console.log(" ⢠Tab content that is not initially visible");
|
|
117
|
+
console.log(" ⢠Accordion/collapsible content");
|
|
118
|
+
} else {
|
|
119
|
+
// Auto elements
|
|
120
|
+
if (results.autoElements.length > 0) {
|
|
121
|
+
console.group("ā
content-visibility: auto");
|
|
122
|
+
console.log(`Found ${results.autoElements.length} element(s)`);
|
|
123
|
+
console.table(results.autoElements);
|
|
124
|
+
console.groupEnd();
|
|
125
|
+
}
|
|
156
126
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
}
|
|
127
|
+
// Hidden elements
|
|
128
|
+
if (results.hiddenElements.length > 0) {
|
|
129
|
+
console.group("š content-visibility: hidden");
|
|
130
|
+
console.log(`Found ${results.hiddenElements.length} element(s)`);
|
|
131
|
+
console.table(results.hiddenElements);
|
|
132
|
+
console.groupEnd();
|
|
133
|
+
}
|
|
165
134
|
|
|
166
|
-
|
|
135
|
+
// Check for missing contain-intrinsic-size
|
|
136
|
+
const missingIntrinsicSize = results.autoElements.filter(
|
|
137
|
+
(el) => lacksIntrinsicSize(el.containIntrinsicSize),
|
|
138
|
+
);
|
|
167
139
|
|
|
168
|
-
|
|
169
|
-
|
|
140
|
+
if (missingIntrinsicSize.length > 0) {
|
|
141
|
+
console.group("ā ļø Missing contain-intrinsic-size");
|
|
142
|
+
console.log(
|
|
143
|
+
"%cThese elements lack contain-intrinsic-size, which may cause layout shifts:",
|
|
144
|
+
"color: #f59e0b; font-weight: bold",
|
|
145
|
+
);
|
|
146
|
+
console.table(
|
|
147
|
+
missingIntrinsicSize.map((el) => ({
|
|
148
|
+
selector: el.selector,
|
|
149
|
+
height: el.height + "px",
|
|
150
|
+
})),
|
|
151
|
+
);
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log("š” Add contain-intrinsic-size to prevent CLS:");
|
|
154
|
+
console.log(" contain-intrinsic-size: auto 500px;");
|
|
155
|
+
console.groupEnd();
|
|
156
|
+
}
|
|
170
157
|
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
158
|
+
// Nodes for inspection
|
|
159
|
+
console.group("š Elements for inspection");
|
|
160
|
+
console.log("Click to expand and inspect in Elements panel:");
|
|
161
|
+
results.nodeArray.forEach((node, i) => {
|
|
162
|
+
console.log(`${i + 1}. `, node);
|
|
163
|
+
});
|
|
164
|
+
console.groupEnd();
|
|
165
|
+
}
|
|
178
166
|
|
|
179
|
-
|
|
180
|
-
const opportunities = [];
|
|
181
|
-
const processedElements = new Set();
|
|
167
|
+
console.groupEnd();
|
|
182
168
|
|
|
183
|
-
|
|
184
|
-
const baseMs = childCount * 0.2;
|
|
185
|
-
if (baseMs < 5) return "Low (~" + baseMs.toFixed(1) + "ms)";
|
|
186
|
-
if (baseMs < 20) return "Medium (~" + baseMs.toFixed(1) + "ms)";
|
|
187
|
-
return "High (~" + baseMs.toFixed(1) + "ms)";
|
|
169
|
+
return results;
|
|
188
170
|
}
|
|
189
171
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
172
|
+
// Analyze opportunities for content-visibility optimization
|
|
173
|
+
// Options:
|
|
174
|
+
// - threshold: distance from viewport bottom to consider "offscreen" (default: 0)
|
|
175
|
+
// - minHeight: minimum element height in px (default: 100)
|
|
176
|
+
// - minChildren: minimum child elements to be considered (default: 5)
|
|
177
|
+
function analyzeContentVisibilityOpportunities(options = {}) {
|
|
178
|
+
const { threshold = 0, minHeight = 100, minChildren = 5 } = options;
|
|
179
|
+
|
|
180
|
+
const viewportHeight = window.innerHeight;
|
|
181
|
+
const opportunities = [];
|
|
182
|
+
const processedElements = new Set();
|
|
183
|
+
|
|
184
|
+
function estimateRenderSavings(childCount) {
|
|
185
|
+
const baseMs = childCount * 0.2;
|
|
186
|
+
if (baseMs < 5) return "Low (~" + baseMs.toFixed(1) + "ms)";
|
|
187
|
+
if (baseMs < 20) return "Medium (~" + baseMs.toFixed(1) + "ms)";
|
|
188
|
+
return "High (~" + baseMs.toFixed(1) + "ms)";
|
|
195
189
|
}
|
|
196
|
-
return false;
|
|
197
|
-
}
|
|
198
190
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
191
|
+
function isAncestorProcessed(el) {
|
|
192
|
+
let parent = el.parentElement;
|
|
193
|
+
while (parent) {
|
|
194
|
+
if (processedElements.has(parent)) return true;
|
|
195
|
+
parent = parent.parentElement;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
202
199
|
|
|
203
|
-
|
|
204
|
-
|
|
200
|
+
function analyzeElement(el) {
|
|
201
|
+
// Skip already processed or descendant of processed
|
|
202
|
+
if (processedElements.has(el) || isAncestorProcessed(el)) return;
|
|
205
203
|
|
|
206
|
-
|
|
207
|
-
|
|
204
|
+
const rect = el.getBoundingClientRect();
|
|
205
|
+
const cs = window.getComputedStyle(el);
|
|
208
206
|
|
|
209
|
-
|
|
210
|
-
|
|
207
|
+
// Skip if already using content-visibility
|
|
208
|
+
if (cs["content-visibility"] && cs["content-visibility"] !== "visible") return;
|
|
211
209
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (distanceFromViewport < threshold) return;
|
|
210
|
+
// Skip elements not meeting size criteria
|
|
211
|
+
if (rect.height < minHeight || rect.width === 0) return;
|
|
215
212
|
|
|
216
|
-
|
|
213
|
+
// Check if element is below the viewport + threshold
|
|
214
|
+
const distanceFromViewport = rect.top - viewportHeight;
|
|
215
|
+
if (distanceFromViewport < threshold) return;
|
|
217
216
|
|
|
218
|
-
|
|
219
|
-
if (childCount < minChildren) return;
|
|
217
|
+
const childCount = el.querySelectorAll("*").length;
|
|
220
218
|
|
|
221
|
-
|
|
219
|
+
// Skip elements with too few children
|
|
220
|
+
if (childCount < minChildren) return;
|
|
222
221
|
|
|
223
|
-
|
|
224
|
-
selector: uniqueSelector(el),
|
|
225
|
-
height: Math.round(rect.height) + "px",
|
|
226
|
-
distanceFromViewport: Math.round(distanceFromViewport) + "px",
|
|
227
|
-
childElements: childCount,
|
|
228
|
-
estimatedSavings: estimateRenderSavings(childCount),
|
|
229
|
-
element: el,
|
|
230
|
-
});
|
|
231
|
-
}
|
|
222
|
+
processedElements.add(el);
|
|
232
223
|
|
|
233
|
-
|
|
234
|
-
|
|
224
|
+
opportunities.push({
|
|
225
|
+
selector: uniqueSelector(el),
|
|
226
|
+
height: Math.round(rect.height) + "px",
|
|
227
|
+
distanceFromViewport: Math.round(distanceFromViewport) + "px",
|
|
228
|
+
childElements: childCount,
|
|
229
|
+
estimatedSavings: estimateRenderSavings(childCount),
|
|
230
|
+
element: el,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
235
233
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
234
|
+
// Walk all elements in the DOM
|
|
235
|
+
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false);
|
|
239
236
|
|
|
240
|
-
|
|
241
|
-
|
|
237
|
+
while (walker.nextNode()) {
|
|
238
|
+
analyzeElement(walker.currentNode);
|
|
239
|
+
}
|
|
242
240
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
console.log(
|
|
246
|
-
`%cSettings: threshold=${threshold}px, minHeight=${minHeight}px, minChildren=${minChildren}`,
|
|
247
|
-
"color: #888;",
|
|
248
|
-
);
|
|
249
|
-
console.log("");
|
|
241
|
+
// Sort by child count (highest impact first)
|
|
242
|
+
opportunities.sort((a, b) => b.childElements - a.childElements);
|
|
250
243
|
|
|
251
|
-
|
|
252
|
-
console.
|
|
253
|
-
console.log("Try adjusting: analyzeContentVisibilityOpportunities({ threshold: -200, minHeight: 50, minChildren: 3 })");
|
|
254
|
-
} else {
|
|
244
|
+
// Display results
|
|
245
|
+
console.group("š” Content-Visibility Opportunities");
|
|
255
246
|
console.log(
|
|
256
|
-
`%
|
|
257
|
-
"
|
|
247
|
+
`%cSettings: threshold=${threshold}px, minHeight=${minHeight}px, minChildren=${minChildren}`,
|
|
248
|
+
"color: #888;",
|
|
258
249
|
);
|
|
259
250
|
console.log("");
|
|
260
251
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
252
|
+
if (opportunities.length === 0) {
|
|
253
|
+
console.log("%cā
No opportunities found with current settings.", "color: #22c55e; font-weight: bold");
|
|
254
|
+
console.log("Try adjusting: analyzeContentVisibilityOpportunities({ threshold: -200, minHeight: 50, minChildren: 3 })");
|
|
255
|
+
} else {
|
|
256
|
+
console.log(
|
|
257
|
+
`%cFound ${opportunities.length} element(s) that could benefit from content-visibility: auto`,
|
|
258
|
+
"font-weight: bold;",
|
|
259
|
+
);
|
|
260
|
+
console.log("");
|
|
261
|
+
|
|
262
|
+
// Show table without element reference
|
|
263
|
+
const tableData = opportunities.slice(0, 20).map((opportunity) => ({
|
|
264
|
+
selector: opportunity.selector,
|
|
265
|
+
height: opportunity.height,
|
|
266
|
+
distanceFromViewport: opportunity.distanceFromViewport,
|
|
267
|
+
childElements: opportunity.childElements,
|
|
268
|
+
estimatedSavings: opportunity.estimatedSavings,
|
|
269
|
+
}));
|
|
270
|
+
console.table(tableData);
|
|
271
|
+
|
|
272
|
+
if (opportunities.length > 20) {
|
|
273
|
+
console.log(`... and ${opportunities.length - 20} more elements`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Log elements for inspection
|
|
277
|
+
console.log("");
|
|
278
|
+
console.group("š Elements for inspection");
|
|
279
|
+
opportunities.slice(0, 10).forEach((opp, i) => {
|
|
280
|
+
console.log(`${i + 1}. `, opp.element);
|
|
281
|
+
});
|
|
282
|
+
console.groupEnd();
|
|
283
|
+
|
|
284
|
+
console.log("");
|
|
285
|
+
console.group("š Implementation Example");
|
|
286
|
+
console.log("Add this CSS to optimize rendering:");
|
|
287
|
+
console.log("");
|
|
288
|
+
console.log(
|
|
289
|
+
"%c/* Optimize offscreen content */\n" +
|
|
290
|
+
".your-selector {\n" +
|
|
291
|
+
" content-visibility: auto;\n" +
|
|
292
|
+
" contain-intrinsic-size: auto 500px; /* Use actual height */\n" +
|
|
293
|
+
"}",
|
|
294
|
+
"font-family: monospace; background: #1e1e1e; color: #9cdcfe; padding: 10px; border-radius: 4px;",
|
|
295
|
+
);
|
|
296
|
+
console.groupEnd();
|
|
273
297
|
}
|
|
274
298
|
|
|
275
|
-
// Log elements for inspection
|
|
276
|
-
console.log("");
|
|
277
|
-
console.group("š Elements for inspection");
|
|
278
|
-
opportunities.slice(0, 10).forEach((opp, i) => {
|
|
279
|
-
console.log(`${i + 1}. `, opp.element);
|
|
280
|
-
});
|
|
281
299
|
console.groupEnd();
|
|
282
300
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
301
|
+
// The shape of every result (snippets/SCHEMA.md). The elements themselves are in the console,
|
|
302
|
+
// above: a DOM node is not data a script or an agent can read
|
|
303
|
+
return {
|
|
304
|
+
script: "Content-Visibility",
|
|
305
|
+
status: "ok",
|
|
306
|
+
count: opportunities.length,
|
|
307
|
+
details: {
|
|
308
|
+
highImpact: opportunities.filter((o) => o.estimatedSavings.startsWith("High")).length,
|
|
309
|
+
threshold,
|
|
310
|
+
minHeight,
|
|
311
|
+
minChildren,
|
|
312
|
+
},
|
|
313
|
+
items: opportunities.slice(0, 50).map((opportunity) => ({
|
|
314
|
+
selector: opportunity.selector,
|
|
315
|
+
height: opportunity.height,
|
|
316
|
+
distanceFromViewport: opportunity.distanceFromViewport,
|
|
317
|
+
childElements: opportunity.childElements,
|
|
318
|
+
estimatedSavings: opportunity.estimatedSavings,
|
|
319
|
+
})),
|
|
320
|
+
issues: [],
|
|
321
|
+
};
|
|
296
322
|
}
|
|
297
323
|
|
|
298
|
-
|
|
324
|
+
window.analyzeContentVisibilityOpportunities = analyzeContentVisibilityOpportunities;
|
|
299
325
|
|
|
300
|
-
|
|
301
|
-
opportunities: opportunities.map((opportunity) => ({
|
|
302
|
-
selector: opportunity.selector,
|
|
303
|
-
height: opportunity.height,
|
|
304
|
-
distanceFromViewport: opportunity.distanceFromViewport,
|
|
305
|
-
childElements: opportunity.childElements,
|
|
306
|
-
estimatedSavings: opportunity.estimatedSavings,
|
|
307
|
-
})),
|
|
308
|
-
totalElements: opportunities.length,
|
|
309
|
-
highImpact: opportunities.filter((o) => o.estimatedSavings.startsWith("High")).length,
|
|
310
|
-
elements: opportunities.map((o) => o.element),
|
|
311
|
-
};
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
window.analyzeContentVisibilityOpportunities = analyzeContentVisibilityOpportunities;
|
|
315
|
-
|
|
316
|
-
// Run detection
|
|
317
|
-
(() => {
|
|
326
|
+
// Run detection
|
|
318
327
|
const cvResults = detectContentVisibility();
|
|
319
328
|
console.log(
|
|
320
329
|
"%c\n To find optimization opportunities, run: %canalyzeContentVisibilityOpportunities()",
|
package/snippets/SCHEMA.md
CHANGED
|
@@ -912,6 +912,19 @@ With no usage the script returns `count: 0`, no items and an `info` issue.
|
|
|
912
912
|
}
|
|
913
913
|
```
|
|
914
914
|
|
|
915
|
+
`analyzeContentVisibilityOpportunities(options)`, which the script leaves on `window`, returns the elements below the viewport that could use `content-visibility: auto`, the ones with more descendants first and up to 50; `count` holds the total. `details` has how many have a high estimated saving and the options the search used. `issues` is always empty.
|
|
916
|
+
|
|
917
|
+
```json
|
|
918
|
+
{
|
|
919
|
+
"script": "Content-Visibility",
|
|
920
|
+
"status": "ok",
|
|
921
|
+
"count": 1,
|
|
922
|
+
"details": { "highImpact": 0, "threshold": 0, "minHeight": 100, "minChildren": 5 },
|
|
923
|
+
"items": [{ "selector": "#comments", "height": "640px", "distanceFromViewport": "1200px", "childElements": 48, "estimatedSavings": "Medium (~9.6ms)" }],
|
|
924
|
+
"issues": []
|
|
925
|
+
}
|
|
926
|
+
```
|
|
927
|
+
|
|
915
928
|
### Interaction
|
|
916
929
|
|
|
917
930
|
Tracking scripts return `status: "tracking"` first. The function named by `getDataFn` returns the full result. Items are capped at 50 (20 for frames) and sorted by relevance; `count` holds the total.
|
package/snippets/_view/index.js
CHANGED
|
@@ -9,6 +9,7 @@ import en from './locales/en.js'
|
|
|
9
9
|
import { formatOfKey, formatValue, SUFFIXES } from './format.js'
|
|
10
10
|
|
|
11
11
|
export { formatValue }
|
|
12
|
+
export { auditReport, describeOverview, issueList, issuesOf, overviewOf, summarizeIssues } from './report.js'
|
|
12
13
|
|
|
13
14
|
const LOCALES = { en }
|
|
14
15
|
|