From 3125e8a8e75be412d14ea57a9d1f9d7b6217a7a3 Mon Sep 17 00:00:00 2001 From: Ivan Akulov Date: Fri, 1 Aug 2025 21:32:04 +0200 Subject: [PATCH 1/2] core: compute LCP as FCP if it happened within the same frame --- .../metrics/lantern-largest-contentful-paint.js | 10 +++++++++- core/test/audits/__snapshots__/metrics-test.js.snap | 4 ++-- .../__snapshots__/predictive-perf-test.js.snap | 6 +++--- .../byte-efficiency/byte-efficiency-audit-test.js | 12 ++++++------ .../metrics/largest-contentful-paint-test.js | 6 +++--- 5 files changed, 23 insertions(+), 15 deletions(-) diff --git a/core/computed/metrics/lantern-largest-contentful-paint.js b/core/computed/metrics/lantern-largest-contentful-paint.js index a2eb51c048d4..7fdd0efce9bc 100644 --- a/core/computed/metrics/lantern-largest-contentful-paint.js +++ b/core/computed/metrics/lantern-largest-contentful-paint.js @@ -13,11 +13,19 @@ class LanternLargestContentfulPaint extends Lantern.Metrics.LargestContentfulPai /** * @param {LH.Artifacts.MetricComputationDataInput} data * @param {LH.Artifacts.ComputedContext} context - * @param {Omit=} extras + * @param {Required>} extras * @return {Promise} */ static async computeMetricWithGraphs(data, context, extras) { const params = await getComputationDataParams(data, context); + // If FCP and LCP happened within the same frame, we can assume their simulated values + // are also the same. + if ( + params.processedNavigation.timestamps.firstContentfulPaint === + params.processedNavigation.timestamps.largestContentfulPaint + ) { + return extras.fcpResult; + } return Promise.resolve(this.compute(params, extras)).catch(lanternErrorAdapter); } diff --git a/core/test/audits/__snapshots__/metrics-test.js.snap b/core/test/audits/__snapshots__/metrics-test.js.snap index c1643bc2d1b7..aef0078295d0 100644 --- a/core/test/audits/__snapshots__/metrics-test.js.snap +++ b/core/test/audits/__snapshots__/metrics-test.js.snap @@ -10,7 +10,7 @@ Object { "firstContentfulPaintTs": undefined, "interactive": 3149, "interactiveTs": undefined, - "largestContentfulPaint": 1524, + "largestContentfulPaint": 1059, "largestContentfulPaintAllFrames": undefined, "largestContentfulPaintAllFramesTs": undefined, "largestContentfulPaintTs": undefined, @@ -118,7 +118,7 @@ Object { "firstContentfulPaintTs": undefined, "interactive": 3149, "interactiveTs": undefined, - "largestContentfulPaint": 1524, + "largestContentfulPaint": 1059, "largestContentfulPaintAllFrames": undefined, "largestContentfulPaintAllFramesTs": undefined, "largestContentfulPaintTs": undefined, diff --git a/core/test/audits/__snapshots__/predictive-perf-test.js.snap b/core/test/audits/__snapshots__/predictive-perf-test.js.snap index 3c39841ffdac..fe273da27b00 100644 --- a/core/test/audits/__snapshots__/predictive-perf-test.js.snap +++ b/core/test/audits/__snapshots__/predictive-perf-test.js.snap @@ -3,15 +3,15 @@ exports[`Performance: predictive performance audit should compute the predicted values 1`] = ` Object { "optimisticFCP": 1059, - "optimisticLCP": 1445, + "optimisticLCP": 1059, "optimisticSI": 261, "optimisticTTI": 2132, "pessimisticFCP": 1059, - "pessimisticLCP": 1603, + "pessimisticLCP": 1059, "pessimisticSI": 1109, "pessimisticTTI": 3981, "roughEstimateOfFCP": 1059, - "roughEstimateOfLCP": 1524, + "roughEstimateOfLCP": 1059, "roughEstimateOfLCPLoadEnd": undefined, "roughEstimateOfLCPLoadStart": undefined, "roughEstimateOfSI": 1059, diff --git a/core/test/audits/byte-efficiency/byte-efficiency-audit-test.js b/core/test/audits/byte-efficiency/byte-efficiency-audit-test.js index 6b2115325001..3468a89b721a 100644 --- a/core/test/audits/byte-efficiency/byte-efficiency-audit-test.js +++ b/core/test/audits/byte-efficiency/byte-efficiency-audit-test.js @@ -235,7 +235,7 @@ describe('Byte efficiency base audit', () => { {computedCache: new Map()} ); - assert.equal(result.numericValue, 160); + assert.equal(result.numericValue, 0); }); it('should create load simulator with the specified settings', async () => { @@ -263,14 +263,14 @@ describe('Byte efficiency base audit', () => { let settings = {throttlingMethod: 'simulate', throttling: modestThrottling}; let result = await MockAudit.audit(artifacts, {settings, computedCache}); // expect modest savings - expect(result.numericValue).toBeLessThan(5000); - expect(result.numericValue).toMatchInlineSnapshot(`1220`); + expect(result.numericValue).toBeLessThan(500); + expect(result.numericValue).toMatchInlineSnapshot(`160`); settings = {throttlingMethod: 'simulate', throttling: ultraSlowThrottling}; result = await MockAudit.audit(artifacts, {settings, computedCache}); - // expect lots of savings - expect(result.numericValue).not.toBeLessThan(5000); - expect(result.numericValue).toMatchInlineSnapshot(`13580`); + // expect more savings + expect(result.numericValue).not.toBeLessThan(500); + expect(result.numericValue).toMatchInlineSnapshot(`1740`); }); it('should compute savings with throughput in timespan mode', async () => { diff --git a/core/test/computed/metrics/largest-contentful-paint-test.js b/core/test/computed/metrics/largest-contentful-paint-test.js index 7626287286f2..3e1a5c6c2d53 100644 --- a/core/test/computed/metrics/largest-contentful-paint-test.js +++ b/core/test/computed/metrics/largest-contentful-paint-test.js @@ -31,9 +31,9 @@ describe('Metrics: LCP', () => { pessimistic: Math.round(result.pessimisticEstimate.timeInMs)}). toMatchInlineSnapshot(` Object { - "optimistic": 1445, - "pessimistic": 1603, - "timing": 1524, + "optimistic": 1059, + "pessimistic": 1059, + "timing": 1059, } `); }); From 9401eb10f02dd3135cc35d727119ff79b3e5d39f Mon Sep 17 00:00:00 2001 From: Ivan Akulov Date: Sun, 3 Aug 2025 21:24:25 +0200 Subject: [PATCH 2/2] tests: add a test for FCP short-circuiting --- .../lantern-largest-contentful-paint.js | 20 +++++++++- .../metrics/largest-contentful-paint-test.js | 38 +++++++++++++++++++ 2 files changed, 57 insertions(+), 1 deletion(-) diff --git a/core/computed/metrics/lantern-largest-contentful-paint.js b/core/computed/metrics/lantern-largest-contentful-paint.js index 7fdd0efce9bc..73254724f97c 100644 --- a/core/computed/metrics/lantern-largest-contentful-paint.js +++ b/core/computed/metrics/lantern-largest-contentful-paint.js @@ -10,6 +10,8 @@ import {getComputationDataParams, lanternErrorAdapter} from './lantern-metric.js import {LanternFirstContentfulPaint} from './lantern-first-contentful-paint.js'; class LanternLargestContentfulPaint extends Lantern.Metrics.LargestContentfulPaint { + static disableFcpShortCircuiting = false; + /** * @param {LH.Artifacts.MetricComputationDataInput} data * @param {LH.Artifacts.ComputedContext} context @@ -22,7 +24,8 @@ class LanternLargestContentfulPaint extends Lantern.Metrics.LargestContentfulPai // are also the same. if ( params.processedNavigation.timestamps.firstContentfulPaint === - params.processedNavigation.timestamps.largestContentfulPaint + params.processedNavigation.timestamps.largestContentfulPaint && + !this.disableFcpShortCircuiting ) { return extras.fcpResult; } @@ -38,6 +41,21 @@ class LanternLargestContentfulPaint extends Lantern.Metrics.LargestContentfulPai const fcpResult = await LanternFirstContentfulPaint.request(data, context); return this.computeMetricWithGraphs(data, context, {fcpResult}); } + + /** + * Designed to be used solely in tests. + * + * @param {() => Promise} callback + * @return {Promise} + */ + static async withFcpShortCircuitDisabled(callback) { + this.disableFcpShortCircuiting = true; + try { + return await callback(); + } finally { + this.disableFcpShortCircuiting = false; + } + } } const LanternLargestContentfulPaintComputed = makeComputedArtifact( diff --git a/core/test/computed/metrics/largest-contentful-paint-test.js b/core/test/computed/metrics/largest-contentful-paint-test.js index 3e1a5c6c2d53..800d36748867 100644 --- a/core/test/computed/metrics/largest-contentful-paint-test.js +++ b/core/test/computed/metrics/largest-contentful-paint-test.js @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import {LanternLargestContentfulPaint} from '../../../computed/metrics/lantern-largest-contentful-paint.js'; import {LargestContentfulPaint} from '../../../computed/metrics/largest-contentful-paint.js'; import {getURLArtifactFromDevtoolsLog, readJson} from '../../test-utils.js'; @@ -52,4 +53,41 @@ Object { } `); }); + + it( + 'when simulating, should return the FCP result ' + + 'when original FCP and LCP timestamps are the same', + async () => { + const settings = {throttlingMethod: 'simulate'}; + const URL = getURLArtifactFromDevtoolsLog(devtoolsLog); + + const simulatedLcpOnlyResult = + await LanternLargestContentfulPaint.withFcpShortCircuitDisabled(() => + LanternLargestContentfulPaint.request({ + trace, + devtoolsLog, + gatherContext, + settings, + URL, + SourceMaps: [], + simulator: null, + }, {settings, computedCache: new Map()}) + ); + + const simulatedLcpWithFcpResult = await LargestContentfulPaint.request({ + trace, + devtoolsLog, + gatherContext, + settings, + URL, + SourceMaps: [], + simulator: null, + }, {settings, computedCache: new Map()}); + + expect(simulatedLcpOnlyResult.timing).not.toBe(simulatedLcpWithFcpResult.timing); + expect(simulatedLcpOnlyResult.optimisticEstimate.timeInMs) + .not.toBe(simulatedLcpWithFcpResult.optimisticEstimate.timeInMs); + expect(simulatedLcpOnlyResult.pessimisticEstimate.timeInMs) + .not.toBe(simulatedLcpWithFcpResult.pessimisticEstimate.timeInMs); + }); });