Skip to content

Commit

Permalink
LCP
Browse files Browse the repository at this point in the history
  • Loading branch information
connorjclark committed Mar 25, 2024
1 parent f0db7c3 commit 3b43d11
Show file tree
Hide file tree
Showing 3 changed files with 131 additions and 93 deletions.
92 changes: 12 additions & 80 deletions core/computed/metrics/lantern-largest-contentful-paint.js
Original file line number Diff line number Diff line change
@@ -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<LH.Artifacts.NetworkRequest>} 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, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
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);
}

/**
Expand All @@ -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});
}
}

Expand Down
108 changes: 108 additions & 0 deletions core/lib/lantern/metrics/largest-contentful-paint.js
Original file line number Diff line number Diff line change
@@ -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<import('../metric.js').Extras, 'optimistic'>=} extras
* @return {Promise<LH.Artifacts.LanternMetric>}
*/
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};
Original file line number Diff line number Diff line change
@@ -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),
Expand Down

0 comments on commit 3b43d11

Please sign in to comment.