Loading
Content Visibility

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:

  1. detectContentVisibility() - Finds all elements with content-visibility: auto and provides inspection tools
  2. analyzeContentVisibilityOpportunities(options) - Walks the DOM to find offscreen elements that could benefit from content-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-size warnings 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):

OptionDefaultDescription
threshold0Distance in px from viewport bottom. Use negative values (e.g., -200) to include elements closer to the fold
minHeight100Minimum element height in px to be considered
minChildren5Minimum 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:

ValueBehavior
visibleDefault. Content always rendered.
autoContent rendered when near viewport. Browser skips rendering for offscreen elements.
hiddenContent never rendered (like display: none but preserves element state).

When using content-visibility: auto:

  1. Initial Load: Offscreen elements are not rendered
  2. Scroll: Elements render as they approach the viewport
  3. 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:

  1. Measure the actual element: Use DevTools to inspect the element's rendered height, or use the analyzeContentVisibilityOpportunities() function which reports element heights
  2. Use an average: For dynamic content (like comments or cards), use an average expected height
  3. Err on the side of larger: A slightly larger estimate is better than too small, it reduces the chance of noticeable layout shifts
  4. 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 TypeTypical Height
Footer200-400px
Comments section600-1000px
Product card300-450px
Blog article section400-800px
Sidebar widget200-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:

ScenarioEstimated Improvement
Long blog post with comments100-300ms render time
E-commerce product grid (50+ items)200-500ms render time
Documentation page with many sections150-400ms render time
Social feed with infinite scroll300-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:

Real-world Example:

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.