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
- Chrome. This page used version 152.0.7977.76 on Windows. The figures below are one capture on one machine, taken on 2026-09-11, and they move between machines and Chrome versions.
- Node 22 or later and
puppeteer-core, installed withnpm i puppeteer-core. It drives the Chrome you already have. See the puppeteer API. - The Chrome path in each script is the Windows default. On macOS it is
/Applications/Google Chrome.app/Contents/MacOS/Google Chrome. - Save this as
dom-size-server.mjs. It serves one page with a known shape: 800 list items under a singleul, and a chain of 30 wrappers around one paragraph.
// 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
- Step 1.
Start the probe page. Stop it with Ctrl-C when you are finished.
node dom-size-server.mjsdom size probe on http://localhost:8893/Open
http://localhost:8893/in Chrome, press F12, select the Console tab and rundocument.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. - Step 2.
Measure the three numbers together. Save this as
dom-size.mjsand 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#rowsThe 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.
- 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/HTTPurl 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 olA 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.
- 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 111Three findings in four lines. Identical trees of
divandlidiffer by exactly 100 in the browser's own counter, and settinglist-style: nonecloses 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 thatgetElementsByTagNamecannot 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
Thresholds
What to check next
- How to find detached DOM nodes: the nodes that count against the same total after they leave the tree.
- How to check memory leak in a web page: the same counters read across rounds, which is how growth is proved.
- How to check page size: the bytes that produced the tree, measured on the wire.
- How to check which third party scripts a page loads: a widget that injects several hundred nodes is the usual reason a total jumps between releases.
- How to check accessibility tree: the tree Chrome builds from this one, and which grows with it.
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.
Related on this site
basic6 minpublished updated Maks Verny