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.
@@ -1,320 +1,329 @@
1
1
  // Detect elements with content-visibility: auto and analyze optimization opportunities
2
2
 
3
- // @shared uniqueSelector
4
- // A CSS selector that matches only el in its document (or shadow root): its id when it is unique,
5
- // otherwise a path of tag, classes and :nth-of-type() that goes up to the nearest ancestor with a
6
- // unique id, or to the root, and stops as soon as it matches one element.
7
- function uniqueSelector(el) {
8
- const root = el.getRootNode();
9
- const matchesOne = (selector) => {
10
- try {
11
- return root.querySelectorAll(selector).length === 1;
12
- } catch {
13
- return false;
14
- }
15
- };
16
- const parts = [];
17
- let node = el;
18
- while (node && node.nodeType === 1) {
19
- if (node.id && matchesOne(`#${window.CSS.escape(node.id)}`)) {
20
- parts.unshift(`#${window.CSS.escape(node.id)}`);
21
- break;
22
- }
23
- const tag = node.localName;
24
- let part = tag + Array.from(node.classList).slice(0, 2).map((c) => `.${window.CSS.escape(c)}`).join("");
25
- const siblings = node.parentNode ? Array.from(node.parentNode.children) : [node];
26
- if (siblings.filter((s) => s.matches(part)).length > 1) {
27
- part += `:nth-of-type(${siblings.filter((s) => s.localName === tag).indexOf(node) + 1})`;
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.unshift(part);
30
- if (matchesOne(parts.join(" > "))) break;
31
- node = node.parentElement;
34
+ return parts.join(" > ");
32
35
  }
33
- return parts.join(" > ");
34
- }
35
- // @end-shared uniqueSelector
36
-
37
- // An element with content-visibility: auto has no size before it first renders unless
38
- // contain-intrinsic-size gives one, so the page moves when it does. content-visibility: auto adds auto
39
- // to contain-intrinsic-size, so Chrome reports an element with no size as "auto none", also when the CSS
40
- // says none: what counts is whether the value has a length, as in "auto 500px".
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
- // Get element dimensions and position
66
- function getElementInfo(node) {
67
- const rect = node.getBoundingClientRect();
68
- const cs = window.getComputedStyle(node);
69
-
70
- return {
71
- selector: uniqueSelector(node),
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
- // Recursively find all elements with content-visibility
82
- function findContentVisibilityElements(node) {
83
- const cs = window.getComputedStyle(node);
84
- const cv = cs["content-visibility"];
85
-
86
- if (cv && cv !== "visible") {
87
- const info = getElementInfo(node);
88
-
89
- if (cv === "auto") {
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
- for (let i = 0; i < node.children.length; i++) {
98
- findContentVisibilityElements(node.children[i]);
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
- // Run the detection
103
- findContentVisibilityElements(document.body);
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
- // Display results
106
- console.group("šŸ” Content-Visibility Detection");
87
+ if (cv && cv !== "visible") {
88
+ const info = getElementInfo(node);
107
89
 
108
- if (results.autoElements.length === 0 && results.hiddenElements.length === 0) {
109
- console.log("%cNo content-visibility usage found.", "color: orange; font-weight: bold;");
110
- console.log("");
111
- console.log("šŸ’” Consider applying content-visibility: auto to:");
112
- console.log(" • Footer sections");
113
- console.log(" • Below-the-fold content");
114
- console.log(" • Long lists or card grids");
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
- // Hidden elements
127
- if (results.hiddenElements.length > 0) {
128
- console.group("šŸ”’ content-visibility: hidden");
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
- // Check for missing contain-intrinsic-size
135
- const missingIntrinsicSize = results.autoElements.filter(
136
- (el) => lacksIntrinsicSize(el.containIntrinsicSize),
137
- );
103
+ // Run the detection
104
+ findContentVisibilityElements(document.body);
138
105
 
139
- if (missingIntrinsicSize.length > 0) {
140
- console.group("āš ļø Missing contain-intrinsic-size");
141
- console.log(
142
- "%cThese elements lack contain-intrinsic-size, which may cause layout shifts:",
143
- "color: #f59e0b; font-weight: bold",
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("šŸ’” Add contain-intrinsic-size to prevent CLS:");
153
- console.log(" contain-intrinsic-size: auto 500px;");
154
- console.groupEnd();
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
- // Nodes for inspection
158
- console.group("šŸ”Ž Elements for inspection");
159
- console.log("Click to expand and inspect in Elements panel:");
160
- results.nodeArray.forEach((node, i) => {
161
- console.log(`${i + 1}. `, node);
162
- });
163
- console.groupEnd();
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
- console.groupEnd();
135
+ // Check for missing contain-intrinsic-size
136
+ const missingIntrinsicSize = results.autoElements.filter(
137
+ (el) => lacksIntrinsicSize(el.containIntrinsicSize),
138
+ );
167
139
 
168
- return results;
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
- // Analyze opportunities for content-visibility optimization
172
- // Options:
173
- // - threshold: distance from viewport bottom to consider "offscreen" (default: 0)
174
- // - minHeight: minimum element height in px (default: 100)
175
- // - minChildren: minimum child elements to be considered (default: 5)
176
- function analyzeContentVisibilityOpportunities(options = {}) {
177
- const { threshold = 0, minHeight = 100, minChildren = 5 } = options;
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
- const viewportHeight = window.innerHeight;
180
- const opportunities = [];
181
- const processedElements = new Set();
167
+ console.groupEnd();
182
168
 
183
- function estimateRenderSavings(childCount) {
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
- function isAncestorProcessed(el) {
191
- let parent = el.parentElement;
192
- while (parent) {
193
- if (processedElements.has(parent)) return true;
194
- parent = parent.parentElement;
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
- function analyzeElement(el) {
200
- // Skip already processed or descendant of processed
201
- if (processedElements.has(el) || isAncestorProcessed(el)) return;
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
- const rect = el.getBoundingClientRect();
204
- const cs = window.getComputedStyle(el);
200
+ function analyzeElement(el) {
201
+ // Skip already processed or descendant of processed
202
+ if (processedElements.has(el) || isAncestorProcessed(el)) return;
205
203
 
206
- // Skip if already using content-visibility
207
- if (cs["content-visibility"] && cs["content-visibility"] !== "visible") return;
204
+ const rect = el.getBoundingClientRect();
205
+ const cs = window.getComputedStyle(el);
208
206
 
209
- // Skip elements not meeting size criteria
210
- if (rect.height < minHeight || rect.width === 0) return;
207
+ // Skip if already using content-visibility
208
+ if (cs["content-visibility"] && cs["content-visibility"] !== "visible") return;
211
209
 
212
- // Check if element is below the viewport + threshold
213
- const distanceFromViewport = rect.top - viewportHeight;
214
- if (distanceFromViewport < threshold) return;
210
+ // Skip elements not meeting size criteria
211
+ if (rect.height < minHeight || rect.width === 0) return;
215
212
 
216
- const childCount = el.querySelectorAll("*").length;
213
+ // Check if element is below the viewport + threshold
214
+ const distanceFromViewport = rect.top - viewportHeight;
215
+ if (distanceFromViewport < threshold) return;
217
216
 
218
- // Skip elements with too few children
219
- if (childCount < minChildren) return;
217
+ const childCount = el.querySelectorAll("*").length;
220
218
 
221
- processedElements.add(el);
219
+ // Skip elements with too few children
220
+ if (childCount < minChildren) return;
222
221
 
223
- opportunities.push({
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
- // Walk all elements in the DOM
234
- const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false);
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
- while (walker.nextNode()) {
237
- analyzeElement(walker.currentNode);
238
- }
234
+ // Walk all elements in the DOM
235
+ const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false);
239
236
 
240
- // Sort by child count (highest impact first)
241
- opportunities.sort((a, b) => b.childElements - a.childElements);
237
+ while (walker.nextNode()) {
238
+ analyzeElement(walker.currentNode);
239
+ }
242
240
 
243
- // Display results
244
- console.group("šŸ’” Content-Visibility Opportunities");
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
- if (opportunities.length === 0) {
252
- console.log("%cāœ… No opportunities found with current settings.", "color: #22c55e; font-weight: bold");
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
- `%cFound ${opportunities.length} element(s) that could benefit from content-visibility: auto`,
257
- "font-weight: bold;",
247
+ `%cSettings: threshold=${threshold}px, minHeight=${minHeight}px, minChildren=${minChildren}`,
248
+ "color: #888;",
258
249
  );
259
250
  console.log("");
260
251
 
261
- // Show table without element reference
262
- const tableData = opportunities.slice(0, 20).map((opportunity) => ({
263
- selector: opportunity.selector,
264
- height: opportunity.height,
265
- distanceFromViewport: opportunity.distanceFromViewport,
266
- childElements: opportunity.childElements,
267
- estimatedSavings: opportunity.estimatedSavings,
268
- }));
269
- console.table(tableData);
270
-
271
- if (opportunities.length > 20) {
272
- console.log(`... and ${opportunities.length - 20} more elements`);
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
- console.log("");
284
- console.group("šŸ“ Implementation Example");
285
- console.log("Add this CSS to optimize rendering:");
286
- console.log("");
287
- console.log(
288
- "%c/* Optimize offscreen content */\n" +
289
- ".your-selector {\n" +
290
- " content-visibility: auto;\n" +
291
- " contain-intrinsic-size: auto 500px; /* Use actual height */\n" +
292
- "}",
293
- "font-family: monospace; background: #1e1e1e; color: #9cdcfe; padding: 10px; border-radius: 4px;",
294
- );
295
- console.groupEnd();
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
- console.groupEnd();
324
+ window.analyzeContentVisibilityOpportunities = analyzeContentVisibilityOpportunities;
299
325
 
300
- return {
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()",
@@ -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.
@@ -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