How to check DOM size of a page

Open DevTools, select the Console tab and run document.getElementsByTagName('*').length. That returns one of the three numbers that matter. A script driving Chrome over the DevTools protocol adds the other two, maximum depth and the largest child count under one element. The probe page below reports 1,634 elements, depth 33, 800 children.

Why check this

Run this before release on any page that renders a list from data, and again after a change to a table, a grid or an infinite scroll. The three numbers move independently. A page can hold few elements and still stall because one parent owns 800 children, and a page with a modest total can be 30 levels deep because a layout component wraps everything twice on every render.

The failure it prevents is the filter click that freezes the tab. Every style recalculation walks the children of the element that changed, so an order list with 800 rows under one ul pays for all 800 on each keystroke in the search box. The same tree is copied into the accessibility tree, so the cost lands on assistive technology as well.

Prerequisites

// dom-size-server.mjs   run: node dom-size-server.mjs   stop: Ctrl-C
import http from 'node:http';
const rows = Array.from({ length: 800 }, (_, i) =>
  `<li class="row"><span class="cell">order ${i + 1}</span></li>`).join('');
let card = '<p class="deep">nested content</p>';
for (let i = 0; i < 30; i++) card = `<div class="w${i}">${card}</div>`;
const page = `<!doctype html><meta charset="utf-8"><title>dom size probe</title>
<h1>dom size probe</h1>
<section id="cards">${card}</section>
<ul id="rows">${rows}</ul>`;
http.createServer((req, res) => {
  res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
  res.end(page);
}).listen(8893, () => console.log('dom size probe on http://localhost:8893/'));

Steps

  1. Step 1.

    Start the probe page. Stop it with Ctrl-C when you are finished.

    node dom-size-server.mjs
    
    dom size probe on http://localhost:8893/

    Open http://localhost:8893/ in Chrome, press F12, select the Console tab and run document.getElementsByTagName('*').length. It answers 1639, the count for the whole document. Keep that number; the next step produces two more that disagree with it on purpose.

  2. Step 2.

    Measure the three numbers together. Save this as dom-size.mjs and run it against the probe page.

    // dom-size.mjs   run: node dom-size.mjs <url>
    import { launch } from 'puppeteer-core';
    const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
    const browser = await launch({ executablePath: CHROME, headless: true });
    const page = await browser.newPage();
    const cdp = await page.createCDPSession();
    await page.goto(process.argv[2], { waitUntil: 'networkidle2' });
    await cdp.send('Performance.enable');
    await cdp.send('HeapProfiler.collectGarbage');
    const shape = await page.evaluate(() => {
      let maxDepth = 0, deepest = '', maxKids = 0, widest = '';
      const id = (el) => el.tagName.toLowerCase() + (el.id ? '#' + el.id : '');
      const walk = (el, d, path) => {
        const p = path + ' > ' + id(el);
        if (d > maxDepth) { maxDepth = d; deepest = p; }
        if (el.children.length > maxKids) { maxKids = el.children.length; widest = id(el); }
        for (const c of el.children) walk(c, d + 1, p);
      };
      walk(document.body, 1, '');
      const w = document.createTreeWalker(document, NodeFilter.SHOW_ALL);
      let all = 1;
      while (w.nextNode()) all++;
      return { body: document.body.getElementsByTagName('*').length,
               elements: document.getElementsByTagName('*').length, all, maxDepth,
               deepest: deepest.split(' > ').slice(-5).join(' > '), maxKids, widest };
    });
    const { metrics } = await cdp.send('Performance.getMetrics');
    const v = (n) => metrics.find((m) => m.name === n)?.value;
    console.log('url                  ', process.argv[2]);
    console.log('elements in body     ', shape.body);
    console.log('elements in document ', shape.elements);
    console.log('nodes, tree walker   ', shape.all);
    console.log('nodes, Performance   ', v('Nodes'));
    console.log('event listeners      ', v('JSEventListeners'));
    console.log('max depth below body ', shape.maxDepth, shape.deepest);
    console.log('max children         ', shape.maxKids, 'at', shape.widest);
    await browser.close();
    
    url                   http://localhost:8893/
    elements in body      1634
    elements in document  1639
    nodes, tree walker    2447
    nodes, Performance    3247
    event listeners       0
    max depth below body  33 div > div > div > div > p
    max children          800 at ul#rows

    The forced collection before the reading is not decoration. Without it the node counter still holds the nodes of the document you navigated away from, which is covered on How to check memory leak in a web page.

  3. Step 3.

    Run the same script against a page you did not build, so you have something to compare against.

    node dom-size.mjs https://developer.mozilla.org/en-US/docs/Web/HTTP
    
    url                   https://developer.mozilla.org/en-US/docs/Web/HTTP
    elements in body      1950
    elements in document  2000
    nodes, tree walker    3496
    nodes, Performance    4635
    event listeners       78
    max depth below body  16 details > ol > li > a > code
    max children          171 at ol

    A documentation page with more elements than the probe is healthier on both other axes: half the depth and a fifth of the children under its widest parent. The total on its own would have ranked the two pages the wrong way round.

  4. Step 4.

    Find out why the counters disagree, because on a real page the gap is where the nodes hide. Save this as node-counters.mjs.

    // node-counters.mjs   run: node node-counters.mjs
    import { launch } from 'puppeteer-core';
    const CHROME = 'C:/Program Files/Google/Chrome/Application/chrome.exe';
    const browser = await launch({ executablePath: CHROME, headless: true });
    const page = await browser.newPage();
    const cdp = await page.createCDPSession();
    await cdp.send('Performance.enable');
    const shadow = `<my-list></my-list><script>
    customElements.define('my-list', class extends HTMLElement {
      connectedCallback() {
        const r = this.attachShadow({ mode: 'open' });
        for (let i = 0; i < 100; i++) r.appendChild(document.createElement('div'));
      }
    });
    <\/script>`;
    const cases = {
      '100 li inside ul': '<ul>' + '<li>a</li>'.repeat(100) + '</ul>',
      '100 div': '<div>' + '<div>a</div>'.repeat(100) + '</div>',
      '100 li, list-style none': '<style>li{list-style:none}</style><ul>' + '<li>a</li>'.repeat(100) + '</ul>',
      '100 div in a shadow root': shadow,
    };
    for (const [name, body] of Object.entries(cases)) {
      await page.goto('data:text/html,' + encodeURIComponent('<!doctype html><title>t</title>' + body));
      await cdp.send('HeapProfiler.collectGarbage');
      const n = await page.evaluate(() => {
        const w = document.createTreeWalker(document, NodeFilter.SHOW_ALL);
        let all = 1;
        while (w.nextNode()) all++;
        return { all, els: document.getElementsByTagName('*').length };
      });
      const { metrics } = await cdp.send('Performance.getMetrics');
      console.log(name.padEnd(25), 'getElementsByTagName', String(n.els).padStart(4),
                  '  tree walker', String(n.all).padStart(4),
                  '  Performance.Nodes', metrics.find((m) => m.name === 'Nodes').value);
    }
    await browser.close();
    
    100 li inside ul          getElementsByTagName  105   tree walker  208   Performance.Nodes 308
    100 div                   getElementsByTagName  105   tree walker  208   Performance.Nodes 208
    100 li, list-style none   getElementsByTagName  106   tree walker  210   Performance.Nodes 210
    100 div in a shadow root  getElementsByTagName    6   tree walker   10   Performance.Nodes 111

    Three findings in four lines. Identical trees of div and li differ by exactly 100 in the browser's own counter, and setting list-style: none closes the gap, so each list marker is counted as a node. The shadow root case is the one that matters on a component-based site: 100 elements that getElementsByTagName cannot see and the browser counts anyway.

How to read the result

| What you see | What it means | What to do | | --- | --- | --- | | Elements in body above 800 | Lighthouse warns at this point | Compare against the depth and child figures before acting on the total alone. | | Max children in the hundreds | One parent owns a long list | Paginate, virtualise, or split the list into sections. Style recalculation walks all of them. | | Max depth above 30 | Wrapper components nest inside each other | Read the printed path. Repeated tag names in it name the component that doubles up. | | Performance.Nodes far above the tree walker count | List markers, text nodes, or a shadow root | Run the step 4 comparison on the page. A shadow root hides its contents from DOM queries. | | Event listeners rising with the element count | Each row binds its own handler | Move to one delegated listener on the container. |

Common mistakes

Sign: The console count and the Lighthouse count differ by a few elements.Cause: Lighthouse counts the nodes under body, and document.getElementsByTagName('*') counts the whole document, including head, title, meta and link elements. The probe page here reads 1634 against 1639 for the same DOM. Quote which of the two a number came from.
Sign: A component-heavy page reports a small element count and still feels heavy.Cause: getElementsByTagName and querySelectorAll do not descend into shadow roots. In step 4 a custom element with 100 divs in an open shadow root reports 6 elements to the DOM query and 111 nodes to the browser counter. Read Performance.Nodes on any page built from web components.
Sign: Performance.Nodes is higher than any count you can reproduce by walking the DOM.Cause: It counts text nodes, and it counts one extra node for every list item marker, which is documented nowhere and visible in step 4 as a difference of exactly 100 between li and div. It also counts the nodes of a document that has not been collected yet, so force a collection before reading it.
Sign: The total element count is the only figure in the ticket.Cause: Depth and child count are separate failures with separate fixes. The probe page has fewer elements than the MDN page measured in step 3, four times the children under one parent, and twice the depth. A single number cannot carry that.

Thresholds

Lighthouse warns when the body element holds more than about 800 nodes, and errors above about 1,400 Source: https://developer.chrome.com/docs/lighthouse/performance/dom-size
Probe page: 1634 elements in body, maximum depth 33, maximum 800 children under ul#rows Source: Measured with Chrome 152.0.7977.76 on 2026-09-11, see the Verified block

What to check next

FAQ

How many DOM nodes is too many?

Lighthouse warns above roughly 800 nodes under body and errors above roughly 1,400. Treat those as a prompt to look, not a verdict. A 3,000 node page with shallow nesting and no oversized parent behaves better than a 900 node page with 800 children in one list.

How do I count DOM elements on a page?

Run document.getElementsByTagName('*').length in the Console. It counts elements only, in the main document, and it skips shadow roots. For the number the browser itself uses, read Nodes from Performance.getMetrics over the DevTools protocol, as step 2 does.

Does DOM size affect performance on its own?

Size alone does not. The cost appears in style recalculation, layout and the memory held per node, and it scales with how many children one element has and how deep the tree goes. Measure those two before rewriting a page around the total.

Where does DevTools show the DOM node count?

Open DevTools, then More tools, then Performance monitor. The DOM Nodes counter there is the same value the scripts on this page read, and it updates live while you interact with the page.

Verified

Verified by Maks VernyChrome 152.0.7977.76node 22.23.2puppeteer-core 25.10.0

Each output block is what the command above it printed on that date, on the host named in the step. Figures read from a live site move between runs. Compare the shape of the answer rather than the digits, and see the methodology for how a page is re-verified.

basic6 minpublished updated Maks Verny