-
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
core: add cap to amp stylesheet links for simulated throttling #11069
Conversation
@@ -54,6 +55,12 @@ function getNodesAndTimingByUrl(nodeTimings) { | |||
const nodeTiming = nodeTimings.get(node); | |||
if (!nodeTiming) return; | |||
|
|||
if (usesAMP && node.record.resourceType === 'Stylesheet' |
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'll definitely need a beefy comment here on why this is fair and not totally cheating/playing nice for AMP.
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.
yes, a big beefy comment with a link to our invitation for any stack owner with similar logic to do this would be awesome 👍
it'd also be great to link to the amp source (it should be in that issue) so that later we can double check that the behavior still exists.
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.
Thanks @adamraine this is a great start!
To @connorjclark 's point, it would also be nice if the override logic were extracted to computeStackSpecificTiming
functions where the AMP-specific logic (and potential future others) could live rather than a boolean parameter intermingled in the core code. For one it makes it clear where future stacks would be added, and for another it marks the stack-specific logic as clear extra optional niceness instead of a core part of the logic of this audit.
To that end, I don't think we're going to want the nodeTiming mutation done inside getNodesAndTimingByUrl
if we're going to be capping the overall savings as well.
There are two parts to cap.
wastedMs
per stylesheetoverallWastedMs
Mutating the object that's supposed to reflect the true timings makes this need slightly less clear and doesn't fully solve the second part of the problem. Assuming we extract two computeStackSpecificTiming
functions for the two problems, we can the override just the "wasted" variables instead of the simulated request data. How does that sound?
Unit tests for this would also be great :)
@@ -54,6 +55,12 @@ function getNodesAndTimingByUrl(nodeTimings) { | |||
const nodeTiming = nodeTimings.get(node); | |||
if (!nodeTiming) return; | |||
|
|||
if (usesAMP && node.record.resourceType === 'Stylesheet' |
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.
yes, a big beefy comment with a link to our invitation for any stack owner with similar logic to do this would be awesome 👍
it'd also be great to link to the amp source (it should be in that issue) so that later we can double check that the behavior still exists.
@@ -95,7 +103,7 @@ class RenderBlockingResources extends Audit { | |||
const fcpSimulation = await FirstContentfulPaint.request(metricComputationData, context); | |||
const fcpTsInMs = traceOfTab.timestamps.firstContentfulPaint / 1000; | |||
|
|||
const nodesByUrl = getNodesAndTimingByUrl(fcpSimulation.optimisticEstimate.nodeTimings); | |||
const nodesByUrl = getNodesAndTimingByUrl(fcpSimulation.optimisticEstimate.nodeTimings, usesAMP); |
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 think this is the layer at which we're going to want to cap the timing, see more in my overall review comment
* @param {Node} node | ||
* @param {LH.Gatherer.Simulation.NodeTiming} nodeTiming | ||
* @param {LH.Artifacts.DetectedStack[]} Stacks | ||
*/ | ||
function computeStackSpecificTiming(node, nodeTiming, Stacks) { | ||
let stackSpecificTiming = Object.assign({}, nodeTiming); | ||
if(!Stacks) return stackSpecificTiming; | ||
const stackSpecificTiming = Object.assign({}, nodeTiming); |
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.
nit: prefer object spreading.
const stackSpecificTiming = Object.assign({}, nodeTiming); | |
const stackSpecificTiming = {...nodeTiming}; |
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.
nice @adamraine we're getting there 👍
friendly reminder that adding some early unit tests will be your friend here and help catch potential bugs! :)
const originalEstimate = simulator.simulate(fcpGraph).timeInMs; | ||
static estimateSavingsWithGraphs(simulator, fcpGraph, deferredIds, wastedCssBytesByUrl, Stacks) { | ||
const originalEstimate = simulator.simulate(fcpGraph, {label: 'rbr-estimate'}).timeInMs; | ||
const originalNodeTimings = Simulator.ALL_NODE_TIMINGS.get('rbr-estimate'); |
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.
very clever use of the API surface! :)
I don't think we've ever used the label
to do anything more than output debug information before though, and I'm not sure it's worth doing it this time around when simulate
already returns the nodeTimings
const nodeTiming = originalNodeTimings.get(node); | ||
if (nodeTiming) { | ||
const stackSpecificTiming = computeStackSpecificTiming(node, nodeTiming, Stacks); | ||
stackSpecificEstimate -= nodeTiming.duration - stackSpecificTiming.duration; |
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'm trying to wrap my head around how this works, so let me see if I follow what you're trying to do here.
- we lookup the node timing in the "before" version of the graph
- compute its stack-specific timing
- take the difference from the non-stack-specific timing and subtract it from the overall "before" estimate
- this lowers the effective "before" estimate to better reflect the fact that we might be overestimating it
right?
This is on the right track but I think it will run into issues with parallel requests.
Consider the page dependency graph
|-index.html 1000ms
|- style-1.css 2000ms
|- style-2.css 2000ms
|- style-3.css 2000ms
In this case we'll subtract 3 full seconds off the before time which was only 3s to begin with. A fully correct solution in this style would require threading the duration changes all the way down through core lantern, but that's a bit more involved than I think we need here :)
Instead, you could adjust all of the nodeTimings
coming from simulate
and recompute const originalEstimate
as the Math.max(...Array.from(adjustedNodeTimings.map(timing => timing.endTime)))
, you will also have to adjust the endTime of any dependent requests of the stylesheet (see line 137 for an example of how to find dependent nodes)
const stackSpecificTiming = Object.assign({}, nodeTiming); | ||
if (!Stacks) return stackSpecificTiming; | ||
if (Stacks.some(stack => stack.id === 'amp')) { | ||
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second [https://github.com/ampproject/amphtml/blob/master/src/font-stylesheet-timeout.js] |
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.
protip: press y
while on a github blob to turn the shortcut ref into a permalink of the current hash, this way if amp ever renames its master branch to main or moves the file future readers will still be able to find what you were talking about :)
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second [https://github.com/ampproject/amphtml/blob/master/src/font-stylesheet-timeout.js] | |
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second | |
// https://github.com/ampproject/amphtml/blob/8e03ac2f315774070651584a7e046ff24212c9b1/src/font-stylesheet-timeout.js#L54-L59 |
const stackSpecificTiming = Object.assign({}, nodeTiming); | ||
if (!Stacks) return stackSpecificTiming; | ||
if (Stacks.some(stack => stack.id === 'amp')) { | ||
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second [https://github.com/ampproject/amphtml/blob/master/src/font-stylesheet-timeout.js] |
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.
based on their latest master it actually looks like they cap the stylesheet end load time to 2.1s now instead of the stylesheet load time to 1s which is pretty neat, we'll need to adjust our logic accordingly
if (Stacks.some(stack => stack.id === 'amp')) { | ||
// AMP will load a linked stylesheet asynchronously if it has not been loaded after 1 second [https://github.com/ampproject/amphtml/blob/master/src/font-stylesheet-timeout.js] | ||
// Any potential savings for AMP stylesheet nodes must therefore be capped at 1 second. | ||
// https://github.com/GoogleChrome/lighthouse/issues/2832#issuecomment-591066081 |
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.
let's move this comment line to the jsdoc description of the function explaining this is where any stack-specific overrides should happen
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.
spoke in our video call, but going back to the original issue I think we have to do a little more around dependent requests.
I added an example trace from an AMP page we should try to test against :)
@@ -60,6 +60,56 @@ function getNodesAndTimingByUrl(nodeTimings) { | |||
return urlMap; | |||
} | |||
|
|||
/** | |||
* Adjust the timing of a node and it's dependencies to account for stack specific overrides. |
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.
* Adjust the timing of a node and it's dependencies to account for stack specific overrides. | |
* Adjust the timing of a node and its dependencies to account for stack specific overrides. |
sorry if that was been copied from one of my comments 😆
|
||
/** | ||
* Any stack specific timing overrides should go in this function. | ||
* https://github.com/GoogleChrome/lighthouse/issues/2832#issuecomment-591066081 |
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.
* https://github.com/GoogleChrome/lighthouse/issues/2832#issuecomment-591066081 | |
* @see https://github.com/GoogleChrome/lighthouse/issues/2832#issuecomment-591066081 |
@@ -170,13 +228,18 @@ class RenderBlockingResources extends Audit { | |||
return !canDeferRequest; | |||
})); | |||
|
|||
// Recalculate the "before" time based on our adjusted node timings. | |||
const stackSpecificEstimate = Math.max(...Array.from( |
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.
Keeping with the global name change WDYT about...
const stackSpecificEstimate = Math.max(...Array.from( | |
const estimateBeforeInline = Math.max(...Array.from( |
// Saving 1000 + 1000 + 100ms for TCP handshake + 1 RT savings + server response time | ||
assert.equal(result, 2100); | ||
}); | ||
|
||
it('computes savings for stylesheets deferred by AMP', () => { |
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('computes savings for stylesheets deferred by AMP', () => { | |
it('does not report savings from AMP-stack when document already exceeds 2.1s', () => { |
All (the pull request submitter and all commit authors) CLAs are signed, but one or more commits were authored or co-authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that by leaving a comment that contains only Note to project maintainer: There may be cases where the author cannot leave a comment, or the comment is not properly detected as consent. In those cases, you can manually confirm consent of the commit author(s), and set the ℹ️ Googlers: Go here for more info. |
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.
LGTM 🎉
It's worth continued noting in the issue that this audit will still produce results because AMP's method does still use render-blocking requests. It just limits the impact they have to ~2.1s. Since most the comments in that issue are complaining about results that are less than this threshold, I wouldn't expect many of them to be satisfied but at least we know our results are technically sound now :)
Before | After |
---|---|
const difference = nodeTiming.duration - stackSpecificTiming.duration; | ||
if (!difference) return; | ||
|
||
// Adjust timing of all dependent 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.
// Adjust timing of all dependent nodes | |
// AMP's method of removal of stylesheets effectively removes all dependent nodes from the FCP graph | |
// Ensure all dependent nodes are not affecting our estimated savings either. |
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.
just to make it clear this is still AMP specific if we ever add more
node.traverse(node => { | ||
adjustedNodeTimings.delete(node); |
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.
nit: shadowing can be confusing to those who are trying to understand it for the first time :)
node.traverse(node => { | |
adjustedNodeTimings.delete(node); | |
node.traverse(childNode => { | |
adjustedNodeTimings.delete(childNode); |
@googlebot I consent. |
1 similar comment
@googlebot I consent. |
CLAs look good, thanks! ℹ️ Googlers: Go here for more info. |
Summary
Since AMP defers loading external stylesheets if they take more than 1s we need to cap the duration in render blocking resources at 1s. The cap has already been applied for active throttling and this PR will do the same for simulated throttling.
@patrickhulce we currently just cap any stylesheet when AMP is loaded. Let me know if that is too broad.
Related Issues/PRs
#2832