-
Notifications
You must be signed in to change notification settings - Fork 9.4k
/
lcp-image-record.js
74 lines (64 loc) · 3.02 KB
/
lcp-image-record.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* @license
* Copyright 2023 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import {makeComputedArtifact} from './computed-artifact.js';
import {NetworkRecords} from './network-records.js';
import {ProcessedNavigation} from './processed-navigation.js';
import {LighthouseError} from '../lib/lh-error.js';
/**
* @fileoverview Match the LCP event with the paint event to get the request of the image actually painted.
* This could differ from the `ImageElement` associated with the nodeId if e.g. the LCP
* was a pseudo-element associated with a node containing a smaller background-image.
*/
class LCPImageRecord {
/**
* @param {{trace: LH.Trace, devtoolsLog: LH.DevtoolsLog}} data
* @param {LH.Artifacts.ComputedContext} context
* @return {Promise<LH.Artifacts.NetworkRequest|undefined>}
*/
static async compute_(data, context) {
const {trace, devtoolsLog} = data;
const networkRecords = await NetworkRecords.request(devtoolsLog, context);
const processedNavigation = await ProcessedNavigation.request(trace, context);
if (processedNavigation.timings.largestContentfulPaint === undefined) {
throw new LighthouseError(LighthouseError.errors.NO_LCP);
}
// Use main-frame-only LCP to match the metric value.
const lcpEvent = processedNavigation.largestContentfulPaintEvt;
if (!lcpEvent) return;
const lcpImagePaintEvent = trace.traceEvents.filter(e => {
return e.name === 'LargestImagePaint::Candidate' &&
e.args.frame === lcpEvent.args.frame &&
e.args.data?.DOMNodeId === lcpEvent.args.data?.nodeId &&
e.args.data?.size === lcpEvent.args.data?.size;
// Get last candidate, in case there was more than one.
}).sort((a, b) => b.ts - a.ts)[0];
const lcpUrl = lcpImagePaintEvent?.args.data?.imageUrl;
if (!lcpUrl) return;
const candidates = networkRecords.filter(record => {
return record.url === lcpUrl &&
record.finished &&
// Same frame as LCP trace event.
record.frameId === lcpImagePaintEvent.args.frame &&
record.networkRequestTime < (processedNavigation.timestamps.largestContentfulPaint || 0);
}).map(record => {
// Follow any redirects to find the real image request.
while (record.redirectDestination) {
record = record.redirectDestination;
}
return record;
}).filter(record => {
// Don't select if also loaded by some other means (xhr, etc). `resourceType`
// isn't set on redirect _sources_, so have to check after following redirects.
return record.resourceType === 'Image';
});
// If there are still multiple candidates, at this point it appears the page
// simply made multiple requests for the image. The first loaded is the best
// guess of the request that made the image available for use.
return candidates.sort((a, b) => a.networkEndTime - b.networkEndTime)[0];
}
}
const LCPImageRecordComputed = makeComputedArtifact(LCPImageRecord, ['devtoolsLog', 'trace']);
export {LCPImageRecordComputed as LCPImageRecord};