Skip to content
Open
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
18 changes: 13 additions & 5 deletions core/audits/lcp-lazy-loaded.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

import {Audit} from './audit.js';
import * as i18n from '../lib/i18n/i18n.js';
import ImageRecords from '../computed/image-records.js';
import NetworkRecords from '../computed/network-records.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. */
Expand All @@ -30,7 +32,7 @@ class LargestContentfulPaintLazyLoaded extends Audit {
failureTitle: str_(UIStrings.failureTitle),
description: str_(UIStrings.description),
supportedModes: ['navigation'],
requiredArtifacts: ['TraceElements', 'ViewportDimensions', 'ImageElements'],
requiredArtifacts: ['TraceElements', 'ViewportDimensions', 'ImageElements', 'devtoolsLogs'],
};
}

Expand All @@ -47,16 +49,22 @@ class LargestContentfulPaintLazyLoaded extends Audit {

/**
* @param {LH.Artifacts} artifacts
* @return {LH.Audit.Product}
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*/
static audit(artifacts) {
static async audit(artifacts, context) {
const devtoolsLog = artifacts.devtoolsLogs[LargestContentfulPaintLazyLoaded.DEFAULT_PASS];
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
const images = await ImageRecords.request({
ImageElements: artifacts.ImageElements,
networkRecords,
}, context);
const lcpElement = artifacts.TraceElements
.find(element => element.traceEventType === 'largest-contentful-paint');
const lcpElementImage = lcpElement ? artifacts.ImageElements.find(elem => {
const lcpElementImage = lcpElement ? images.find(elem => {
return elem.node.devtoolsNodePath === lcpElement.node.devtoolsNodePath;
}) : undefined;


if (!lcpElementImage ||
!this.isImageInViewport(lcpElementImage, artifacts.ViewportDimensions)) {
return {score: 1, notApplicable: true};
Expand Down
9 changes: 8 additions & 1 deletion core/audits/preload-lcp-image.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import MainResource from '../computed/main-resource.js';
import LanternLCP from '../computed/metrics/lantern-largest-contentful-paint.js';
import LoadSimulator from '../computed/load-simulator.js';
import {ByteEfficiencyAudit} from './byte-efficiency/byte-efficiency-audit.js';
import ImageRecords from '../computed/image-records.js';
import NetworkRecords from '../computed/network-records.js';

const UIStrings = {
/** Title of a lighthouse audit that tells a user to preload an image in order to improve their LCP time. */
Expand Down Expand Up @@ -217,6 +219,11 @@ class PreloadLCPImageAudit extends Audit {
const gatherContext = artifacts.GatherContext;
const trace = artifacts.traces[PreloadLCPImageAudit.DEFAULT_PASS];
const devtoolsLog = artifacts.devtoolsLogs[PreloadLCPImageAudit.DEFAULT_PASS];
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
const images = await ImageRecords.request({
ImageElements: artifacts.ImageElements,
networkRecords,
}, context);
const URL = artifacts.URL;
const metricData = {trace, devtoolsLog, gatherContext, settings: context.settings, URL};
const lcpElement = artifacts.TraceElements
Expand All @@ -230,7 +237,7 @@ class PreloadLCPImageAudit extends Audit {

const graph = lanternLCP.pessimisticGraph;
// eslint-disable-next-line max-len
const {lcpNodeToPreload, initiatorPath} = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, artifacts.ImageElements);
const {lcpNodeToPreload, initiatorPath} = PreloadLCPImageAudit.getLCPNodeToPreload(mainResource, graph, lcpElement, images);

const {results, wastedMs} =
PreloadLCPImageAudit.computeWasteWithGraph(lcpElement, lcpNodeToPreload, graph, simulator);
Expand Down
8 changes: 4 additions & 4 deletions core/computed/image-records.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
*/
'use strict';

import URL from '../lib/url-shim.js';
import {makeComputedArtifact} from './computed-artifact.js';

class ImageRecords {
Expand Down Expand Up @@ -39,12 +38,13 @@ class ImageRecords {

for (const element of data.ImageElements) {
const networkRecord = indexedNetworkRecords[element.src];
const mimeType = networkRecord?.mimeType;
// Ignore if we aren't sure this is really an image.
// TODO: we should add something in tracing to get this straight from blink.
if (!networkRecord) continue;

// Don't change the guessed mime type if no mime type was found.
imageRecords.push({
...element,
mimeType: mimeType ? mimeType : URL.guessMimeType(element.src),
mimeType: networkRecord.mimeType,
Copy link
Member

Choose a reason for hiding this comment

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

brendan said:

This might be going too aggressive, though :/ About 1.6% of sites in HTTP Archive (July 2022) currently have an image LCP which would no longer be detected after this. It is catching a bunch of accidental css files (at least going by filenames), but there also just seem to be a ton of people serving images as application/images, application/png, or good old application/xml.

and then I was like... "i thought we were gonna used resourceType but then see #13338 (comment) where you showed an HTML page getting described as resourceType: Image

i tried to reproduce this locally and failed. but then tried <img src="page.html">. Yeah very interesting.

These resourceTypes are defined in the blink loader and set (for non-scripts) here

I guess this type is still in the "loader" before it's determined that it's an invalid asset for the intended use.

Ah well. So you're right about resourceType not being useful for this situation.

humph.

});
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ describe('Page uses responsive images', () => {
mimeType: 'image/png',
resourceSize: 1024 * 100,
transferSize: 0,
finished: true,
statusCode: 200,
url: 'https://google.com/logo.png',
};
const auditResult = await UsesResponsiveImagesAudit.audit_({
Expand Down
95 changes: 45 additions & 50 deletions core/test/audits/lcp-lazy-loaded-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
*/

import LargestContentfulPaintLazyLoaded from '../../audits/lcp-lazy-loaded.js';
import {createTestTrace} from '../create-test-trace.js';
import {networkRecordsToDevtoolsLog} from '../network-records-to-devtools-log.js';

const SAMPLE_NODE = {
devtoolsNodePath: '1,HTML,1,BODY,3,DIV,2,IMG',
Expand All @@ -14,7 +16,7 @@ const SAMPLE_NODE = {
};
function generateImage(loading, clientRectTop) {
return {
src: 'test',
src: 'http://example.com/test.png',
loading,
clientRect: {
top: clientRectTop,
Expand All @@ -25,23 +27,41 @@ function generateImage(loading, clientRectTop) {
node: SAMPLE_NODE,
};
}

/**
* @param {ImageElement[]} imageElements
*/
function createArtifacts(imageElements) {
const networkRecords = imageElements.map(e => ({
url: e.src,
mimeType: 'image/png',
}));
return {
traces: {
[LargestContentfulPaintLazyLoaded.DEFAULT_PASS]: createTestTrace({
traceEnd: 6e3,
largestContentfulPaint: 45e2,
}),
},
devtoolsLogs: {[LargestContentfulPaintLazyLoaded.DEFAULT_PASS]:
networkRecordsToDevtoolsLog(networkRecords)},
TraceElements: imageElements.length ? [{
traceEventType: 'largest-contentful-paint',
node: SAMPLE_NODE,
}] : [],
ImageElements: imageElements,
ViewportDimensions: {
innerHeight: 500,
innerWidth: 300,
},
};
}

describe('Performance: lcp-lazy-loaded audit', () => {
it('correctly surfaces the lazy loaded LCP element', async () => {
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);
const artifacts = createArtifacts([generateImage('lazy', 0)]);
const context = {settings: {}, computedCache: new Map()};
const auditResult = await LargestContentfulPaintLazyLoaded.audit(artifacts, context);
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');
Expand All @@ -50,49 +70,24 @@ describe('Performance: lcp-lazy-loaded audit', () => {
});

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);
const artifacts = createArtifacts([generateImage('eager', 0)]);
const context = {settings: {}, computedCache: new Map()};
const auditResult = await LargestContentfulPaintLazyLoaded.audit(artifacts, context);
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);
const artifacts = createArtifacts([generateImage('lazy', 700)]);
const context = {settings: {}, computedCache: new Map()};
const auditResult = await LargestContentfulPaintLazyLoaded.audit(artifacts, context);
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);
const artifacts = createArtifacts([]);
const context = {settings: {}, computedCache: new Map()};
const auditResult = await LargestContentfulPaintLazyLoaded.audit(artifacts, context);
expect(auditResult.score).toEqual(1);
expect(auditResult.notApplicable).toEqual(true);
});
Expand Down
1 change: 1 addition & 0 deletions core/test/audits/preload-lcp-image-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ describe('Performance: preload-lcp audit', () => {
{
requestId: '4',
resourceType: 'Image',
mimeType: 'image/png',
priority: 'High',
isLinkPreload: false,
startTime: 2,
Expand Down
24 changes: 19 additions & 5 deletions core/test/computed/image-records-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ function mockRequest(partial = {}) {
*/
function mockElement(partial = {}) {
return {
src: 'https://example.com/img.png',
src: partial.src ?? 'https://example.com/img.png',
Copy link
Contributor

Choose a reason for hiding this comment

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

Isn't this what's happening already? Where partial.src overrides this value if provided?

srcset: '',
displayedWidth: 200,
displayedHeight: 200,
Expand Down Expand Up @@ -159,14 +159,28 @@ describe('compute_', () => {
expect(elements).toEqual([mockElement({mimeType: 'image/png'})]);
});

it('guess mime type if no request', async () => {
it('ignore image element if no valid record', async () => {
const elements = await ImageRecords.compute_({
ImageElements: [
mockElement(),
mockElement({src: 'https://example.com/img.png'}),
mockElement({src: 'https://example.com/img2.png'}),
],
networkRecords: [
mockRequest({
mimeType: 'iam/trash',
url: 'https://example.com/img.png',
finished: true,
statusCode: 200,
}),
mockRequest({
mimeType: 'image/png',
url: 'https://example.com/img2.png',
finished: true,
statusCode: 500,
}),
],
networkRecords: [],
});

expect(elements).toEqual([mockElement({mimeType: 'image/png'})]);
expect(elements).toEqual([]);
});
});
Loading