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

new_audit: lcp-lazy-loaded #12838

Merged
merged 18 commits into from
Aug 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lighthouse-cli/test/cli/__snapshots__/index-test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,9 @@ Object {
Object {
"path": "largest-contentful-paint-element",
},
Object {
"path": "lcp-lazy-loaded",
},
Object {
"path": "layout-shift-elements",
},
Expand Down Expand Up @@ -1092,6 +1095,11 @@ Object {
"id": "largest-contentful-paint-element",
"weight": 0,
},
Object {
"group": "diagnostics",
"id": "lcp-lazy-loaded",
"weight": 0,
},
Object {
"group": "diagnostics",
"id": "layout-shift-elements",
Expand Down
1 change: 1 addition & 0 deletions lighthouse-cli/test/fixtures/perf/delayed-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ function stall(ms) {
setTimeout(() => {
const imgEl = document.createElement('img');
imgEl.src = '../dobetterweb/lighthouse-480x318.jpg';
imgEl.loading = 'lazy';
const textEl = document.createElement('div');
textEl.textContent = 'Sorry!';
textEl.style.height = '18px' // this height can be flaky so we set it manually
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ const traceElements = {
traceEventType: 'largest-contentful-paint',
node: {
nodeLabel: 'section > img',
snippet: '<img src="../dobetterweb/lighthouse-480x318.jpg">',
snippet: '<img src="../dobetterweb/lighthouse-480x318.jpg" loading="lazy">',
boundingRect: {
top: 108,
bottom: 426,
Expand Down Expand Up @@ -295,6 +295,19 @@ const traceElements = {
],
},
},
'lcp-lazy-loaded': {
score: 0,
details: {
items: [
{
node: {
type: 'node',
nodeLabel: 'section > img',
},
},
],
},
},
'layout-shift-elements': {
score: null,
displayValue: '2 elements found',
Expand Down
83 changes: 83 additions & 0 deletions lighthouse-core/audits/lcp-lazy-loaded.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/**
* @license Copyright 2021 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

const Audit = require('./audit.js');
const i18n = require('../lib/i18n/i18n.js');

const UIStrings = {
/** Title of a Lighthouse audit that provides detail on whether the largest above-the-fold image was loaded with sufficient priority. This descriptive title is shown to users when the image was loaded properly. */
title: 'Largest Contentful Paint image was not lazily loaded',
/** Title of a Lighthouse audit that provides detail on whether the largest above-the-fold image was loaded with sufficient priority. This descriptive title is shown to users when the image was loaded inefficiently using the `loading=lazy` attribute. */
failureTitle: 'Largest Contentful Paint image was lazily loaded',
/** Description of a Lighthouse audit that tells the user why the advice is important. This is displayed after a user expands the section to see more. No character length limits. */
description: 'Above-the-fold images that are lazily loaded render later in the page lifecycle, which can delay the largest contentful paint. [Learn more](https://web.dev/lcp-lazy-loading/).',
};

const str_ = i18n.createMessageInstanceIdFn(__filename, UIStrings);

class LargestContentfulPaintLazyLoaded extends Audit {
/**
* @return {LH.Audit.Meta}
*/
static get meta() {
return {
id: 'lcp-lazy-loaded',
title: str_(UIStrings.title),
description: str_(UIStrings.description),
supportedModes: ['navigation'],
requiredArtifacts: ['TraceElements', 'ViewportDimensions', 'ImageElements'],
};
}

/**
* @param {LH.Artifacts.ImageElement} image
* @param {LH.Artifacts.ViewportDimensions} viewportDimensions
* @return {boolean}
*/
static isImageInViewport(image, viewportDimensions) {
const imageTop = image.clientRect.top;
const viewportHeight = viewportDimensions.innerHeight;
return imageTop < viewportHeight;
}

/**
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.Product}
*/
static audit(artifacts) {
const lcpElement = artifacts.TraceElements
.find(element => element.traceEventType === 'largest-contentful-paint');
const lcpElementImage = lcpElement ? artifacts.ImageElements.find(elem => {
return elem.node.devtoolsNodePath === lcpElement.node.devtoolsNodePath;
}) : undefined;


if (!lcpElementImage ||
!this.isImageInViewport(lcpElementImage, artifacts.ViewportDimensions)) {
return {score: 1, notApplicable: true};
}

/** @type {LH.Audit.Details.Table['headings']} */
const headings = [
{key: 'node', itemType: 'node', text: str_(i18n.UIStrings.columnElement)},
];

const details = Audit.makeTableDetails(headings, [
{
node: Audit.makeNodeItem(lcpElementImage.node),
},
]);

return {
score: lcpElementImage.loading === 'lazy' ? 0 : 1,
details,
};
}
}

module.exports = LargestContentfulPaintLazyLoaded;
module.exports.UIStrings = UIStrings;
2 changes: 2 additions & 0 deletions lighthouse-core/config/default-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ const defaultConfig = {
'third-party-summary',
'third-party-facades',
'largest-contentful-paint-element',
'lcp-lazy-loaded',
'layout-shift-elements',
'long-tasks',
'no-unload-listeners',
Expand Down Expand Up @@ -466,6 +467,7 @@ const defaultConfig = {
{id: 'third-party-summary', weight: 0, group: 'diagnostics'},
{id: 'third-party-facades', weight: 0, group: 'diagnostics'},
{id: 'largest-contentful-paint-element', weight: 0, group: 'diagnostics'},
{id: 'lcp-lazy-loaded', weight: 0, group: 'diagnostics'},
{id: 'layout-shift-elements', weight: 0, group: 'diagnostics'},
{id: 'uses-passive-event-listeners', weight: 0, group: 'diagnostics'},
{id: 'no-document-write', weight: 0, group: 'diagnostics'},
Expand Down
9 changes: 9 additions & 0 deletions lighthouse-core/lib/i18n/locales/en-US.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions lighthouse-core/lib/i18n/locales/en-XL.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

102 changes: 102 additions & 0 deletions lighthouse-core/test/audits/lcp-lazy-loaded-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* @license Copyright 2020 The Lighthouse Authors. All Rights Reserved.
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
*/
'use strict';

const LargestContentfulPaintLazyLoaded =
require('../../audits/lcp-lazy-loaded.js');

/* eslint-env jest */
const SAMPLE_NODE = {
devtoolsNodePath: '1,HTML,1,BODY,3,DIV,2,IMG',
selector: 'div.l-header > div.chorus-emc__content',
nodeLabel: 'My Test Label',
snippet: '<img class="test-class">',
};
function generateImage(loading, clientRectTop) {
return {
src: 'test',
loading,
clientRect: {
top: clientRectTop,
bottom: 400,
left: 0,
right: 0,
},
node: SAMPLE_NODE,
};
}
describe('Performance: lcp-lazy-loaded audit', () => {
it('correctly surfaces the lazy loaded LCP element', async () => {
milutin marked this conversation as resolved.
Show resolved Hide resolved
const artifacts = {
TraceElements: [{
traceEventType: 'largest-contentful-paint',
node: SAMPLE_NODE,
}],
ImageElements: [
generateImage('lazy', 0),
],
ViewportDimensions: {
innerHeight: 500,
innerWidth: 300,
},
};

const auditResult = await LargestContentfulPaintLazyLoaded.audit(artifacts);
expect(auditResult.score).toEqual(0);
expect(auditResult.details.items).toHaveLength(1);
expect(auditResult.details.items[0].node.path).toEqual('1,HTML,1,BODY,3,DIV,2,IMG');
expect(auditResult.details.items[0].node.nodeLabel).toEqual('My Test Label');
expect(auditResult.details.items[0].node.snippet).toEqual('<img class="test-class">');
});

it('eager LCP element scores 1', async () => {
const artifacts = {
TraceElements: [{
traceEventType: 'largest-contentful-paint',
node: SAMPLE_NODE,
}],
ImageElements: [
generateImage('eager', 0),
],
ViewportDimensions: {
innerHeight: 500,
innerWidth: 300,
},
};
const auditResult = await LargestContentfulPaintLazyLoaded.audit(artifacts);
expect(auditResult.score).toEqual(1);
expect(auditResult.details.items).toHaveLength(1);
});

it('not applicable when outside of viewport', async () => {
const artifacts = {
TraceElements: [{
traceEventType: 'largest-contentful-paint',
node: SAMPLE_NODE,
}],
ImageElements: [
generateImage('lazy', 700),
],
ViewportDimensions: {
innerHeight: 500,
innerWidth: 300,
},
};
const auditResult = await LargestContentfulPaintLazyLoaded.audit(artifacts);
expect(auditResult.notApplicable).toEqual(true);
});

it('doesn\'t throw an error when there is nothing to show', async () => {
const artifacts = {
TraceElements: [],
ImageElements: [],
};

const auditResult = await LargestContentfulPaintLazyLoaded.audit(artifacts);
expect(auditResult.score).toEqual(1);
expect(auditResult.notApplicable).toEqual(true);
});
});
2 changes: 1 addition & 1 deletion lighthouse-core/test/fraggle-rock/api-test-pptr.js
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ describe('Fraggle Rock API', () => {
const {lhr} = result;
const {auditResults, failedAudits, erroredAudits} = getAuditsBreakdown(lhr);
// TODO(FR-COMPAT): This assertion can be removed when full compatibility is reached.
expect(auditResults.length).toMatchInlineSnapshot(`153`);
expect(auditResults.length).toMatchInlineSnapshot(`154`);
expect(erroredAudits).toHaveLength(0);

const failedAuditIds = failedAudits.map(audit => audit.id);
Expand Down
55 changes: 55 additions & 0 deletions lighthouse-core/test/results/sample_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -1859,6 +1859,43 @@
]
}
},
"lcp-lazy-loaded": {
"id": "lcp-lazy-loaded",
"title": "Largest Contentful Paint image was not lazily loaded",
"description": "Above-the-fold images that are lazily loaded render later in the page lifecycle, which can delay the largest contentful paint. [Learn more](https://web.dev/lcp-lazy-loading/).",
"score": 1,
"scoreDisplayMode": "binary",
"details": {
"type": "table",
"headings": [
{
"key": "node",
"itemType": "node",
"text": "Element"
}
],
"items": [
{
"node": {
"type": "node",
"lhId": "page-1-IMG",
"path": "3,HTML,1,BODY,14,IMG",
"selector": "body > img",
"boundingRect": {
"top": 574,
"bottom": 654,
"left": 132,
"right": 252,
"width": 120,
"height": 80
},
"snippet": "<img src=\"lighthouse-480x318.jpg?iar2\" width=\"120\" height=\"80\">",
"nodeLabel": "img"
}
}
]
}
},
"layout-shift-elements": {
"id": "layout-shift-elements",
"title": "Avoid large layout shifts",
Expand Down Expand Up @@ -5188,6 +5225,11 @@
"weight": 0,
"group": "diagnostics"
},
{
"id": "lcp-lazy-loaded",
"weight": 0,
"group": "diagnostics"
},
{
"id": "layout-shift-elements",
"weight": 0,
Expand Down Expand Up @@ -6352,6 +6394,12 @@
"duration": 100,
"entryType": "measure"
},
{
"startTime": 0,
"name": "lh:audit:lcp-lazy-loaded",
"duration": 100,
"entryType": "measure"
},
{
"startTime": 0,
"name": "lh:audit:layout-shift-elements",
Expand Down Expand Up @@ -7687,10 +7735,17 @@
],
"lighthouse-core/lib/i18n/i18n.js | columnElement": [
"audits[largest-contentful-paint-element].details.headings[0].text",
"audits[lcp-lazy-loaded].details.headings[0].text",
"audits[layout-shift-elements].details.headings[0].text",
"audits[non-composited-animations].details.headings[0].text",
"audits[dom-size].details.headings[1].text"
],
"lighthouse-core/audits/lcp-lazy-loaded.js | title": [
"audits[lcp-lazy-loaded].title"
],
"lighthouse-core/audits/lcp-lazy-loaded.js | description": [
"audits[lcp-lazy-loaded].description"
],
"lighthouse-core/audits/layout-shift-elements.js | title": [
"audits[layout-shift-elements].title"
],
Expand Down
Loading