Skip to main content
All fixes

Page speed, redirects, Core Web Vitals

Reduce excessive DOM size: keep nodes under 1500

DOMs over 1500 nodes slow down style recalc, layout, and INP. Virtualize long lists, lazy-render off-screen content, and prune unused wrappers to cut DOM weight.

What's happening

Lighthouse's "Avoid an excessive DOM size" audit warns when a page exceeds 1500 total nodes, has a single parent with more than 60 children, or has a tree depth greater than 32. Big DOMs slow every browser operation: style recalculation, layout, paint, and especially React reconciliation. The cost is paid on initial load and on every interaction.

Chrome DevTools' Performance panel shows style recalc and layout costs in the Summary tab, often dominating the main thread on big-DOM pages. The Memory tab shows DOM node count, and the Performance Insights tab flags expensive layout shifts caused by DOM size. INP regressions on these pages usually trace back to DOM-traversal cost in event handlers.

The most common culprit is rendering a long list (1000+ rows of a data table, infinite-scroll feed, comments thread) without virtualization. Each row often has 5-10 nested elements, so 1000 rows means 5000-10000 DOM nodes — well past the 1500-node guideline.

Why it matters

Excessive DOM size correlates strongly with poor INP. Every click handler that touches the DOM, every state update that triggers re-render, every CSS recalc — all scale linearly with node count. A 10000-node DOM can spend 200-400ms on style recalc alone, blowing past the INP threshold.

Memory consumption is the secondary issue. Each DOM node averages 200-300 bytes plus per-element JS state (event listeners, React fiber nodes). A 10000-node React app holds 5-10MB just in DOM + reconciler, which on low-memory Android devices triggers GC pauses and OOM crashes.

Common causes

  • Long lists rendered without virtualization (react-virtuoso, react-window, TanStack Virtual).
  • Component trees that wrap each child in 3-5 layers of div containers for layout.
  • Carousels and tabs that render all panels eagerly instead of lazy-mounting active tab.
  • Server-rendered HTML for pages that should use pagination or infinite scroll.
  • Modals, dropdowns, and tooltips that render in the DOM even when closed.
  • Component libraries with deeply nested wrapper structures (10+ levels).
  • Off-screen content rendered eagerly instead of with content-visibility: auto.

Detect this on your site

Run a quick scan with the Speed Test. The tool surfaces this exact issue with the records and context needed to apply the fix below.

Open Speed Test

How to fix it

  1. 1

    Measure current DOM size

    Run document.querySelectorAll('*').length in the DevTools console. Lighthouse's audit shows the largest parent (most children) and deepest descendant. Both metrics matter — fix the largest parents first.

  2. 2

    Virtualize long lists

    For lists with 50+ items, use TanStack Virtual or react-window. They render only the rows visible in the viewport plus a small overscan buffer, dropping a 1000-row list from 10000 nodes to 100. Fixed-height rows are simplest; variable height needs measurement.

  3. 3

    Apply content-visibility: auto to off-screen sections

    content-visibility: auto tells the browser to skip rendering work for elements outside the viewport. Combined with contain-intrinsic-size to reserve space, you skip layout, style, and paint for distant sections without changing the DOM structure.

  4. 4

    Conditionally mount modals and dropdowns

    Don't render modal contents in the DOM until the modal opens. Use {isOpen && } instead of {isOpen?: null} with always-mounted siblings. The DOM stays small; mount cost is paid only on actual use.

  5. 5

    Flatten unnecessary wrapper divs

    Audit your component tree for divs whose only job is to hold a single child. Replace with React.Fragment or remove entirely. Each level of nesting compounds reconciler work and CSS-in-JS specificity costs.

  6. 6

    Lazy-mount tab contents

    Tab UIs often pre-render all panels for instant switching. Render only the active tab's content; mount others on first activation. Trade a one-time switch latency for a much smaller initial DOM.

  7. 7

    Paginate or infinite-scroll long content

    Server-rendering 1000 comments at once is bad for SSR cost and for client DOM size. Paginate, or use cursor-based infinite scroll with virtualization. Show 20-50 items at once, fetch more on demand.

Example

// Bad: renders 1000+ DOM nodes
<ul>
  {items.map((item) => <Row key={item.id} item={item} />)}
</ul>

// Good: TanStack Virtual renders only visible rows
import { useVirtualizer } from "@tanstack/react-virtual";

function VirtualList({ items }) {
  const parentRef = useRef(null);
  const virtualizer = useVirtualizer({
    count: items.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 48,
    overscan: 5,
  });
  return (
    <div ref={parentRef} style={{ height: 600, overflow: "auto" }}>
      <div style={{ height: virtualizer.getTotalSize(), position: "relative" }}>
        {virtualizer.getVirtualItems().map((v) => (
          <Row key={v.key} item={items[v.index]}
            style={{ position: "absolute", top: v.start, height: v.size }} />
        ))}
      </div>
    </div>
  );
}

TanStack Virtual cuts a 1000-row list to 20 rendered nodes.

Frequently asked

No it's a Lighthouse warning threshold. Sites can ship 3000-5000 nodes without obvious user-facing issues if interactions are simple. The cost compounds with React reconciler work and CSS-in-JS so the practical ceiling depends on your stack.

No. The element is still in the DOM and crawlable; the browser just skips visual rendering work until it scrolls into view. Google explicitly supports it.

Use it whenever a wrapper div serves no styling or layout purpose. Don't over-nest but also don't remove wrappers that you actually need for flex/grid layout.

Related fixes