├── .gitignore ├── README.md ├── promo-material ├── promo-icon.png ├── promo-large.png ├── promo-large.psd ├── promo-screenshot-1.png └── promo-screenshot-2.png └── src ├── .eslintignore ├── .eslintrc ├── background.js ├── component-stats.html ├── component-stats.js ├── d3.js ├── devtools.html ├── devtools.js ├── dom-treemap.html ├── dom-treemap.js ├── icon-list-up.svg ├── icon.svg ├── icon128.png ├── icon16.png ├── icon48.png └── manifest.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | **/*.zip 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DOM Treemap 2 | 3 | A [Chrome](https://chrome.google.com/webstore/detail/dom-treemap/albnoggfgnooeefdjpncieecohhblonh) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/dom-treemap/) Devtools extension that helps you explore the distribution of DOM nodes in the document tree. 4 | 5 | ![](promo-material/promo-large.png) 6 | 7 | After a Google Lighthouse audit complaining an excessive DOM size, have you ever wondered in which corner of your document most DOM nodes are buried? Neither Lighthouse nor the Devtools themselves help finding those areas. 8 | 9 | That's what DOM Treemap is for. It extends your Devtools Elements tab with an additional pane which visualizes the distribution of node descendants of the currently inspected DOM element. This makes it easy for you drill down your DOM and to locate all those hidden node heavy parts. 10 | 11 | On top of that there is an extra tab for all those BEM affiniciados which visualizes via yet another treemap how node intensive your different BEM component are in average. 12 | 13 | ![](promo-material/promo-screenshot-1.png) 14 | 15 | ![](promo-material/promo-screenshot-2.png) 16 | 17 | [DOM Treemap in the Chrome Web Store](https://chrome.google.com/webstore/detail/dom-treemap/albnoggfgnooeefdjpncieecohhblonh) 18 | 19 | [DOM Treemap in the Firefox add-ons catalogue](https://addons.mozilla.org/en-US/firefox/addon/dom-treemap/) 20 | -------------------------------------------------------------------------------- /promo-material/promo-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schepp/dom-treemap-devtools-extension/ae12343bf908f8413ceb2ce5935d5de3c710b1b1/promo-material/promo-icon.png -------------------------------------------------------------------------------- /promo-material/promo-large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schepp/dom-treemap-devtools-extension/ae12343bf908f8413ceb2ce5935d5de3c710b1b1/promo-material/promo-large.png -------------------------------------------------------------------------------- /promo-material/promo-large.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schepp/dom-treemap-devtools-extension/ae12343bf908f8413ceb2ce5935d5de3c710b1b1/promo-material/promo-large.psd -------------------------------------------------------------------------------- /promo-material/promo-screenshot-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schepp/dom-treemap-devtools-extension/ae12343bf908f8413ceb2ce5935d5de3c710b1b1/promo-material/promo-screenshot-1.png -------------------------------------------------------------------------------- /promo-material/promo-screenshot-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schepp/dom-treemap-devtools-extension/ae12343bf908f8413ceb2ce5935d5de3c710b1b1/promo-material/promo-screenshot-2.png -------------------------------------------------------------------------------- /src/.eslintignore: -------------------------------------------------------------------------------- 1 | src/js/polyfills -------------------------------------------------------------------------------- /src/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb-base", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "serviceworker": true 7 | }, 8 | "globals": { 9 | "workbox": true 10 | }, 11 | "plugins": [ 12 | "compat", 13 | "no-storage" 14 | ], 15 | "rules": { 16 | "no-console": "off", 17 | "no-shadow": "off", 18 | "no-param-reassign": "off", 19 | "no-underscore-dangle": "off", 20 | "import/no-extraneous-dependencies": "off", 21 | "comma-dangle": ["error", "always-multiline"], 22 | "linebreak-style": "off", 23 | "compat/compat": "off", 24 | "no-storage/no-browser-storage": 2 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/background.js: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schepp/dom-treemap-devtools-extension/ae12343bf908f8413ceb2ce5935d5de3c710b1b1/src/background.js -------------------------------------------------------------------------------- /src/component-stats.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 21 | 22 | 23 |
24 | 25 | 26 | -------------------------------------------------------------------------------- /src/component-stats.js: -------------------------------------------------------------------------------- 1 | function constructData() { 2 | function getDomPath(elem, depth) { 3 | var stack = []; 4 | while (elem.parentNode != null) { 5 | var sibCount = 0; 6 | var sibIndex = 0; 7 | for (var i = 0; i < elem.parentNode.childNodes.length; i++) { 8 | var sib = elem.parentNode.childNodes[i]; 9 | if (sib.nodeName === elem.nodeName) { 10 | if (sib === elem) { 11 | sibIndex = sibCount; 12 | } 13 | sibCount++; 14 | } 15 | } 16 | if (elem.id) { 17 | stack.unshift('#' + elem.id); 18 | } else if (elem.className && elem.className.split) { 19 | stack.unshift('.' + elem.className.split(' ').filter(c => !!c).join('.')); 20 | } else if (sibCount > 1) { 21 | stack.unshift(elem.nodeName.toLowerCase() + ':eq(' + sibIndex + ')'); 22 | } else { 23 | stack.unshift(elem.nodeName.toLowerCase()); 24 | } 25 | elem = elem.parentNode; 26 | } 27 | 28 | stack = stack.slice(1); // removes the html element 29 | 30 | if (depth) { 31 | return stack.slice(-1 * Math.abs(depth)); 32 | } 33 | 34 | return stack; 35 | } 36 | function getDescendantNodeCount(elem) { 37 | return Array.from(elem.children).reduce((accumulator, child) => { 38 | return accumulator + getDescendantNodeCount(child); 39 | }, elem.childNodes.length); 40 | } 41 | 42 | const average = arr => arr.reduce( ( p, c ) => p + c, 0 ) / arr.length; 43 | const statsMap = {}; 44 | const findReactComponent = (el) => { 45 | for (const key in el) { 46 | if (key.startsWith('__reactInternalInstance$')) { 47 | const fiberNode = el[key]; 48 | 49 | return fiberNode && fiberNode.return && fiberNode.return.stateNode; 50 | } 51 | } 52 | return null; 53 | }; 54 | 55 | switch (true) { 56 | default: 57 | return false; 58 | break; 59 | 60 | // BEM 61 | case !!document.querySelectorAll('[class*="__"]').length: 62 | Array.from(document.querySelectorAll('[class]:not([class*="__"]):not([class*=":"])')) 63 | .filter(elem => elem.className && elem.className.split) 64 | .forEach(elem => { 65 | const primaryClassNames = elem 66 | .className 67 | .split(' ') 68 | .map(part => part.trim()) 69 | .filter(part => !!part) 70 | .filter(part => part.indexOf('--') === -1) 71 | 72 | if (!primaryClassNames.length) { 73 | return; 74 | } 75 | 76 | statsMap[primaryClassNames[0]] = statsMap[primaryClassNames[0]] || []; 77 | statsMap[primaryClassNames[0]].push(getDescendantNodeCount(elem)); 78 | }); 79 | break; 80 | 81 | // React 82 | case false && !!Array.from(document.querySelectorAll('*')).find(elem => findReactComponent(elem)): 83 | Array.from(document.querySelectorAll('*')) 84 | .map(elem => findReactComponent(elem)) 85 | .filter(elem => !!elem) 86 | .forEach(elem => { 87 | const name = getDomPath(elem); 88 | statsMap[name] = statsMap[name] || []; 89 | statsMap[name].push(getDescendantNodeCount(elem)); 90 | }); 91 | break; 92 | } 93 | 94 | return { 95 | children: Object.keys(statsMap) 96 | .filter(key => statsMap[key].length > 1) 97 | .map(key => ({ 98 | name: key, 99 | value: average(statsMap[key]), 100 | })), 101 | } 102 | } 103 | 104 | function updateTreeMap() { 105 | const treemap = (data) => { 106 | const DOM = { 107 | uid: (name) => { 108 | let count = 0; 109 | 110 | function Id(id) { 111 | this.id = id; 112 | this.href = new URL(`#${id}`, location) + ''; 113 | } 114 | 115 | Id.prototype.toString = function () { 116 | return 'url(' + this.href + ')'; 117 | }; 118 | 119 | return new Id('O-' + (name == null ? '' : name + '-') + ++count); 120 | } 121 | } 122 | const format = d3.format(',d'); 123 | const color = d3.scaleOrdinal(d3.schemeCategory10); 124 | let width = window.innerWidth; 125 | let height = Math.min(window.innerWidth, window.innerHeight); 126 | 127 | const tile = (node, x0, y0, x1, y1) => { 128 | d3.treemapBinary(node, 0, 0, width, height); 129 | for (const child of node.children) { 130 | child.x0 = x0 + child.x0 / width * (x1 - x0); 131 | child.x1 = x0 + child.x1 / width * (x1 - x0); 132 | child.y0 = y0 + child.y0 / height * (y1 - y0); 133 | child.y1 = y0 + child.y1 / height * (y1 - y0); 134 | } 135 | } 136 | 137 | const treemap = data => d3.treemap() 138 | .tile(tile) 139 | .size([width, height]) 140 | .padding(1) 141 | .round(true) 142 | (d3.hierarchy(data) 143 | .sum(d => d.value) 144 | .sort((a, b) => b.value - a.value)) 145 | 146 | const root = treemap(data); 147 | 148 | const svg = d3.create('svg') 149 | .attr('viewBox', [0, 0, width, height]) 150 | .style('font', '10px sans-serif'); 151 | 152 | const leaf = svg.selectAll('g') 153 | .data(root.leaves()) 154 | .join('g') 155 | .attr('transform', d => `translate(${d.x0},${d.y0})`); 156 | 157 | leaf.append('title') 158 | .text(d => `${d.data.name}: ${format(d.value)} average descendants`); 159 | 160 | leaf.append('rect') 161 | .attr('id', d => (d.leafUid = DOM.uid('leaf')).id) 162 | .attr('fill', d => { while (d.depth > 1) d = d.parent; return color(d.data.name); }) 163 | .attr('fill-opacity', 0.6) 164 | .attr('width', d => d.x1 - d.x0) 165 | .attr('height', d => d.y1 - d.y0); 166 | 167 | leaf.append('clipPath') 168 | .attr('id', d => (d.clipUid = DOM.uid('clip')).id) 169 | .append('use') 170 | .attr('xlink:href', d => d.leafUid.href); 171 | 172 | leaf.append('foreignObject') 173 | .attr('clip-path', d => d.clipUid) 174 | .attr('width', d => d.x1 - d.x0) 175 | .attr('height', d => d.y1 - d.y0) 176 | .attr('fill-opacity', (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null) 177 | .append('xhtml:p') 178 | .attr('style', d => `width: 100%; overflow: hidden; margin: 0; padding: min(1em, ${(d.y1 - d.y0) / 4}px); font-size: min(0.75rem, ${(d.y1 - d.y0) / 2}px); white-space: nowrap; text-overflow: ellipsis;`) 179 | .html(d => `${d.data.name}${(d.y1 - d.y0) > 20 ? '
' : ''}${(d.x1 - d.x0) > 60 ? `${format(d.value)} average descendants` : ''}`); 180 | 181 | return svg.node(); 182 | }; 183 | const appendTreeMap = (data) => { 184 | if (!data) { 185 | document.getElementById('treemap').innerHTML = '

This page doesn\'t seems to use BEM methodology.

'; 186 | return; 187 | } 188 | 189 | const svg = treemap(data); 190 | const oldSvg = document.getElementById('treemap').querySelector('svg'); 191 | 192 | document.getElementById('treemap')[oldSvg ? 'replaceChild' : 'appendChild'](svg, oldSvg); 193 | }; 194 | const evalExpression = `(() => { ${constructData.toString()}; return constructData() })()`; 195 | 196 | if (typeof chrome !== 'undefined' && chrome.devtools) { 197 | chrome.devtools.inspectedWindow.eval( 198 | evalExpression, 199 | function (data, isException) { 200 | if (isException) { 201 | console.error(isException); 202 | return; 203 | } 204 | 205 | appendTreeMap(data); 206 | } 207 | ); 208 | } else { 209 | const data = constructData(); 210 | 211 | appendTreeMap(data); 212 | } 213 | } 214 | 215 | updateTreeMap(); 216 | 217 | window.addEventListener('resize', () => { 218 | updateTreeMap(); 219 | }); 220 | 221 | if (typeof chrome !== 'undefined' && chrome.devtools) { 222 | chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { 223 | updateTreeMap(); 224 | }); 225 | 226 | document.documentElement.classList.add(`-theme-with-${chrome.devtools.panels.themeName}-background`); 227 | } 228 | 229 | -------------------------------------------------------------------------------- /src/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

DevTools sidebar

5 | 6 | -------------------------------------------------------------------------------- /src/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.elements.createSidebarPane('DOM Treemap', function(sidebar) { 2 | sidebar.setPage('dom-treemap.html'); 3 | }); 4 | 5 | chrome.devtools.panels.create('BEM Component Stats', 'icon128.png', 'component-stats.html'); 6 | -------------------------------------------------------------------------------- /src/dom-treemap.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 62 | 63 | 64 |
65 |

Currently selected element:

66 | 71 |
72 |
73 |
74 |

Descendant node distribution amongst the direct children of the currently selected node. Click on a leaf to drill yourself down the DOM tree.

75 |
76 | 77 | 78 | -------------------------------------------------------------------------------- /src/dom-treemap.js: -------------------------------------------------------------------------------- 1 | function constructData(elem) { 2 | function getDomPath(elem, depth) { 3 | var stack = []; 4 | 5 | if (elem === document.documentElement) { 6 | return[':root']; 7 | } 8 | 9 | while (elem.parentNode != null) { 10 | var sibCount = 0; 11 | var sibIndex = 0; 12 | for (var i = 0; i < elem.parentNode.childNodes.length; i++) { 13 | var sib = elem.parentNode.childNodes[i]; 14 | if (sib.nodeName === elem.nodeName) { 15 | if (sib === elem) { 16 | sibIndex = sibCount; 17 | } 18 | sibCount++; 19 | } 20 | } 21 | if (elem.id) { 22 | stack.unshift('#' + elem.id); 23 | } else if (elem.className && elem.className.split) { 24 | stack.unshift('.' + elem.className.split(' ').filter(c => !!c).join('.')); 25 | } else if (sibCount > 1) { 26 | stack.unshift(elem.nodeName.toLowerCase() + ':eq(' + sibIndex + ')'); 27 | } else { 28 | stack.unshift(elem.nodeName.toLowerCase()); 29 | } 30 | elem = elem.parentNode; 31 | } 32 | 33 | stack = stack.slice(1); // removes the html element 34 | 35 | if (depth) { 36 | return stack.slice(-1 * Math.abs(depth)); 37 | } 38 | 39 | return stack; 40 | } 41 | 42 | function getDescendantNodeCount(elem) { 43 | return Array.from(elem.children).reduce((accumulator, child) => { 44 | return accumulator + getDescendantNodeCount(child); 45 | }, elem.childNodes.length); 46 | } 47 | 48 | return { 49 | selector: getDomPath(elem).join(' > '), 50 | name: getDomPath(elem, 1).join(' > '), 51 | children: Array.from(elem.children).map((elem, index) => ({ 52 | index, 53 | selector: getDomPath(elem).join(' > '), 54 | name: getDomPath(elem, 1).join(' > '), 55 | value: getDescendantNodeCount(elem), 56 | })), 57 | } 58 | } 59 | 60 | function updateTreeMap() { 61 | const treemap = (data) => { 62 | const DOM = { 63 | uid: (name) => { 64 | let count = 0; 65 | 66 | function Id(id) { 67 | this.id = id; 68 | this.href = new URL(`#${id}`, location) + ''; 69 | } 70 | 71 | Id.prototype.toString = function () { 72 | return 'url(' + this.href + ')'; 73 | }; 74 | 75 | return new Id('O-' + (name == null ? '' : name + '-') + ++count); 76 | } 77 | } 78 | const format = d3.format(',d'); 79 | const color = d3.scaleOrdinal(d3.schemeCategory10); 80 | let width = window.innerWidth; 81 | let height = Math.min(window.innerWidth, window.innerHeight); 82 | 83 | const tile = (node, x0, y0, x1, y1) => { 84 | d3.treemapBinary(node, 0, 0, width, height); 85 | for (const child of node.children) { 86 | child.x0 = x0 + child.x0 / width * (x1 - x0); 87 | child.x1 = x0 + child.x1 / width * (x1 - x0); 88 | child.y0 = y0 + child.y0 / height * (y1 - y0); 89 | child.y1 = y0 + child.y1 / height * (y1 - y0); 90 | } 91 | } 92 | 93 | const treemap = data => d3.treemap() 94 | .tile(tile) 95 | .size([width, height]) 96 | .padding(1) 97 | .round(true) 98 | (d3.hierarchy(data) 99 | .sum(d => d.value) 100 | .sort((a, b) => b.value - a.value)) 101 | 102 | const root = treemap(data); 103 | 104 | const svg = d3.create('svg') 105 | .attr('viewBox', [0, 0, width, height]) 106 | .style('font', '10px sans-serif'); 107 | 108 | const leaf = svg.selectAll('g') 109 | .data(root.leaves()) 110 | .join('g') 111 | .attr('transform', d => `translate(${d.x0},${d.y0})`) 112 | .attr('cursor', 'pointer') 113 | .on('click', (event, d) => { 114 | const evalExpression = `inspect($0.querySelector(':scope > :nth-child(${d.data.index + 1})'))`; 115 | 116 | chrome.devtools.inspectedWindow.eval(evalExpression); 117 | }); 118 | 119 | leaf.append('title') 120 | .text(d => `${d.data.name}: ${format(d.value)} descendants`); 121 | 122 | leaf.append('rect') 123 | .attr('id', d => (d.leafUid = DOM.uid('leaf')).id) 124 | .attr('fill', d => { while (d.depth > 1) d = d.parent; return color(d.data.name); }) 125 | .attr('fill-opacity', 0.6) 126 | .attr('width', d => d.x1 - d.x0) 127 | .attr('height', d => d.y1 - d.y0); 128 | 129 | leaf.append('clipPath') 130 | .attr('id', d => (d.clipUid = DOM.uid('clip')).id) 131 | .append('use') 132 | .attr('xlink:href', d => d.leafUid.href); 133 | 134 | leaf.append('foreignObject') 135 | .attr('clip-path', d => d.clipUid) 136 | .attr('width', d => d.x1 - d.x0) 137 | .attr('height', d => d.y1 - d.y0) 138 | .attr('fill-opacity', (d, i, nodes) => i === nodes.length - 1 ? 0.7 : null) 139 | .append('xhtml:p') 140 | .attr('style', d => `width: 100%; overflow: hidden; margin: 0; padding: min(1em, ${(d.y1 - d.y0) / 4}px); font-size: min(0.75rem, ${(d.y1 - d.y0) / 2}px); white-space: nowrap; text-overflow: ellipsis;`) 141 | .html(d => `${d.data.name}${(d.y1 - d.y0) > 20 ? '
' : ''}${(d.x1 - d.x0) > 60 ? `${format(d.value)} descendants` : ''}`); 142 | 143 | return svg.node(); 144 | }; 145 | const evalExpression = `(() => { ${constructData.toString()}; return constructData($0) })()`; 146 | 147 | if (typeof chrome !== 'undefined' && chrome.devtools) { 148 | chrome.devtools.inspectedWindow.eval( 149 | evalExpression, 150 | function (data, isException) { 151 | if (isException) { 152 | console.error(isException); 153 | return; 154 | } 155 | 156 | const svg = treemap(data); 157 | const oldSvg = document.getElementById('treemap').querySelector('svg'); 158 | 159 | document.getElementById('selected-element').textContent = data.name; 160 | document.getElementById('treemap')[oldSvg ? 'replaceChild' : 'appendChild'](svg, oldSvg); 161 | } 162 | ); 163 | } else { 164 | const data = constructData(document.body); 165 | const svg = treemap(data); 166 | const oldSvg = document.getElementById('treemap').querySelector('svg'); 167 | 168 | document.getElementById('selected-element').textContent = data.name; 169 | document.getElementById('treemap')[oldSvg ? 'replaceChild' : 'appendChild'](svg, oldSvg); 170 | } 171 | } 172 | 173 | updateTreeMap(); 174 | 175 | window.addEventListener('resize', () => { 176 | updateTreeMap(); 177 | }); 178 | 179 | document.addEventListener('click', (e) => { 180 | if (!e.target.closest('button')) { 181 | return; 182 | } 183 | 184 | const evalExpression = `if ($0 !== document.documentElement) inspect($0.parentElement)`; 185 | 186 | chrome.devtools.inspectedWindow.eval(evalExpression); 187 | }); 188 | 189 | if (typeof chrome !== 'undefined' && chrome.devtools) { 190 | chrome.devtools.panels.elements.onSelectionChanged.addListener(() => { 191 | updateTreeMap(); 192 | }); 193 | 194 | document.documentElement.classList.add(`-theme-with-${chrome.devtools.panels.themeName}-background`); 195 | } 196 | -------------------------------------------------------------------------------- /src/icon-list-up.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schepp/dom-treemap-devtools-extension/ae12343bf908f8413ceb2ce5935d5de3c710b1b1/src/icon128.png -------------------------------------------------------------------------------- /src/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schepp/dom-treemap-devtools-extension/ae12343bf908f8413ceb2ce5935d5de3c710b1b1/src/icon16.png -------------------------------------------------------------------------------- /src/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Schepp/dom-treemap-devtools-extension/ae12343bf908f8413ceb2ce5935d5de3c710b1b1/src/icon48.png -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "icons": { 4 | "16": "icon16.png", 5 | "48": "icon48.png", 6 | "128": "icon128.png" 7 | }, 8 | "name": "DOM Treemap", 9 | "version": "1.1.1", 10 | "description": "Helps you explore the distribution of DOM nodes in the document tree.", 11 | "devtools_page": "devtools.html", 12 | "permissions": [] 13 | } 14 | --------------------------------------------------------------------------------