Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core(cls-culprits-insight): implement #16357

Merged
merged 5 commits into from
Feb 26, 2025
Merged
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
108 changes: 96 additions & 12 deletions core/audits/insights/cls-culprits-insight.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
/* eslint-disable no-unused-vars */ // TODO: remove once implemented.

/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {UIStrings} from '@paulirish/trace_engine/models/trace/insights/CLSCulprits.js';
import {UIStrings as InsightUIStrings} from '@paulirish/trace_engine/models/trace/insights/CLSCulprits.js';

import {Audit} from '../audit.js';
import * as i18n from '../../lib/i18n/i18n.js';
import {adaptInsightToAuditProduct, makeNodeItemForNodeId} from './insight-audit.js';
import TraceElements from '../../gather/gatherers/trace-elements.js';
import {CumulativeLayoutShift} from '../../computed/metrics/cumulative-layout-shift.js';

const MAX_LAYOUT_SHIFTS_PER_CLUSTER = 5;

/** @typedef {{extra?: LH.Audit.Details.NodeValue | LH.Audit.Details.UrlValue, cause: LH.IcuMessage}} SubItem */

// eslint-disable-next-line max-len
const str_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/CLSCulprits.js', UIStrings);
const insightStr_ = i18n.createIcuMessageFn('node_modules/@paulirish/trace_engine/models/trace/insights/CLSCulprits.js', InsightUIStrings);

/* eslint-disable max-len */
const UIStrings = {
/** Label for a column in a data table; entries in this column will be a number representing how large the layout shift was. */
columnScore: 'Layout shift score',
};
/* eslint-enable max-len */

const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings);

class CLSCulpritsInsight extends Audit {
/**
Expand All @@ -22,32 +35,103 @@ class CLSCulpritsInsight extends Audit {
static get meta() {
return {
id: 'cls-culprits-insight',
title: str_(UIStrings.title),
failureTitle: str_(UIStrings.title),
description: str_(UIStrings.description),
title: insightStr_(InsightUIStrings.title),
failureTitle: insightStr_(InsightUIStrings.title),
description: insightStr_(InsightUIStrings.description),
guidanceLevel: 3,
requiredArtifacts: ['traces', 'TraceElements'],
replacesAudits: ['layout-shifts', 'non-composited-animations', 'unsized-images'],
};
}

/**
* @param {import('@paulirish/trace_engine/models/trace/insights/CLSCulprits.js').CLSCulpritsInsightModel} insight
* @param {import('../../lib/trace-engine.js').SaneSyntheticLayoutShift} event
* @param {LH.Artifacts.TraceElement[]} TraceElements
* @return {LH.Audit.Details.TableSubItems|undefined}
*/
static getCulpritSubItems(insight, event, TraceElements) {
const culprits = insight.shifts.get(event);
if (!culprits) {
return;
}

/** @type {SubItem[]} */
const subItems = [];
for (const backendNodeId of culprits.unsizedImages) {
subItems.push({
extra: makeNodeItemForNodeId(TraceElements, backendNodeId),
cause: insightStr_(InsightUIStrings.unsizedImages),
});
}
for (const request of culprits.fontRequests) {
const url = request.args.data.url;
subItems.push({
extra: {type: 'url', value: url},
cause: insightStr_(InsightUIStrings.fontRequest),
});
}
if (culprits.iframeIds.length) {
subItems.push({
cause: insightStr_(InsightUIStrings.injectedIframe),
});
}

if (subItems.length) {
return {type: 'subitems', items: subItems};
}
}

/**
* @param {LH.Artifacts} artifacts
* @param {LH.Audit.Context} context
* @return {Promise<LH.Audit.Product>}
*/
static async audit(artifacts, context) {
// TODO: implement.
return adaptInsightToAuditProduct(artifacts, context, 'CLSCulprits', (insight) => {
/** @type {LH.Audit.Details.Table['headings']} */
const headings = [
/* eslint-disable max-len */
{key: 'node', valueType: 'node', subItemsHeading: {key: 'extra'}, label: insightStr_(i18n.UIStrings.columnElement)},
{key: 'score', valueType: 'numeric', subItemsHeading: {key: 'cause', valueType: 'text'}, granularity: 0.001, label: str_(UIStrings.columnScore)},
/* eslint-enable max-len */
];
/** @type {LH.Audit.Details.Table['items']} */
const items = [
];
return Audit.makeTableDetails(headings, items);

const tables = insight.clusters.map(cluster => {
const events =
/** @type {import('../../lib/trace-engine.js').SaneSyntheticLayoutShift[]} */ (
cluster.events.filter(e => !!e.args.data)
).sort((a, b) => b.args.data.weighted_score_delta - a.args.data.weighted_score_delta)
.slice(0, MAX_LAYOUT_SHIFTS_PER_CLUSTER);
const impactByNodeId = CumulativeLayoutShift.getImpactByNodeId(events.map(e => ({
impactedNodes: e.args.data.impacted_nodes,
ts: e.ts,
isMainFrame: e.args.data.is_main_frame,
weightedScore: e.args.data.weighted_score_delta,
event: /** @type {any} */ (e),
})));

/** @type {LH.Audit.Details.Table['items']} */
const items = events.map(event => {
const biggestImpactNodeId = TraceElements.getBiggestImpactNodeForShiftEvent(
event.args.data.impacted_nodes || [], impactByNodeId, event);
return {
node: makeNodeItemForNodeId(artifacts.TraceElements, biggestImpactNodeId),
score: event.args.data?.weighted_score_delta,
subItems: this.getCulpritSubItems(insight, event, artifacts.TraceElements),
};
});
items.unshift({
node: {type: 'text', value: insightStr_(i18n.UIStrings.total)},
score: cluster.clusterCumulativeScore,
});
return Audit.makeTableDetails(headings, items);
});

return Audit.makeListDetails(tables);
});
}
}

export default CLSCulpritsInsight;
export {UIStrings};
2 changes: 1 addition & 1 deletion core/audits/layout-shifts.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class LayoutShifts extends Audit {
const items = [];
const layoutShiftEvents =
/** @type {import('../lib/trace-engine.js').SaneSyntheticLayoutShift[]} */(
clusters.flatMap(c => c.events)
clusters.flatMap(c => c.events).filter(e => !!e.args.data)
);
const topLayoutShiftEvents = layoutShiftEvents
.sort((a, b) => b.args.data.weighted_score_delta - a.args.data.weighted_score_delta)
Expand Down
2 changes: 2 additions & 0 deletions core/lib/i18n/i18n.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ const UIStrings = {
columnFailingElem: 'Failing Elements',
/** Label for a column in a data table; entries will be a description of the table item. */
columnDescription: 'Description',
/** Label for a row in a data table; the row represents the total number of something. */
total: 'Total',
/** Label for a row in a data table; entries will be the total number and byte size of all resources loaded by a web page. */
totalResourceType: 'Total',
Copy link
Member

Choose a reason for hiding this comment

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

Could we just remove totalResourceType now?

Copy link
Collaborator Author

@connorjclark connorjclark Feb 25, 2025

Choose a reason for hiding this comment

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

I don't know for sure. maybe the contexts encourage different translations.

/** Label for a row in a data table; entries will be the total number and byte size of all 'Document' resources loaded by a web page. */
Expand Down
2 changes: 2 additions & 0 deletions core/scripts/i18n/collect-strings.js
Original file line number Diff line number Diff line change
Expand Up @@ -756,6 +756,8 @@ function checkKnownFixedCollisions(strings) {
'Severity',
'The page was evicted from the cache to allow another page to be cached.',
'The page was evicted from the cache to allow another page to be cached.',
'Total',
'Total',
'Use $MARKDOWN_SNIPPET_0$ to detect unused JavaScript code. $LINK_START_0$Learn more$LINK_END_0$',
'Use $MARKDOWN_SNIPPET_0$ to detect unused JavaScript code. $LINK_START_0$Learn more$LINK_END_0$',
'Use the $MARKDOWN_SNIPPET_0$ component and set the appropriate $MARKDOWN_SNIPPET_1$. $LINK_START_0$Learn more$LINK_END_0$.',
Expand Down
82 changes: 72 additions & 10 deletions core/test/fixtures/user-flows/reports/sample-flow-result.json
Original file line number Diff line number Diff line change
Expand Up @@ -3841,8 +3841,15 @@
"id": "cls-culprits-insight",
"title": "Layout shift culprits",
"description": "Layout shifts occur when elements move absent any user interaction. [Investigate the causes of layout shifts](https://web.dev/articles/optimize-cls), such as elements being added, removed, or their fonts changing as the page loads.",
"score": null,
"scoreDisplayMode": "notApplicable",
"score": 1,
"scoreDisplayMode": "numeric",
"metricSavings": {
"CLS": 0
},
"details": {
"type": "list",
"items": []
},
"guidanceLevel": 3,
"replacesAudits": [
"layout-shifts",
Expand Down Expand Up @@ -7902,7 +7909,7 @@
"audits[total-byte-weight].details.headings[1].label",
"audits[unused-javascript].details.headings[1].label"
],
"core/lib/i18n/i18n.js | totalResourceType": [
"core/lib/i18n/i18n.js | total": [
"audits[resource-summary].details.items[0].label"
],
"core/lib/i18n/i18n.js | scriptResourceType": [
Expand Down Expand Up @@ -11277,8 +11284,51 @@
"id": "cls-culprits-insight",
"title": "Layout shift culprits",
"description": "Layout shifts occur when elements move absent any user interaction. [Investigate the causes of layout shifts](https://web.dev/articles/optimize-cls), such as elements being added, removed, or their fonts changing as the page loads.",
"score": null,
"scoreDisplayMode": "notApplicable",
"score": 0,
"scoreDisplayMode": "numeric",
"metricSavings": {
"CLS": 0
},
"details": {
"type": "list",
"items": [
{
"type": "table",
"headings": [
{
"key": "node",
"valueType": "node",
"subItemsHeading": {
"key": "extra"
},
"label": "Element"
},
{
"key": "score",
"valueType": "numeric",
"subItemsHeading": {
"key": "cause",
"valueType": "text"
},
"granularity": 0.001,
"label": "Layout shift score"
}
],
"items": [
{
"node": {
"type": "text",
"value": "Total"
},
"score": 0.10206561360874848
},
{
"score": 0.10206561360874848
}
]
}
]
},
"guidanceLevel": 3,
"replacesAudits": [
"layout-shifts",
Expand Down Expand Up @@ -13135,8 +13185,9 @@
"audits[uses-long-cache-ttl].details.headings[2].label",
"audits[total-byte-weight].details.headings[1].label"
],
"core/lib/i18n/i18n.js | totalResourceType": [
"audits[resource-summary].details.items[0].label"
"core/lib/i18n/i18n.js | total": [
"audits[resource-summary].details.items[0].label",
"audits[cls-culprits-insight].details.items[0].items[0].node.value"
],
"core/lib/i18n/i18n.js | imageResourceType": [
"audits[resource-summary].details.items[1].label"
Expand Down Expand Up @@ -13198,6 +13249,7 @@
],
"core/lib/i18n/i18n.js | columnElement": [
"audits[layout-shifts].details.headings[0].label",
"audits[cls-culprits-insight].details.items[0].headings[0].label",
"audits[dom-size-insight].details.headings[1].label"
],
"core/audits/layout-shifts.js | columnScore": [
Expand Down Expand Up @@ -13383,6 +13435,9 @@
"node_modules/@paulirish/trace_engine/models/trace/insights/CLSCulprits.js | description": [
"audits[cls-culprits-insight].description"
],
"core/audits/insights/cls-culprits-insight.js | columnScore": [
"audits[cls-culprits-insight].details.items[0].headings[1].label"
],
"node_modules/@paulirish/trace_engine/models/trace/insights/DocumentLatency.js | title": [
"audits[document-latency-insight].title"
],
Expand Down Expand Up @@ -22975,8 +23030,15 @@
"id": "cls-culprits-insight",
"title": "Layout shift culprits",
"description": "Layout shifts occur when elements move absent any user interaction. [Investigate the causes of layout shifts](https://web.dev/articles/optimize-cls), such as elements being added, removed, or their fonts changing as the page loads.",
"score": null,
"scoreDisplayMode": "notApplicable",
"score": 1,
"scoreDisplayMode": "numeric",
"metricSavings": {
"CLS": 0
},
"details": {
"type": "list",
"items": []
},
"guidanceLevel": 3,
"replacesAudits": [
"layout-shifts",
Expand Down Expand Up @@ -27127,7 +27189,7 @@
"audits[third-party-summary].details.headings[1].label",
"audits[total-byte-weight].details.headings[1].label"
],
"core/lib/i18n/i18n.js | totalResourceType": [
"core/lib/i18n/i18n.js | total": [
"audits[resource-summary].details.items[0].label"
],
"core/lib/i18n/i18n.js | imageResourceType": [
Expand Down
Loading