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

feat: add OffscreenImages Audit #1807

Merged
merged 15 commits into from
Mar 23, 2017
Merged

feat: add OffscreenImages Audit #1807

merged 15 commits into from
Mar 23, 2017

Conversation

patrickhulce
Copy link
Collaborator

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

category: 'Images',
name: 'unused-images',
description: 'Unused images',
helpText: 'Images that are not above the fold should be lazy loaded on interaction',
Copy link
Contributor

Choose a reason for hiding this comment

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

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


const IGNORE_THRESHOLD_IN_BYTES = 2048;

class UnusedImages extends Audit {
Copy link
Contributor

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"

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 OffscreenImages sounds good

}

/**
* @param {{top: number, right: number, bottom: number, left: number}} imageRect
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: {ClientRect}

Copy link
Collaborator Author

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;
Copy link
Contributor

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.

Copy link
Collaborator Author

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;
Copy link
Contributor

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.

Copy link
Collaborator Author

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.

@paulirish
Copy link
Member

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?

@patrickhulce
Copy link
Collaborator Author

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.

@patrickhulce patrickhulce changed the title feat: add UnusedImage Audit feat: add OffscreenImages Audit Mar 3, 2017
scrollWidth: window.innerWidth,
scrollHeight: window.innerHeight,
Copy link
Contributor

@ebidel ebidel Mar 8, 2017

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;
Copy link
Contributor

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.';
Copy link
Contributor

@ebidel ebidel Mar 8, 2017

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?

Copy link
Contributor

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.

Copy link
Collaborator Author

@patrickhulce patrickhulce Mar 8, 2017

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 documentElement.scrollWidth !== documentElement.clientWidth makes the intention a lot more clear 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 shown

Copy link
Contributor

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

Copy link
Collaborator Author

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.

Copy link
Contributor

@ebidel ebidel Mar 8, 2017

Choose a reason for hiding this comment

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

SGTM on those renames.

Copy link
Collaborator Author

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.

Copy link
Contributor

@ebidel ebidel left a 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 :)

@patrickhulce
Copy link
Collaborator Author

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

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.

left two nits

More conceptual issues:

  1. 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
  2. "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) ||
Copy link
Member

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) {
  // ...
}

?

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

return results;
}

// Don't warn about an image that was also used appropriately
Copy link
Member

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"

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

@patrickhulce
Copy link
Collaborator Author

patrickhulce commented Mar 9, 2017

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

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+?

"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?

Will reword, and we should probably check to see if that's already the case...

@ebidel
Copy link
Contributor

ebidel commented Mar 22, 2017

@patrickhulce this needs a rebase.

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.

👻🖼📐

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');
Copy link
Member

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

Copy link
Collaborator Author

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;
Copy link
Member

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?

Copy link
Collaborator Author

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;
Copy link
Member

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

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

describe('OffscreenImages audit', () => {
const _ttiAuditOriginal = TTIAudit.audit;
before(() => {
const timeToInteractive = 2000000;
Copy link
Member

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

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

function generateRecord(resourceSizeInKb, startTime = 0, mimeType = 'image/png') {
return {
mimeType,
startTime,
Copy link
Member

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?

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

});
});

it('disregards lazily loaded images', () => {
Copy link
Member

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'?

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

@brendankenny
Copy link
Member

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"
}

@patrickhulce
Copy link
Collaborator Author

patrickhulce commented Mar 23, 2017

Hmm, do we need to bring in something to slow down the byte efficiency tester page? Or induce more screenshots with a spinner?

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 :)

@brendankenny
Copy link
Member

ok, so travis is happy on a few reruns, so lets ship it. Not sure how to restart the appveyor builds

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.

None yet

4 participants