From 3b43d11ea4ccd2c1fb2d9d65343bae510d576cb5 Mon Sep 17 00:00:00 2001 From: Connor Clark Date: Mon, 25 Mar 2024 15:11:14 -0700 Subject: [PATCH] LCP --- .../lantern-largest-contentful-paint.js | 92 ++------------- .../metrics/largest-contentful-paint.js | 108 ++++++++++++++++++ .../lantern-largest-contentful-paint-test.js | 24 ++-- 3 files changed, 131 insertions(+), 93 deletions(-) create mode 100644 core/lib/lantern/metrics/largest-contentful-paint.js rename core/test/{computed => lib/lantern}/metrics/lantern-largest-contentful-paint-test.js (52%) diff --git a/core/computed/metrics/lantern-largest-contentful-paint.js b/core/computed/metrics/lantern-largest-contentful-paint.js index ad2fe2da464c..8348c2d9aff1 100644 --- a/core/computed/metrics/lantern-largest-contentful-paint.js +++ b/core/computed/metrics/lantern-largest-contentful-paint.js @@ -1,91 +1,25 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2018 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import {makeComputedArtifact} from '../computed-artifact.js'; -import {LanternMetric} from './lantern-metric.js'; -import {LighthouseError} from '../../lib/lh-error.js'; +import {LargestContentfulPaint} from '../../lib/lantern/metrics/largest-contentful-paint.js'; +import {getComputationDataParams} from './lantern-metric.js'; import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js'; -/** @typedef {import('../../lib/lantern/base-node.js').Node} Node */ +/** @typedef {import('../../lib/lantern/metric.js').Extras} Extras */ -class LanternLargestContentfulPaint extends LanternMetric { +class LanternLargestContentfulPaint extends LargestContentfulPaint { /** - * @return {LH.Gatherer.Simulation.MetricCoefficients} - */ - static get COEFFICIENTS() { - return { - intercept: 0, - optimistic: 0.5, - pessimistic: 0.5, - }; - } - - /** - * Low priority image nodes are usually offscreen and very unlikely to be the - * resource that is required for LCP. Our LCP graphs include everything except for these images. - * - * @param {Node} node - * @return {boolean} - */ - static isNotLowPriorityImageNode(node) { - if (node.type !== 'network') return true; - const isImage = node.record.resourceType === 'Image'; - const isLowPriority = node.record.priority === 'Low' || node.record.priority === 'VeryLow'; - return !isImage || !isLowPriority; - } - - /** - * @param {Node} dependencyGraph - * @param {LH.Artifacts.ProcessedNavigation} processedNavigation - * @return {Node} - */ - static getOptimisticGraph(dependencyGraph, processedNavigation) { - const lcp = processedNavigation.timestamps.largestContentfulPaint; - if (!lcp) { - throw new LighthouseError(LighthouseError.errors.NO_LCP); - } - - return LanternFirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, { - cutoffTimestamp: lcp, - treatNodeAsRenderBlocking: LanternLargestContentfulPaint.isNotLowPriorityImageNode, - }); - } - - /** - * @param {Node} dependencyGraph - * @param {LH.Artifacts.ProcessedNavigation} processedNavigation - * @return {Node} - */ - static getPessimisticGraph(dependencyGraph, processedNavigation) { - const lcp = processedNavigation.timestamps.largestContentfulPaint; - if (!lcp) { - throw new LighthouseError(LighthouseError.errors.NO_LCP); - } - - return LanternFirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, { - cutoffTimestamp: lcp, - treatNodeAsRenderBlocking: _ => true, - // For pessimistic LCP we'll include *all* layout nodes - additionalCpuNodesToTreatAsRenderBlocking: node => node.didPerformLayout(), - }); - } - - /** - * @param {LH.Gatherer.Simulation.Result} simulationResult - * @return {LH.Gatherer.Simulation.Result} + * @param {LH.Artifacts.MetricComputationDataInput} data + * @param {LH.Artifacts.ComputedContext} context + * @param {Omit=} extras + * @return {Promise} */ - static getEstimateFromSimulation(simulationResult) { - const nodeTimesNotOffscreenImages = Array.from(simulationResult.nodeTimings.entries()) - .filter(entry => LanternLargestContentfulPaint.isNotLowPriorityImageNode(entry[0])) - .map(entry => entry[1].endTime); - - return { - timeInMs: Math.max(...nodeTimesNotOffscreenImages), - nodeTimings: simulationResult.nodeTimings, - }; + static async computeMetricWithGraphs(data, context, extras) { + return this.compute(await getComputationDataParams(data, context), extras); } /** @@ -95,9 +29,7 @@ class LanternLargestContentfulPaint extends LanternMetric { */ static async compute_(data, context) { const fcpResult = await LanternFirstContentfulPaint.request(data, context); - const metricResult = await this.computeMetricWithGraphs(data, context); - metricResult.timing = Math.max(metricResult.timing, fcpResult.timing); - return metricResult; + return this.computeMetricWithGraphs(data, context, {fcpResult}); } } diff --git a/core/lib/lantern/metrics/largest-contentful-paint.js b/core/lib/lantern/metrics/largest-contentful-paint.js new file mode 100644 index 000000000000..b7cdffb59ca4 --- /dev/null +++ b/core/lib/lantern/metrics/largest-contentful-paint.js @@ -0,0 +1,108 @@ +/** + * @license + * Copyright 2024 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import * as Lantern from '../types/lantern.js'; +import {Metric} from '../metric.js'; +import {LighthouseError} from '../../../lib/lh-error.js'; +import {FirstContentfulPaint} from './first-contentful-paint.js'; + +/** @typedef {import('../base-node.js').Node} Node */ + +class LargestContentfulPaint extends Metric { + /** + * @return {LH.Gatherer.Simulation.MetricCoefficients} + */ + static get COEFFICIENTS() { + return { + intercept: 0, + optimistic: 0.5, + pessimistic: 0.5, + }; + } + + /** + * Low priority image nodes are usually offscreen and very unlikely to be the + * resource that is required for LCP. Our LCP graphs include everything except for these images. + * + * @param {Node} node + * @return {boolean} + */ + static isNotLowPriorityImageNode(node) { + if (node.type !== 'network') return true; + const isImage = node.record.resourceType === 'Image'; + const isLowPriority = node.record.priority === 'Low' || node.record.priority === 'VeryLow'; + return !isImage || !isLowPriority; + } + + /** + * @param {Node} dependencyGraph + * @param {LH.Artifacts.ProcessedNavigation} processedNavigation + * @return {Node} + */ + static getOptimisticGraph(dependencyGraph, processedNavigation) { + const lcp = processedNavigation.timestamps.largestContentfulPaint; + if (!lcp) { + throw new LighthouseError(LighthouseError.errors.NO_LCP); + } + + return FirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, { + cutoffTimestamp: lcp, + treatNodeAsRenderBlocking: LargestContentfulPaint.isNotLowPriorityImageNode, + }); + } + + /** + * @param {Node} dependencyGraph + * @param {LH.Artifacts.ProcessedNavigation} processedNavigation + * @return {Node} + */ + static getPessimisticGraph(dependencyGraph, processedNavigation) { + const lcp = processedNavigation.timestamps.largestContentfulPaint; + if (!lcp) { + throw new LighthouseError(LighthouseError.errors.NO_LCP); + } + + return FirstContentfulPaint.getFirstPaintBasedGraph(dependencyGraph, { + cutoffTimestamp: lcp, + treatNodeAsRenderBlocking: _ => true, + // For pessimistic LCP we'll include *all* layout nodes + additionalCpuNodesToTreatAsRenderBlocking: node => node.didPerformLayout(), + }); + } + + /** + * @param {LH.Gatherer.Simulation.Result} simulationResult + * @return {LH.Gatherer.Simulation.Result} + */ + static getEstimateFromSimulation(simulationResult) { + const nodeTimesNotOffscreenImages = Array.from(simulationResult.nodeTimings.entries()) + .filter(entry => LargestContentfulPaint.isNotLowPriorityImageNode(entry[0])) + .map(entry => entry[1].endTime); + + return { + timeInMs: Math.max(...nodeTimesNotOffscreenImages), + nodeTimings: simulationResult.nodeTimings, + }; + } + + /** + * @param {Lantern.Simulation.MetricComputationDataInput} data + * @param {Omit=} extras + * @return {Promise} + */ + static async compute(data, extras) { + const fcpResult = extras?.fcpResult; + if (!fcpResult) { + throw new Error('FCP is required to calculate the LCP metric'); + } + + const metricResult = await super.compute(data, extras); + metricResult.timing = Math.max(metricResult.timing, fcpResult.timing); + return metricResult; + } +} + +export {LargestContentfulPaint}; diff --git a/core/test/computed/metrics/lantern-largest-contentful-paint-test.js b/core/test/lib/lantern/metrics/lantern-largest-contentful-paint-test.js similarity index 52% rename from core/test/computed/metrics/lantern-largest-contentful-paint-test.js rename to core/test/lib/lantern/metrics/lantern-largest-contentful-paint-test.js index b702f67c23a3..4a17c06c31f2 100644 --- a/core/test/computed/metrics/lantern-largest-contentful-paint-test.js +++ b/core/test/lib/lantern/metrics/lantern-largest-contentful-paint-test.js @@ -1,27 +1,25 @@ /** * @license - * Copyright 2019 Google LLC + * Copyright 2024 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import assert from 'assert/strict'; -import {LanternLargestContentfulPaint} from '../../../computed/metrics/lantern-largest-contentful-paint.js'; -import {getURLArtifactFromDevtoolsLog, readJson} from '../../test-utils.js'; +import {LargestContentfulPaint} from '../../../../lib/lantern/metrics/largest-contentful-paint.js'; +import {FirstContentfulPaint} from '../../../../lib/lantern/metrics/first-contentful-paint.js'; +import {getComputationDataFromFixture} from './metric-test-utils.js'; +import {readJson} from '../../../test-utils.js'; -const trace = readJson('../../fixtures/traces/lcp-m78.json', import.meta); -const devtoolsLog = readJson('../../fixtures/traces/lcp-m78.devtools.log.json', import.meta); +const trace = readJson('../../../fixtures/traces/lcp-m78.json', import.meta); +const devtoolsLog = readJson('../../../fixtures/traces/lcp-m78.devtools.log.json', import.meta); -const URL = getURLArtifactFromDevtoolsLog(devtoolsLog); describe('Metrics: Lantern LCP', () => { it('should compute predicted value', async () => { - const gatherContext = {gatherMode: 'navigation'}; - const settings = {}; - const computedCache = new Map(); - const result = await LanternLargestContentfulPaint.request( - {trace, devtoolsLog, gatherContext, settings, URL}, - {computedCache} - ); + const data = await getComputationDataFromFixture({trace, devtoolsLog}); + const result = await LargestContentfulPaint.compute(data, { + fcpResult: await FirstContentfulPaint.compute(data), + }); expect({ timing: Math.round(result.timing),