Skip to content
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

core(lantern): FCP improvements #7513

Merged
merged 10 commits into from
Apr 9, 2019
Merged

Conversation

patrickhulce
Copy link
Collaborator

@patrickhulce patrickhulce commented Mar 14, 2019

Summary

While doing some investigation into resilient FCP, I noticed a few cases popping up where we could be more accurate.

Specifically:

  • Previously, if a network request was for a script, it finished before FCP, and it had a render blocking priority, we would include it in the FCP graph even if the EvaluateScript task for that script happened after FCP. This doesn't make sense so we now exclude such scripts. This in particular was a source of variance as sometimes FCP would happen to be before the script finished downloading and sometimes it wouldn't. Now such scripts will always be excluded.
  • Previously, we were only including EvaluateScript tasks for blocking scripts, no other CPU nodes were included. That frequently misses some pretty big/important tasks like parsing the HTML, the first layout, etc. We now include the first layout/paint/parse HTML task if they happened before FCP.

Impact

Huge boosts to our FCP accuracy!! 🎉 FMP takes a slight hit to be consistent here, but it doesn't move any sites to worse buckets and FCP has a much higher priority/visibility anyway.

This also shrunk the observed variance for the Hulu case from +/- 2000ms to +/-200ms, which is far more tolerable.

image

Copy link
Member

@brendankenny brendankenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a few clarifying questions, a few actually important :)

}

/**
* @param {Node} dependencyGraph
* @param {LH.Artifacts.TraceOfTab} traceOfTab
* @param {number} fcpTs
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

some docs would be good. Not at clear from the outside why this is called fcpTs but lantern-first-meaningful-paint.js is passing in fmp, for instance :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* @param {number} fcpTs
* @param {function(NetworkNode):boolean} networkFilter
* @param {function(CPUNode):boolean=} cpuFilter
* @return {{possiblyRenderBlockingScriptUrls: Set<string>, actuallyRenderBlockingScriptUrls: Set<string>, blockingCpuNodeIds: Set<string>}}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

actually reads like possibly blocking nodes really weren't. definitelyRenderBlockingScriptUrls? :)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha good point sounds good 👍

Copy link
Collaborator Author

@patrickhulce patrickhulce Mar 21, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

well, on further reflection definitely feels a tad too strong :)

we don't know with 100% certainty, but it's the delta between the two is definitelyNotRenderBlocking, maybe I just return that directly

* @param {Node} graph
* @param {number} fcpTs
* @param {function(NetworkNode):boolean} networkFilter
* @param {function(CPUNode):boolean=} cpuFilter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does {(function(CPUNode):boolean)=} work? = isn't supposed to be applicable to return types, but with tsc you never know, so it ends up hard to read

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done 👍

* @param {LH.Artifacts.TraceOfTab} traceOfTab
* @param {number} fcpTs
* @param {function(NetworkNode):boolean} blockingScriptFilter
* @param {function(CPUNode):boolean=} cpuNodeFilter
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cpuNodeFilter does the opposite of what I expected...maybe incorporate block/blocking into its name as well?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah this was a bad name, renamed both of them to extraBlockingCpuNodesToIncludeFilter

@paulirish
Copy link
Member

Previously, if a network request was for a script, it finished before FCP, and it had a render blocking priority, we would include it in the FCP graph even if the EvaluateScript task for that script happened after FCP. This doesn't make sense so we now exclude such scripts

Yeah this doesn't make sense at all. :) Like.... the script is render blocking so why would chrome do a real contentful paint before the evaluateScript??? That's not really supposed to happen. Can we look closer at what's going on here?

We now include the first layout/paint/parse HTML task if they happened before FCP.

that sounds awesome.

@patrickhulce
Copy link
Collaborator Author

patrickhulce commented Mar 21, 2019

Like.... the script is render blocking so why would chrome do a real contentful paint before the evaluateScript??? That's not really supposed to happen. Can we look closer at what's going on here?

Oh the thing is it wasn't really render-blocking. It just had a render-blocking priority. Scripts added to the head by other scripts for example aren't render-blocking but will be requested by Chrome with the same priority as render-blocking scripts. We exclude these types of scripts from the optimistic estimate but still include them in the pessimistic one since they're usually important and could have been the script to actually render the content, but aren't necessarily required.

This PR now uses the extra bit of information about when the EvaluateScript finished to eliminate the ones that definitely weren't required for paint.

Copy link
Member

@brendankenny brendankenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, I like the cleanup!

One big question and a few documentation comments

const firstPaint = cpuNodes.find(node => node.childEvents.some(e => e.name === 'Paint'));
if (firstPaint) blockingCpuNodeIds.add(firstPaint.id);
const firstParse = cpuNodes.find(node => node.childEvents.some(e => e.name === 'ParseHTML'));
if (firstParse) blockingCpuNodeIds.add(firstParse.id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are the FCP/FMP results weird (and not worth keeping) if any of these are missing?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

theoretically yes, but in practice I've seen some of these be shifted slightly so I wouldn't want to throw it all away

maybe another candidate for #7041 ?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe another candidate for #7041 ?

yeah, probably the best thing before we start throwing on it

return node.hasRenderBlockingPriority();
const url = node.record.url;
// If the URL definitely wasn't render-blocking then we filter it out.
if (definitelyNotRenderBlockingScriptUrls.has(url)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

definitelyNotRenderBlockingScriptUrls is a little confusing inside of getBlockingCpuData, but really nice here!

Copy link
Member

@brendankenny brendankenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!


// A script is *definitely not* render blocking if its EvaluateScript task started after filterTimestamp.
// We'll check `cpuNodes` to see if we can find its EvaluateScript.
// If we can't, that means it started after filterTimestamp, and we add it to our set.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah, this is great

@brendankenny
Copy link
Member

@paulirish not sure if you want to take another look before we land

Copy link
Member

@paulirish paulirish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the comments are crazy valuable, especially for this kind of thing.
thanks.

lg here.

@brendankenny
Copy link
Member

looks like need a new golden lhr and render-blocking-resources expectations need adjusting

@patrickhulce patrickhulce force-pushed the lantern_fcp_exclude_late_scripts branch from dea2ef5 to 227e683 Compare March 26, 2019 01:14
@patrickhulce
Copy link
Collaborator Author

sorry y'all the fix for rendering blocking smoke is slightly more involved but I think it makes the definitelyNotRenderBlocking computation a little clearer too in the process.

just one more quick look? :D

the good news is we keep goin' up accuracy-wise! 😃

this is the incremental progress on what we already had 💣 💥 🥇

image

Copy link
Member

@brendankenny brendankenny left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

still LGTM :)

I think it makes the definitelyNotRenderBlocking computation a little clearer too in the process.

agreed!

lighthouse-cli/test/fixtures/dobetterweb/dbw_tester.html Outdated Show resolved Hide resolved
extraBlockingCpuNodesToIncludeFilter
) {
/** @type {Map<string, CPUNode>} */
const scriptUrlToNodeMap = new Map();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe a comment about it being a map from script URL to the earliest associated EvaluateScript CPUNode?

const firstPaint = cpuNodes.find(node => node.childEvents.some(e => e.name === 'Paint'));
if (firstPaint) blockingCpuNodeIds.add(firstPaint.id);
const firstParse = cpuNodes.find(node => node.childEvents.some(e => e.name === 'ParseHTML'));
if (firstParse) blockingCpuNodeIds.add(firstParse.id);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe another candidate for #7041 ?

yeah, probably the best thing before we start throwing on it

Copy link
Member

@paulirish paulirish left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

works for me. excited about this.

@paulirish
Copy link
Member

We discussed shipping this in v5 (#7752), so we may wait to land it.

@brendankenny
Copy link
Member

I think we're good to go here now, right?

@patrickhulce
Copy link
Collaborator Author

We are! let's do it!

@brendankenny brendankenny merged commit c71853f into master Apr 9, 2019
@brendankenny brendankenny deleted the lantern_fcp_exclude_late_scripts branch April 9, 2019 18:19
@paulirish paulirish mentioned this pull request Apr 30, 2019
67 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants