Content Visibility
Detect and analyze all elements using content-visibility: auto on a page. This CSS property is a powerful rendering optimization that allows browsers to skip layout and painting work for offscreen content, significantly improving initial page load performance.
This page provides two complementary analysis functions:
detectContentVisibility()- Finds all elements withcontent-visibility: autoand provides inspection toolsanalyzeContentVisibilityOpportunities(options)- Walks the DOM to find offscreen elements that could benefit fromcontent-visibility: auto, with configurable thresholds for distance, height, and child count
Attribution
This snippet code is based on the script (opens in a new tab) by Arjen Karel (opens in a new tab)
Snippet
// Detect elements with content-visibility: auto and analyze optimization opportunities
function detectContentVisibility() {
// Create an object to store the results
const results = {
autoElements: [],
hiddenElements: [],
visibleElements: [],
nodeArray: [],
};
// Get the name of the node
function getName(node) {
const name = node.nodeName;
return node.nodeType === 1 ? name.toLowerCase() : name.toUpperCase().replace(/^#/, "");
}
// Get the selector for an element
function getSelector(node) {
let sel = "";
try {
while (node && node.nodeType !== 9) {
const el = node;
const part = el.id
? "#" + el.id
: getName(el) +
(el.classList &&
el.classList.value &&
el.classList.value.trim() &&
el.classList.value.trim().length
? "." + el.classList.value.trim().replace(/\s+/g, ".")
: "");
if (sel.length + part.length > 100 - 1) return sel || part;
sel = sel ? part + ">" + sel : part;
if (el.id) break;
node = el.parentNode;
}
} catch (err) {
// Do nothing...
}
return sel;
}
// Check if element is in viewport
function isInViewport(element) {
const rect = element.getBoundingClientRect();
return (
rect.top < window.innerHeight &&
rect.bottom > 0 &&
rect.left < window.innerWidth &&
rect.right > 0
);
}
// Get element dimensions and position
function getElementInfo(node) {
const rect = node.getBoundingClientRect();
const cs = window.getComputedStyle(node);
return {
selector: getSelector(node),
contentVisibility: cs["content-visibility"],
containIntrinsicSize: cs["contain-intrinsic-size"] || "not set",
width: Math.round(rect.width),
height: Math.round(rect.height),
top: Math.round(rect.top + window.scrollY),
inViewport: isInViewport(node),
};
}
// Recursively find all elements with content-visibility
function findContentVisibilityElements(node) {
const cs = window.getComputedStyle(node);
const cv = cs["content-visibility"];
if (cv && cv !== "visible") {
const info = getElementInfo(node);
if (cv === "auto") {
results.autoElements.push(info);
results.nodeArray.push(node);
} else if (cv === "hidden") {
results.hiddenElements.push(info);
}
}
for (let i = 0; i < node.children.length; i++) {
findContentVisibilityElements(node.children[i]);
}
}
// Run the detection
findContentVisibilityElements(document.body);
// Display results
console.group("🔍 Content-Visibility Detection");
if (results.autoElements.length === 0 && results.hiddenElements.length === 0) {
console.log("%cNo content-visibility usage found.", "color: orange; font-weight: bold;");
console.log("");
console.log("💡 Consider applying content-visibility: auto to:");
console.log(" • Footer sections");
console.log(" • Below-the-fold content");
console.log(" • Long lists or card grids");
console.log(" • Tab content that is not initially visible");
console.log(" • Accordion/collapsible content");
} else {
// Auto elements
if (results.autoElements.length > 0) {
console.group("✅ content-visibility: auto");
console.log(`Found ${results.autoElements.length} element(s)`);
console.table(results.autoElements);
console.groupEnd();
}
// Hidden elements
if (results.hiddenElements.length > 0) {
console.group("🔒 content-visibility: hidden");
console.log(`Found ${results.hiddenElements.length} element(s)`);
console.table(results.hiddenElements);
console.groupEnd();
}
// Check for missing contain-intrinsic-size
const missingIntrinsicSize = results.autoElements.filter(
(el) => el.containIntrinsicSize === "not set" || el.containIntrinsicSize === "none",
);
if (missingIntrinsicSize.length > 0) {
console.group("⚠️ Missing contain-intrinsic-size");
console.log(
"%cThese elements lack contain-intrinsic-size, which may cause layout shifts:",
"color: #f59e0b; font-weight: bold",
);
console.table(
missingIntrinsicSize.map((el) => ({
selector: el.selector,
height: el.height + "px",
})),
);
console.log("");
console.log("💡 Add contain-intrinsic-size to prevent CLS:");
console.log(" contain-intrinsic-size: auto 500px;");
console.groupEnd();
}
// Nodes for inspection
console.group("🔎 Elements for inspection");
console.log("Click to expand and inspect in Elements panel:");
results.nodeArray.forEach((node, i) => {
console.log(`${i + 1}. `, node);
});
console.groupEnd();
}
console.groupEnd();
return results;
}
// Analyze opportunities for content-visibility optimization
// Options:
// - threshold: distance from viewport bottom to consider "offscreen" (default: 0)
// - minHeight: minimum element height in px (default: 100)
// - minChildren: minimum child elements to be considered (default: 5)
function analyzeContentVisibilityOpportunities(options = {}) {
const { threshold = 0, minHeight = 100, minChildren = 5 } = options;
const viewportHeight = window.innerHeight;
const opportunities = [];
const processedElements = new Set();
function getSelector(node) {
let sel = "";
try {
while (node && node.nodeType !== 9) {
const el = node;
const name = el.nodeName.toLowerCase();
const part = el.id
? "#" + el.id
: name +
(el.classList && el.classList.value && el.classList.value.trim()
? "." + el.classList.value.trim().split(/\s+/).slice(0, 2).join(".")
: "");
if (sel.length + part.length > 80) return sel || part;
sel = sel ? part + ">" + sel : part;
if (el.id) break;
node = el.parentNode;
}
} catch (err) {}
return sel;
}
function estimateRenderSavings(childCount) {
const baseMs = childCount * 0.2;
if (baseMs < 5) return "Low (~" + baseMs.toFixed(1) + "ms)";
if (baseMs < 20) return "Medium (~" + baseMs.toFixed(1) + "ms)";
return "High (~" + baseMs.toFixed(1) + "ms)";
}
function isAncestorProcessed(el) {
let parent = el.parentElement;
while (parent) {
if (processedElements.has(parent)) return true;
parent = parent.parentElement;
}
return false;
}
function analyzeElement(el) {
// Skip already processed or descendant of processed
if (processedElements.has(el) || isAncestorProcessed(el)) return;
const rect = el.getBoundingClientRect();
const cs = window.getComputedStyle(el);
// Skip if already using content-visibility
if (cs["content-visibility"] && cs["content-visibility"] !== "visible") return;
// Skip elements not meeting size criteria
if (rect.height < minHeight || rect.width === 0) return;
// Check if element is below the viewport + threshold
const distanceFromViewport = rect.top - viewportHeight;
if (distanceFromViewport < threshold) return;
const childCount = el.querySelectorAll("*").length;
// Skip elements with too few children
if (childCount < minChildren) return;
processedElements.add(el);
opportunities.push({
selector: getSelector(el),
height: Math.round(rect.height) + "px",
distanceFromViewport: Math.round(distanceFromViewport) + "px",
childElements: childCount,
estimatedSavings: estimateRenderSavings(childCount),
element: el,
});
}
// Walk all elements in the DOM
const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_ELEMENT, null, false);
while (walker.nextNode()) {
analyzeElement(walker.currentNode);
}
// Sort by child count (highest impact first)
opportunities.sort((a, b) => b.childElements - a.childElements);
// Display results
console.group("💡 Content-Visibility Opportunities");
console.log(
`%cSettings: threshold=${threshold}px, minHeight=${minHeight}px, minChildren=${minChildren}`,
"color: #888;",
);
console.log("");
if (opportunities.length === 0) {
console.log("%c✅ No opportunities found with current settings.", "color: #22c55e; font-weight: bold");
console.log("Try adjusting: analyzeContentVisibilityOpportunities({ threshold: -200, minHeight: 50, minChildren: 3 })");
} else {
console.log(
`%cFound ${opportunities.length} element(s) that could benefit from content-visibility: auto`,
"font-weight: bold;",
);
console.log("");
// Show table without element reference
const tableData = opportunities.slice(0, 20).map(({ element, ...rest }) => rest);
console.table(tableData);
if (opportunities.length > 20) {
console.log(`... and ${opportunities.length - 20} more elements`);
}
// Log elements for inspection
console.log("");
console.group("🔎 Elements for inspection");
opportunities.slice(0, 10).forEach((opp, i) => {
console.log(`${i + 1}. `, opp.element);
});
console.groupEnd();
console.log("");
console.group("📝 Implementation Example");
console.log("Add this CSS to optimize rendering:");
console.log("");
console.log(
"%c/* Optimize offscreen content */\n" +
".your-selector {\n" +
" content-visibility: auto;\n" +
" contain-intrinsic-size: auto 500px; /* Use actual height */\n" +
"}",
"font-family: monospace; background: #1e1e1e; color: #9cdcfe; padding: 10px; border-radius: 4px;",
);
console.groupEnd();
}
console.groupEnd();
return {
opportunities: opportunities.map(({ element, ...rest }) => rest),
totalElements: opportunities.length,
highImpact: opportunities.filter((o) => o.estimatedSavings.startsWith("High")).length,
elements: opportunities.map((o) => o.element),
};
}
// Run detection
detectContentVisibility();
console.log(
"%c\n To find optimization opportunities, run: %canalyzeContentVisibilityOpportunities()",
"color: #3b82f6; font-weight: bold;",
"color: #22c55e; font-weight: bold; font-family: monospace;"
);Understanding the Results
detectContentVisibility() Results
This function scans the entire DOM for elements using content-visibility and provides:
Auto Elements (content-visibility: auto):
- selector: CSS selector path to the element
- contentVisibility: The current value (
auto) - containIntrinsicSize: The placeholder size for layout calculations
- width/height: Current dimensions of the element
- top: Distance from the top of the document
- inViewport: Whether the element is currently visible
Hidden Elements (content-visibility: hidden):
- Elements completely hidden from rendering
- Useful for off-canvas menus, modals, or pre-rendered content
Warnings:
- Missing
contain-intrinsic-sizewarnings help prevent Cumulative Layout Shift (CLS) - Suggests appropriate height values based on current element dimensions
analyzeContentVisibilityOpportunities(options) Results
This function walks the entire DOM to find elements below the viewport that would benefit from content-visibility: auto.
Options (all optional):
| Option | Default | Description |
|---|---|---|
threshold | 0 | Distance in px from viewport bottom. Use negative values (e.g., -200) to include elements closer to the fold |
minHeight | 100 | Minimum element height in px to be considered |
minChildren | 5 | Minimum number of child elements (filters out simple containers) |
Examples:
// Default settings
analyzeContentVisibilityOpportunities()
// More aggressive - find elements closer to viewport
analyzeContentVisibilityOpportunities({ threshold: -200 })
// Find smaller elements with fewer children
analyzeContentVisibilityOpportunities({ minHeight: 50, minChildren: 3 })
// Combine options
analyzeContentVisibilityOpportunities({ threshold: -100, minHeight: 80, minChildren: 10 })Output fields:
- selector: Element identifier
- height: Current height (use this for
contain-intrinsic-size) - distanceFromViewport: How far below the viewport the element is
- childElements: Number of descendant elements (more = higher impact)
- estimatedSavings: Rough estimate of render time saved (Low/Medium/High)
Note: The function automatically filters out nested elements - if a parent container is selected, its children won't appear separately in the results.
How content-visibility Works
The content-visibility CSS property controls whether an element renders its contents:
| Value | Behavior |
|---|---|
visible | Default. Content always rendered. |
auto | Content rendered when near viewport. Browser skips rendering for offscreen elements. |
hidden | Content never rendered (like display: none but preserves element state). |
When using content-visibility: auto:
- Initial Load: Offscreen elements are not rendered
- Scroll: Elements render as they approach the viewport
- Memory: Rendered content may be discarded when scrolling away
Important: contain-intrinsic-size
Always pair content-visibility: auto with contain-intrinsic-size to prevent layout shifts:
.offscreen-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
}Understanding the value:
The contain-intrinsic-size property tells the browser what size to use for the element before its content is rendered. This is critical because without it, offscreen elements would have zero height, causing massive layout shifts when they render.
auto: Tells the browser to remember the actual rendered size after the first render. On subsequent navigations or when scrolling back, it uses the cached size instead of the placeholder.500px(or any height value): The estimated height of your content. This should approximate the actual rendered height of the element.
How to choose the right height value:
- Measure the actual element: Use DevTools to inspect the element's rendered height, or use the
analyzeContentVisibilityOpportunities()function which reports element heights - Use an average: For dynamic content (like comments or cards), use an average expected height
- Err on the side of larger: A slightly larger estimate is better than too small, it reduces the chance of noticeable layout shifts
- It doesn't need to be exact: The browser will adjust once content renders, this is just a placeholder to reserve space
Example values by content type:
| Content Type | Typical Height |
|---|---|
| Footer | 200-400px |
| Comments section | 600-1000px |
| Product card | 300-450px |
| Blog article section | 400-800px |
| Sidebar widget | 200-350px |
Performance Impact
Potential Benefits:
- Faster Initial Render: Browser skips layout/paint for offscreen content
- Reduced Main Thread Work: Less JavaScript style recalculation
- Lower Memory Usage: Offscreen content not in render tree
- Improved INP: Less work during scrolling interactions
Typical Savings:
| Scenario | Estimated Improvement |
|---|---|
| Long blog post with comments | 100-300ms render time |
| E-commerce product grid (50+ items) | 200-500ms render time |
| Documentation page with many sections | 150-400ms render time |
| Social feed with infinite scroll | 300-800ms render time |
Best Practices
Good Candidates for content-visibility: auto:
- Footer sections
- Below-the-fold content sections
- Long lists or card grids
- Comments sections
- Related content / recommendations
- Tab panels (non-active)
- Accordion/collapsible content
- Infinite scroll items
Avoid Using On:
- Above-the-fold content (defeats the purpose)
- Elements with animations that need immediate rendering
- Content that affects layout calculations above it
- Very small elements (overhead not worth it)
Browser Support
content-visibility is supported in:
- Chrome 85+
- Edge 85+
- Opera 71+
- Chrome for Android 85+
Not yet supported in Firefox and Safari (as of 2024), but these browsers simply ignore the property, so it's safe to use as a progressive enhancement.
Implementation Examples
Basic Usage:
/* Apply to sections below the fold */
.content-section:not(:first-child) {
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}Footer Optimization:
footer {
content-visibility: auto;
contain-intrinsic-size: auto 400px;
}Card Grid Optimization:
/* Skip rendering for cards far from viewport */
.product-card:nth-child(n + 5) {
content-visibility: auto;
contain-intrinsic-size: 280px 350px;
}Comments Section:
.comments-container {
content-visibility: auto;
contain-intrinsic-size: auto 800px;
}Further Reading
For an in-depth understanding of content-visibility and rendering optimization:
- content-visibility: the new CSS property that boosts your rendering performance (opens in a new tab) | web.dev
- CSS Containment (opens in a new tab) | MDN Web Docs
- content-visibility on MDN (opens in a new tab) | Complete reference
- Improve Largest Contentful Paint with content-visibility (opens in a new tab) | web.dev
Real-world Example:
- Add content-visibility to NoteButton component (opens in a new tab) | A practical PR showing before/after DevTools screenshots of layout and layers improvements when applying
content-visibilityto a React component
Note
This snippet analyzes computed styles, so it will detect content-visibility applied through any method (inline styles, stylesheets, or JavaScript). The analysis runs synchronously and may take a moment on pages with many elements.