-
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
feat: add OffscreenImages Audit #1807
Conversation
76f0e89
to
fe5470d
Compare
category: 'Images', | ||
name: 'unused-images', | ||
description: 'Unused images', | ||
helpText: 'Images that are not above the fold should be lazy loaded on interaction', |
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.
learn more link to https://developers.google.com/web/updates/2016/04/intersectionobserver?
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
|
||
const IGNORE_THRESHOLD_IN_BYTES = 2048; | ||
|
||
class UnusedImages extends Audit { |
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.
would "OffscreenImages" make more sense? Or "AboveTheFoldImages"
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 OffscreenImages
sounds good
} | ||
|
||
/** | ||
* @param {{top: number, right: number, bottom: number, left: number}} imageRect |
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: {ClientRect}
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.
will do
* @return {number} | ||
*/ | ||
static computeVisiblePixels(imageRect, viewportDimensions) { | ||
const scrollWidth = viewportDimensions.scrollWidth; |
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.
Not clientWidth/Height of the viewport? scrollWidth/Height includes off screen content.
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.
The existing naming of scroll
and viewport
for inner and outer somewhat confused me so I'm up for renaming to just inner and outer, but I think I do want inner*
(which is scroll here), right? Everything else gives me the size of the screen not what content is being displayed.
const url = URL.getDisplayName(image.src, {preserveQuery: true}); | ||
const totalPixels = image.clientWidth * image.clientHeight; | ||
const visiblePixels = this.computeVisiblePixels(image.clientRect, viewportDimensions); | ||
const wastedRatio = 1 - visiblePixels / totalPixels; |
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.
One thought about this audit is that it would be really nice to use IntersectionObserver
for finding the visible ratio of the image to the viewport. That API was designed for this use case. One problem with that approach is that the observer only fires if the image clientrect intersects with the viewports. So, for offscreen images, you'd never get a callback.
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.
True, I open to one that would do set subtraction but that seemed too specific to this audit to put in image usage and felt weird breaking out into a new one. It also meant the settings for failure (margin and so forth) were located in the gatherer rather than with the audit.
We have a spectrum of this throughout LH though, so I'm curious what other opinions are. I'm generally more in favor of keeping gatherers purely informational/more logic-less and having failure conditions measured in audits.
A different approach would be to use the trace and see what images we decoded. If we didn't decode an image yet, we probably didn't need to download it. This would be slightly more resilient in case images move location during load. Also, did you look at #821 ? How does this approach differ? |
Similar overall approach, mostly just updated to leverage the byte efficiency framework and image gatherer that's been added since then. Also adjustments to use innerHeight/Width instead. |
scrollWidth: window.innerWidth, | ||
scrollHeight: window.innerHeight, |
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.
Ah I see these now. Yea, I would switch these to use the same name as the property you're stashing. e.g. innerHeight: window.innerHeight
.
I typically use document.documentElement.clientWidth
to get the viewport width, but inner* works great. That's what you want.
@@ -40,8 +40,8 @@ class ContentWidth extends Audit { | |||
* @return {!AuditResult} | |||
*/ | |||
static audit(artifacts) { | |||
const scrollWidth = artifacts.ViewportDimensions.scrollWidth; | |||
const viewportWidth = artifacts.ViewportDimensions.viewportWidth; | |||
const scrollWidth = artifacts.ViewportDimensions.innerWidth; |
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.
rename var to innerWidth
too?
return 'The content scroll size is ' + artifact.scrollWidth + 'px, ' + | ||
'whereas the viewport size is ' + artifact.viewportWidth + 'px.'; | ||
return 'The content scroll size is ' + artifact.innerWidth + 'px, ' + | ||
'whereas the viewport size is ' + artifact.outerWidth + 'px.'; |
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.
Think this sentence needs updating. The first var is the viewport width and the second is
the size of the browser window. Right? Not sure you want outerWidth/Height
. Isn't that inclusive of the browser chrome? https://developer.mozilla.org/en-US/docs/Web/API/Window/outerWidth
For "content scroll size", don't you want scrollWidth
of the content?
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 on a plane so sorry for the short replies.
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.
No worries, and I was just going with the "if it ain't broke don't fix it" approach, but I agree I don't know what the intention is anymore :). In practice, outerWidth seems to reflect the non-scaled true width of the viewport and innerWidth seems to reflect the width of the content showndocumentElement.scrollWidth !== documentElement.clientWidth
makes the intention a lot more clear
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.
Sorry. I'm still caught up in lingo and intended behavior. Does http://jsbin.com/murusonore/edit?html,console,output capture what you're after?
document.documentElement.scrollWidth == content scroll size
window.innerWidth == visible viewport size
(In the bin example, both of these metrics are < window.outerWidth
because it's running inside the iframe...which is smaller than the viewport.)
Other refs:
innerWidth
: Width (in pixels) of the browser window viewport including, if rendered, the vertical scrollbar.
https://developer.mozilla.org/en-US/docs/Web/API/Window/innerWidth
outerWidth
gets the width of the outside of the browser window. It represents the width of the whole browser window including sidebar (if expanded), window chrome and window resizing borders/handles.
https://developer.mozilla.org/en-US/docs/Web/API/Window/outerWidth
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 not after anything other than to make the audit have more accurate names than it had before the rename :) I'm honestly not sure what this audit is getting after since we already have the viewport
audit checking for the most obvious condition under which this one will fail.
Proposal: rename scrollWidth
variable to viewportWidth
, rename viewportWidth
to windowWidth
and update the description accordingly. Do not change comparison of innerWidth to outerWidth.
Aside, my previous example of scrollWidth != clientWidth
is actually non-sensical I mistakenly assumed they meant something different on documentElement than regular elements.
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.
SGTM on those renames.
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.
Ok talked to Paul and found the motivation of this audit and it's just actually measuring something totally different. I'll clean that up in a future PR :) Will rename for now.
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. Who knows what the audit does, but at least the output text is matching what the code is doing :)
lol, I filed #1836 and linked it in your PSI meta issue |
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.
left two nits
More conceptual issues:
- does it make sense to punish for images that are partly on screen? Slicing them would not be a maintainable solution for developers, so I'm not sure how actionable the advice for those images will be
- "Images that are not above the fold should be lazy loaded on interaction" - should they? What's the downside vs e.g. loading below the fold images after TTI?
|
||
.then(returnedValue => { | ||
if (!Number.isFinite(returnedValue.scrollWidth) || | ||
!Number.isFinite(returnedValue.viewportWidth) || | ||
if (!Number.isFinite(returnedValue.innerWidth) || |
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.
time for something more succinct here? No Object.values
yet, but could do like
const allNumeric = Object.keys(returnedValue).every(key => Number.isFinite(returnedValue[key]));
if (!allNumeric) {
// ...
}
?
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 results; | ||
} | ||
|
||
// Don't warn about an image that was also used appropriately |
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.
what about something like, "if an image was used more than once, warn about the least wasteful usage of it"
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
That's mostly what the extra margin of error is for, but I'm down for only notifying only if a minimum percent offscreen is reached, maybe 75+?
Will reword, and we should probably check to see if that's already the case... |
@patrickhulce this needs a rebase. |
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.
👻🖼📐
looks good, only a few style nits (mostly around clearly marking units :)
'use strict'; | ||
|
||
const Audit = require('./byte-efficiency-audit'); | ||
const TTIAudit = require('../time-to-interactive'); |
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.
using audits in audits, my new arch nemesis :P
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 with ya, @paulirish not a fan of getting these values into computed artifact though 😩
}, new Map()); | ||
|
||
return TTIAudit.audit(artifacts).then(ttiResult => { | ||
const tti = ttiResult.extendedInfo.value.timestamps.timeToInteractive / 1000000; |
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.
ttiInSeconds
or something like that? Any reason you aren't just using ttiResult.rawValue
?
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.
raw value isn't what I want, I need the actual devtools timestamp value
const results = Array.from(resultsMap.values()).filter(item => { | ||
const isWasteful = item.wastedBytes > IGNORE_THRESHOLD_IN_BYTES && | ||
item.wastedPercent > IGNORE_THRESHOLD_IN_PERCENT; | ||
const loadedEarly = item.requestStartTime < tti; |
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 add an explicit comment here (or in description or on devsite? we'll have to doc it somewhere) that offscreen images loaded after TTI are considered fine
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
describe('OffscreenImages audit', () => { | ||
const _ttiAuditOriginal = TTIAudit.audit; | ||
before(() => { | ||
const timeToInteractive = 2000000; |
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.
timeToInteractiveInμs
? :) took me a second and some looking at the TTI audit itself to realize this wasn't just chosen to be a really high TTI so it would be longer than any trace we normally take
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
function generateRecord(resourceSizeInKb, startTime = 0, mimeType = 'image/png') { | ||
return { | ||
mimeType, | ||
startTime, |
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.
mention this is in seconds?
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
}); | ||
}); | ||
|
||
it('disregards lazily loaded images', () => { |
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 explicit 'disregards images loaded after TTI'
?
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
Hmm, do we need to bring in something to slow down the byte efficiency tester page? Or induce more screenshots with a spinner? Now that TTI is needed for it (through the offscreen images audit) it looks like that page is flakey now when no screenshots are captured. ✘ difference at offscreen-images.score: found null, expected false
full found result: {
"score": null,
"rawValue": null,
"error": true,
"debugString": "Audit error: No screenshots found in trace",
"name": "offscreen-images",
"description": "Offscreen images"
} |
It's already being slowed to 5s as part of this PR and not sure why it's flaking on travis, would love to get a reproducible failure case to have more ammo against that bug though. 6/6 smoke test successes locally as I wrote this UPDATE: now 12/12 :) |
ok, so travis is happy on a few reruns, so lets ship it. Not sure how to restart the appveyor builds |
Continues the byte efficiency audits by finding unused images (images that are mostly or entirely outside the viewport). Broken into 2 commits, 1 that renames ContentWidth to ViewportDimensions to accommodate the addition of height and the other that adds the audit with tests.
Partially addresses above the fold content issue for PSI #909