-
Notifications
You must be signed in to change notification settings - Fork 9.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Audit: DOM stats (total nodes, depth, width) #1673
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Was the 1500 also found in one of those links? I couldn't find it. Maybe link to wherever he said that if not. Also would love to see your swaggy HTTP archive stats here :)
* @return {!Promise<!Array<!Object>>} | ||
*/ | ||
afterPass(options) { | ||
return options.driver.querySelectorAll('body, body /deep/ *'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yikes how big is this? Do we have plans to do anything with them or just use the length? Then again I've probably done far more burdensome things than this :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was in a comment I re-posted from an email thread:
#1096 (comment)
Would really love more hard data on the topic, but I do trust whatever Elliot says. I benchmarked a few big apps in the past and they tend to hover around ~1500-2000 nodes (GMail, maps, inbox, github).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Gmail is 4000-5000 elements (depending on the view, sometimes it's 15k on long email threads). This github review page is ~1500. My facebook timeline is 2300 when loaded. I got my numbers sampling a bunch of popular websites.
btw the /deep/ trick won't work with Shadow DOM v1 because I don't think deep will match in there. You'll need to write your own traversal that crawls the entire document. It's unfortunate we don't have any tooling for querySelectorAll with Shadow DOM v1 yet. @hayatoito
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's the crbug: https://bugs.chromium.org/p/chromium/issues/detail?id=633007#c16
Looks like >>>
in qSA
is behind the experimental web platform flag. We can probably move to something like https://gist.github.com/ebidel/fc1302d5fa1ef7c6d42fe8189acc3820 to cover all base.
DevTools also has https://chromedevtools.github.io/debugger-protocol-viewer/tot/DOM/#method-getFlattenedDocument, which might be even better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@patrickhulce we also have the same query here:
.then(_ => options.driver.querySelectorAll('body, body /deep/ *')) // drill into shadow trees |
We could get trickier and create a shared driver method that returns a promise and caches the result after the first call. Thoughts?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree with @patrickhulce that this may be a really big thing to bring back over and keep around just for the length. It doesn't look like the array contents would be particularly helpful for others audits, just an array of node IDs that would then each be wrapped by Elements, but we wouldn't be able to tell them apart in any way without more protocol requests for each one...
Particularly if you're going to have to get tricky with a selector for Shadow DOM v1, maybe switch to evaluateAsync
and ship back only the number of nodes?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could get trickier and create a shared driver method that returns a promise and caches the result after the first call. Thoughts?
We probably should measure, I guess :) Maybe it's not so bad.
Beyond perf, though, for this gatherer there doesn't seem to be any benefit to keeping the array around
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isn't Pavel adding a method specifically to stop us from doing that? :) Haha, if its just a few thousand node ids I'm not that concerned.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
DOM.getFlattenedDocument
? I did a quick dirty benchmark between the two and found querySelectorAll
to be faster:
Runs: 1337ms, 1033ms
console.time('took');
return options.driver.sendCommand('DOM.enable')
.then(_ => options.driver.sendCommand('DOM.getFlattenedDocument', {
depth: -1, pierce: true
}))
.then(result => {
const nodes = result.nodes.filter(node => node.nodeType === 1);
console.timeEnd('took');
return nodes;
});
Runs: 812ms, 425ms
console.time('took');
return options.driver.querySelectorAll('html, html /deep/ *')
.then(nodes => {
console.timeEnd('took');
return nodes;
});
IMO, DOM.getFlattenedDocument
is a bit clunky to use. You have to filter for element nodes, but the biggest drawback is that depth: -1, pierce: true
also traverses into iframes. We definitely don't want that.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I'm fine with it here, just making a comment on why sharing the code may not be a great reason to justify fetching the nodes instead of length (if the other path that's using it will start using one that doesn't need to go and fetch information for each of the items individually after this call finishes :) )
`~${NumDOMNodes.MAX_DOM_NODES} DOM nodes. The sweet spot is around 60 elements wide x ` + | ||
'32 elements deep. A large DOM can increase memory, cause longer ' + | ||
'[style calculations](https://developers.google.com/web/fundamentals/performance/rendering/reduce-the-scope-and-complexity-of-style-calculations), ' + | ||
'and produce costly [layout reflows](https://developers.google.com/speed/articles/reflow). [Learn more](hhttps://developers.google.com/web/fundamentals/performance/rendering/).', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
s/hhttps/https/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
I'm selfishly adding this audit to have more places to show httparchive data :) |
static audit(artifacts) { | ||
const nodes = artifacts.AllDOMNodes; | ||
|
||
const rawValue = nodes.length <= NumDOMNodes.MAX_DOM_NODES; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
it would be great to score these like we do the FMP/TTI/etc audits, so it isn't just "you passed this line and are now failing" and more like, "you are way out of the mainstream/what browsers can handle well, so you score a 25" or whatever.
It would also be nice to be able to quantify the harm done by the amount of DOM content, so it's less a specific number and more about how much harm you're doing yourself, but I don't know if that's possible...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Can you point me to that graphing site?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yeah, sorry, we put the parameters for the curve at the top of each of the audits that use them, e.g.
lighthouse/lighthouse-core/audits/first-meaningful-paint.js
Lines 24 to 27 in 3043cc3
// Parameters (in ms) for log-normal CDF scoring. To see the curve: | |
// https://www.desmos.com/calculator/joz3pqttdq | |
const SCORING_POINT_OF_DIMINISHING_RETURNS = 1600; | |
const SCORING_MEDIAN = 4000; |
you can edit the graph and then save it, which will give a new permalink for that particular graph to add as a comment. You have to create an account to save, but it can login via github :)
description: 'Uses a small number of DOM nodes', | ||
helpText: 'Browser engineers recommend pages contain fewer than ' + | ||
`~${NumDOMNodes.MAX_DOM_NODES} DOM nodes. The sweet spot is around 60 elements wide x ` + | ||
'32 elements deep. A large DOM can increase memory, cause longer ' + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we be looking at length of the longest paths, or distribution of path lengths in the DOM?
* @return {!Promise<!Array<!Object>>} | ||
*/ | ||
afterPass(options) { | ||
return options.driver.querySelectorAll('body, body /deep/ *'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
agree with @patrickhulce that this may be a really big thing to bring back over and keep around just for the length. It doesn't look like the array contents would be particularly helpful for others audits, just an array of node IDs that would then each be wrapped by Elements, but we wouldn't be able to tell them apart in any way without more protocol requests for each one...
Particularly if you're going to have to get tricky with a selector for Shadow DOM v1, maybe switch to evaluateAsync
and ship back only the number of nodes?
* @return {!Promise<!Array<!Object>>} | ||
*/ | ||
afterPass(options) { | ||
return options.driver.querySelectorAll('body, body /deep/ *'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could get trickier and create a shared driver method that returns a promise and caches the result after the first call. Thoughts?
We probably should measure, I guess :) Maybe it's not so bad.
Beyond perf, though, for this gatherer there doesn't seem to be any benefit to keeping the array around
Giving this a big time beef up. Stay tuned. |
PTAL. The audit UI has changed quite a bit, including a new formatter, and now being a scored audit. Take a look at the description. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is really cool, I dig the cards! just a few minor comments
@@ -0,0 +1,8 @@ | |||
<ul class="subitem__details cards__container"> | |||
{{#each this}} | |||
<div class="subitem__detail scorecard" {{#if snippet}}title="{{snippet}}"{{/if}}> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
if it will always be the title can we rename snippet to title?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reserved title
for the card title. Also wanted to make it obvious this was a code snippet
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh whoops, alright
} | ||
const className = element.className; | ||
if (className) { | ||
name += `.${className.split(' ').join('.')}`; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
maybe just use replace here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
path.push(createSelectorsLabel(node)); | ||
} | ||
} | ||
return path.reverse(); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't particularly mind which one, but any gotchas with unshift
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
that works. done.
* @return {!Promise<!Array<!Object>>} | ||
*/ | ||
afterPass(options) { | ||
return options.driver.querySelectorAll('body, body /deep/ *') |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
do we still need to do this if we're already calling evaluateAsync? could probably just return in getDOMStats
without shipping all the nodes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
good call. done.
@@ -149,6 +149,36 @@ module.exports = [ | |||
} | |||
} | |||
}, { | |||
initialUrl: 'http://localhost:10200/dobetterweb/domtester.html?smallDOM', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yay for moving out into some new testers! 🎉
const cards = [ | ||
{title: 'Total DOM Nodes', value: stats.totalDOMNodes.toLocaleString()}, | ||
{title: 'Max DOM Depth', value: stats.depth.max.toLocaleString(), snippet: depthSnippet}, | ||
{title: 'Max Children / Node', value: stats.width.max.toLocaleString(), snippet: widthSnippet} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Max Children / Node" doesn't read too well to me. Seems more like a rate of speed or something.
how about "Maximum Child Nodes"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"Maximum Children". Done
|
||
const cards = [ | ||
{title: 'Total DOM Nodes', value: stats.totalDOMNodes.toLocaleString()}, | ||
{title: 'Max DOM Depth', value: stats.depth.max.toLocaleString(), snippet: depthSnippet}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
while this is the maximum depth i guess its also just the total DOM depth. how about "DOM Depth"
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
done
* @return {!string} | ||
*/ | ||
/* istanbul ignore next */ | ||
function createSelectorsLabel(element) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we use this to show who is the parent of the most childNodes in the scorecard?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
PTAL |
R: all
Fixes #1096.
<details>
, but it didn't feel right. It's kinda nice to see the cards to break up our walls of text. More grey, but they don't have to be...?!Here's what she looks like now:
As seen in the screenshot, there's a
title
attribute for the last two cards that gives users a selector snippet on mouse over. It give context of the user's DOM tree.