diff --git a/core/audits/layout-shifts.js b/core/audits/layout-shifts.js new file mode 100644 index 000000000000..89f319339024 --- /dev/null +++ b/core/audits/layout-shifts.js @@ -0,0 +1,126 @@ +/** + * @license Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import {Audit} from './audit.js'; +import * as i18n from '../lib/i18n/i18n.js'; +import {CumulativeLayoutShift as CumulativeLayoutShiftComputed} from '../computed/metrics/cumulative-layout-shift.js'; +import CumulativeLayoutShift from './metrics/cumulative-layout-shift.js'; +import TraceElements from '../gather/gatherers/trace-elements.js'; + +/** @typedef {LH.Audit.Details.TableItem & {node?: LH.Audit.Details.NodeValue, score: number, subItems?: {type: 'subitems', items: SubItem[]}}} Item */ +/** @typedef {{node?: LH.Audit.Details.NodeValue, cause: LH.IcuMessage}} SubItem */ + +const UIStrings = { + /** Descriptive title of a diagnostic audit that provides the top elements affected by layout shifts. */ + title: 'Avoid large layout shifts', + /** Description of a diagnostic audit that provides the top elements affected by layout shifts. "windowing" means the metric value is calculated using the subset of events in a small window of time during the run. "normalization" is a good substitute for "windowing". The last sentence starting with 'Learn' becomes link text to additional documentation. */ + description: 'These are the largest layout shifts observed on the page. Some layout shifts may not be included in the CLS metric value due to [windowing](https://web.dev/articles/cls#what_is_cls). [Learn how to improve CLS](https://web.dev/articles/optimize-cls)', + /** 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', + /** A possible reason why that the layout shift occured. */ + rootCauseUnsizedMedia: 'The size of an element changed when CSS was applied', +}; + +const str_ = i18n.createIcuMessageFn(import.meta.url, UIStrings); + +class LayoutShifts extends Audit { + /** + * @return {LH.Audit.Meta} + */ + static get meta() { + return { + id: 'layout-shifts', + title: str_(UIStrings.title), + description: str_(UIStrings.description), + scoreDisplayMode: Audit.SCORING_MODES.METRIC_SAVINGS, + guidanceLevel: 2, + requiredArtifacts: ['traces', 'TraceElements'], + }; + } + + /** + * @param {LH.Artifacts} artifacts + * @param {LH.Audit.Context} context + * @return {Promise} + */ + static async audit(artifacts, context) { + const trace = artifacts.traces[Audit.DEFAULT_PASS]; + const clusters = trace.traceEngineResult?.data.LayoutShifts.clusters ?? []; + const {cumulativeLayoutShift: clsSavings, impactByNodeId} = + await CumulativeLayoutShiftComputed.request(trace, context); + + /** @type {Item[]} */ + const items = []; + const layoutShiftEvents = clusters.flatMap(c => c.events); + for (const event of layoutShiftEvents) { + const biggestImpactNodeId = TraceElements.getBiggestImpactNodeForShiftEvent( + event.args.data.impacted_nodes, impactByNodeId); + const biggestImpactElement = artifacts.TraceElements.find( + t => t.traceEventType === 'layout-shift' && t.nodeId === biggestImpactNodeId); + + // Turn root causes into sub-items. + const index = layoutShiftEvents.indexOf(event); + const rootCauses = trace.traceEngineResult?.rootCauses.layoutShifts[index]; + /** @type {SubItem[]} */ + const subItems = []; + if (rootCauses) { + // TODO ! finish these + for (const cause of rootCauses.fontChanges) { + // subItems.push({cause: JSON.stringify(cause)}); + } + for (const cause of rootCauses.iframes) { + // subItems.push({cause: JSON.stringify(cause)}); + } + for (const cause of rootCauses.renderBlockingRequests) { + // subItems.push({cause: JSON.stringify(cause)}); + } + for (const cause of rootCauses.unsizedMedia) { + const unsizedMediaElement = artifacts.TraceElements.find( + t => t.traceEventType === 'layout-shift' && t.nodeId === cause.node.backendNodeId); + subItems.push({ + node: unsizedMediaElement ? Audit.makeNodeItem(unsizedMediaElement.node) : undefined, + cause: str_(UIStrings.rootCauseUnsizedMedia), + }); + } + } + + items.push({ + node: biggestImpactElement ? Audit.makeNodeItem(biggestImpactElement.node) : undefined, + score: event.args.data.weighted_score_delta, + subItems: subItems.length ? {type: 'subitems', items: subItems} : undefined, + }); + } + + /** @type {LH.Audit.Details.Table['headings']} */ + const headings = [ + {key: 'node', valueType: 'node', label: str_(i18n.UIStrings.columnElement)}, + {key: 'score', valueType: 'numeric', granularity: 0.001, label: str_(UIStrings.columnScore)}, + ]; + + const details = Audit.makeTableDetails(headings, items); + + let displayValue; + if (items.length > 0) { + displayValue = str_(i18n.UIStrings.displayValueElementsFound, + {nodeCount: items.length}); + } + + const passed = clsSavings <= CumulativeLayoutShift.defaultOptions.p10; + + return { + score: passed ? 1 : 0, + scoreDisplayMode: passed ? Audit.SCORING_MODES.INFORMATIVE : undefined, + metricSavings: { + CLS: clsSavings, + }, + notApplicable: details.items.length === 0, + displayValue, + details, + }; + } +} + +export default LayoutShifts; +export {UIStrings}; diff --git a/core/config/default-config.js b/core/config/default-config.js index 4fe95c17ff2e..962e1559d16e 100644 --- a/core/config/default-config.js +++ b/core/config/default-config.js @@ -222,6 +222,7 @@ const defaultConfig = { 'largest-contentful-paint-element', 'lcp-lazy-loaded', 'layout-shift-elements', + 'layout-shifts', 'long-tasks', 'no-unload-listeners', 'non-composited-animations', @@ -477,6 +478,7 @@ const defaultConfig = { {id: 'largest-contentful-paint-element', weight: 0}, {id: 'lcp-lazy-loaded', weight: 0}, {id: 'layout-shift-elements', weight: 0}, + {id: 'layout-shifts', weight: 0}, {id: 'uses-passive-event-listeners', weight: 0}, {id: 'no-document-write', weight: 0}, {id: 'long-tasks', weight: 0}, diff --git a/core/config/metrics-to-audits.js b/core/config/metrics-to-audits.js index 79aa0c3bf90c..1e19c3c4979d 100644 --- a/core/config/metrics-to-audits.js +++ b/core/config/metrics-to-audits.js @@ -43,6 +43,7 @@ const tbtRelevantAudits = [ const clsRelevantAudits = [ 'layout-shift-elements', + 'layout-shifts', 'non-composited-animations', 'unsized-images', // 'preload-fonts', // actually in BP, rather than perf diff --git a/core/gather/gatherers/trace-elements.js b/core/gather/gatherers/trace-elements.js index 0c858d9ea09f..2586b33fde4a 100644 --- a/core/gather/gatherers/trace-elements.js +++ b/core/gather/gatherers/trace-elements.js @@ -27,6 +27,7 @@ import {ExecutionContext} from '../driver/execution-context.js'; /** @typedef {{nodeId: number, animations?: {name?: string, failureReasonsMask?: number, unsupportedProperties?: string[]}[], type?: string}} TraceElementData */ const MAX_LAYOUT_SHIFT_ELEMENTS = 15; +const MAX_LAYOUT_SHIFTS = 15; /** * @this {HTMLElement} @@ -64,11 +65,11 @@ class TraceElements extends BaseGatherer { } /** - * This function finds the top (up to 15) elements that contribute to the CLS score of the page. + * This function finds the top (up to 15) elements that shift on the page. * * @param {LH.Trace} trace * @param {LH.Gatherer.Context} context - * @return {Promise>} + * @return {Promise>} */ static async getTopLayoutShiftElements(trace, context) { const {impactByNodeId} = await CumulativeLayoutShift.request(trace, context); @@ -76,7 +77,77 @@ class TraceElements extends BaseGatherer { return [...impactByNodeId.entries()] .sort((a, b) => b[1] - a[1]) .slice(0, MAX_LAYOUT_SHIFT_ELEMENTS) - .map(([nodeId]) => ({nodeId})); + .map(([nodeId]) => nodeId); + } + + /** + * We want to a single representative node to represent the shift, so let's pick + * the one with the largest impact (size x distance moved). + * + * @param {LH.Artifacts.TraceImpactedNode[]} impactedNodes + * @param {Map} impactByNodeId + * @return {number|undefined} + */ + static getBiggestImpactNodeForShiftEvent(impactedNodes, impactByNodeId) { + let biggestImpactNodeId; + let biggestImpactNodeScore = Number.NEGATIVE_INFINITY; + for (const node of impactedNodes) { + const impactScore = impactByNodeId.get(node.node_id); + if (impactScore !== undefined && impactScore > biggestImpactNodeScore) { + biggestImpactNodeId = node.node_id; + biggestImpactNodeScore = impactScore; + } + } + return biggestImpactNodeId; + } + + /** + * This function finds the top (up to 15) layout shifts on the page, and returns + * the id of the largest impacted node of each shift, along with any related nodes + * that may have caused the shift. + * + * @param {LH.Trace} trace + * @param {LH.Gatherer.Context} context + * @return {Promise>} + */ + static async getTopLayoutShifts(trace, context) { + const {impactByNodeId} = await CumulativeLayoutShift.request(trace, context); + const clusters = trace.traceEngineResult?.data.LayoutShifts.clusters ?? []; + const layoutShiftEvents = clusters.flatMap(c => c.events); + + return layoutShiftEvents + .sort((a, b) => b.args.data.weighted_score_delta - a.args.data.weighted_score_delta) + .slice(0, MAX_LAYOUT_SHIFTS) + .flatMap(event => { + const nodeIds = []; + const biggestImpactedNodeId = + this.getBiggestImpactNodeForShiftEvent(event.args.data.impacted_nodes, impactByNodeId); + if (biggestImpactedNodeId !== undefined) { + nodeIds.push(biggestImpactedNodeId); + } + + const index = layoutShiftEvents.indexOf(event); + const rootCauses = trace.traceEngineResult?.rootCauses.layoutShifts[index]; + if (rootCauses) { + for (const cause of rootCauses.unsizedMedia) { + nodeIds.push(cause.node.backendNodeId); + } + } + + return nodeIds; + }); + } + + /** + * @param {LH.Trace} trace + * @param {LH.Gatherer.Context} context + * @return {Promise>} + */ + static async getTopLayoutShiftsNodeIds(trace, context) { + const resultOne = await this.getTopLayoutShiftElements(trace, context); + const resultTwo = await this.getTopLayoutShifts(trace, context); + const unique = [...new Set([...resultOne, ...resultTwo])]; + return unique.map(nodeId => ({nodeId})); } /** @@ -210,7 +281,7 @@ class TraceElements extends BaseGatherer { const {mainThreadEvents} = processedTrace; const lcpNodeData = await TraceElements.getLcpElement(trace, context); - const clsNodeData = await TraceElements.getTopLayoutShiftElements(trace, context); + const clsNodeData = await TraceElements.getTopLayoutShiftsNodeIds(trace, context); const animatedElementData = await this.getAnimatedElements(mainThreadEvents); const responsivenessElementData = await TraceElements.getResponsivenessElement(trace, context); diff --git a/core/gather/gatherers/trace.js b/core/gather/gatherers/trace.js index 27acebe3c1d5..a383194daaee 100644 --- a/core/gather/gatherers/trace.js +++ b/core/gather/gatherers/trace.js @@ -12,10 +12,11 @@ import BaseGatherer from '../base-gatherer.js'; import {TraceProcessor} from '../../lib/tracehouse/trace-processor.js'; +import * as TraceEngine from '../../lib/trace-engine.js'; class Trace extends BaseGatherer { - /** @type {LH.Trace} */ - _trace = {traceEvents: []}; + /** @type {LH.Trace|null} */ + _trace = null; static getDefaultTraceCategories() { return [ @@ -56,6 +57,9 @@ class Trace extends BaseGatherer { 'disabled-by-default-devtools.timeline.frame', 'latencyInfo', + // For CLS root causes. + 'disabled-by-default-devtools.timeline.invalidationTracking', + // Not used by Lighthouse (yet) but included for users that want JS samples when looking at // a trace collected by Lighthouse (e.g. "View Trace" workflow in DevTools) 'disabled-by-default-v8.cpu_profiler', @@ -63,12 +67,13 @@ class Trace extends BaseGatherer { } /** - * @param {LH.Gatherer.ProtocolSession} session + * @param {LH.Gatherer.Driver} driver * @return {Promise} */ - static async endTraceAndCollectEvents(session) { + static async endTraceAndCollectEvents(driver) { /** @type {Array} */ const traceEvents = []; + const session = driver.defaultSession; /** * Listener for when dataCollected events fire for each trace chunk @@ -79,14 +84,18 @@ class Trace extends BaseGatherer { }; session.on('Tracing.dataCollected', dataListener); - return new Promise((resolve, reject) => { + await new Promise((resolve, reject) => { session.once('Tracing.tracingComplete', _ => { session.off('Tracing.dataCollected', dataListener); - resolve({traceEvents}); + resolve({}); }); session.sendCommand('Tracing.end').catch(reject); }); + + const traceEngineResult = await Trace.runTraceEngine(driver, traceEvents); + + return {traceEvents, traceEngineResult}; } static symbol = Symbol('Trace'); @@ -103,6 +112,8 @@ class Trace extends BaseGatherer { async startSensitiveInstrumentation({driver, gatherMode, settings}) { const traceCategories = Trace.getDefaultTraceCategories() .concat(settings.additionalTraceCategories || []); + await driver.defaultSession.sendCommand('DOM.enable'); + await driver.defaultSession.sendCommand('CSS.enable'); await driver.defaultSession.sendCommand('Page.enable'); await driver.defaultSession.sendCommand('Tracing.start', { categories: traceCategories.join(','), @@ -119,10 +130,87 @@ class Trace extends BaseGatherer { * @param {LH.Gatherer.Context} passContext */ async stopSensitiveInstrumentation({driver}) { - this._trace = await Trace.endTraceAndCollectEvents(driver.defaultSession); + this._trace = await Trace.endTraceAndCollectEvents(driver); + } + + /** + * @param {LH.Gatherer.Driver} driver + * @param {LH.TraceEvent[]} traceEvents + */ + static async runTraceEngine(driver, traceEvents) { + const protocolInterface = { + /** @param {string} url */ + // eslint-disable-next-line no-unused-vars + getInitiatorForRequest(url) { + return null; + }, + /** @param {number[]} backendNodeIds */ + async pushNodesByBackendIdsToFrontend(backendNodeIds) { + await driver.defaultSession.sendCommand('DOM.getDocument', {depth: -1, pierce: true}); + const response = await driver.defaultSession.sendCommand( + 'DOM.pushNodesByBackendIdsToFrontend', {backendNodeIds}); + return response.nodeIds; + }, + /** @param {number} nodeId */ + async getNode(nodeId) { + const response = await driver.defaultSession.sendCommand('DOM.describeNode', {nodeId}); + // Why is this always zero? Uh, let's fix it here. + response.node.nodeId = nodeId; + return response.node; + }, + /** @param {number} nodeId */ + async getComputedStyleForNode(nodeId) { + try { + const response = await driver.defaultSession.sendCommand( + 'CSS.getComputedStyleForNode', {nodeId}); + return response.computedStyle; + } catch { + return []; + } + }, + /** @param {number} nodeId */ + async getMatchedStylesForNode(nodeId) { + try { + const response = await driver.defaultSession.sendCommand( + 'CSS.getMatchedStylesForNode', {nodeId}); + return response; + } catch { + return []; + } + }, + /** @param {string} url */ + // eslint-disable-next-line no-unused-vars + async fontFaceForSource(url) { + return null; + }, + }; + + const engine = TraceEngine.TraceProcessor.createWithAllHandlers(); + await engine.parse(traceEvents); + const data = engine.data; + + /** @type {LH.TraceEngineRootCauses} */ + const rootCauses = { + layoutShifts: {}, + }; + const rootCausesEngine = new TraceEngine.RootCauses(protocolInterface); + const layoutShiftEvents = data.LayoutShifts.clusters.flatMap(c => c.events); + for (const event of layoutShiftEvents) { + const r = await rootCausesEngine.layoutShifts.rootCausesForEvent(data, event); + rootCauses.layoutShifts[layoutShiftEvents.indexOf(event)] = r; + } + + return { + data, + rootCauses, + }; } getArtifact() { + if (!this._trace) { + throw new Error('unexpected null _trace'); + } + return this._trace; } } diff --git a/core/lib/trace-engine.js b/core/lib/trace-engine.js index 1812fdf9de94..c2d30d551a11 100644 --- a/core/lib/trace-engine.js +++ b/core/lib/trace-engine.js @@ -5,8 +5,11 @@ import {polyfillDOMRect} from './polyfill-dom-rect.js'; polyfillDOMRect(); +/** @type {import('../../types/trace-engine.js').TraceProcessor & typeof import('../../types/trace-engine.js').TraceProcessor} */ const TraceProcessor = TraceEngine.Processor.TraceProcessor; +/** @type {import('../../types/trace-engine.js').TraceHandlers} */ const TraceHandlers = TraceEngine.Handlers.ModelHandlers; +/** @type {import('../../types/trace-engine.js').RootCauses & typeof import('../../types/trace-engine.js').RootCauses} */ const RootCauses = TraceEngine.RootCauses.RootCauses.RootCauses; export { diff --git a/core/test/fixtures/user-flows/reports/sample-flow-result.json b/core/test/fixtures/user-flows/reports/sample-flow-result.json index 9a8b521e7062..7d37930a65db 100644 --- a/core/test/fixtures/user-flows/reports/sample-flow-result.json +++ b/core/test/fixtures/user-flows/reports/sample-flow-result.json @@ -1861,6 +1861,22 @@ }, "guidanceLevel": 2 }, + "layout-shifts": { + "id": "layout-shifts", + "title": "Avoid large layout shifts", + "description": "These are the largest layout shifts observed on the page. Some layout shifts may not be included in the CLS metric value due to [windowing](https://web.dev/articles/cls#what_is_cls). [Learn how to improve CLS](https://web.dev/articles/optimize-cls)", + "score": null, + "scoreDisplayMode": "notApplicable", + "metricSavings": { + "CLS": 0.002631263732910156 + }, + "details": { + "type": "table", + "headings": [], + "items": [] + }, + "guidanceLevel": 2 + }, "long-tasks": { "id": "long-tasks", "title": "Avoid long main-thread tasks", @@ -3952,6 +3968,7 @@ "acronym": "CLS", "relevantAudits": [ "layout-shift-elements", + "layout-shifts", "non-composited-animations", "unsized-images" ] @@ -4103,6 +4120,10 @@ "id": "layout-shift-elements", "weight": 0 }, + { + "id": "layout-shifts", + "weight": 0 + }, { "id": "uses-passive-event-listeners", "weight": 0 @@ -6264,61 +6285,61 @@ }, { "startTime": 147, - "name": "lh:audit:long-tasks", + "name": "lh:audit:layout-shifts", "duration": 1, "entryType": "measure" }, { "startTime": 148, - "name": "lh:audit:no-unload-listeners", + "name": "lh:audit:long-tasks", "duration": 1, "entryType": "measure" }, { "startTime": 149, - "name": "lh:audit:non-composited-animations", + "name": "lh:audit:no-unload-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 150, - "name": "lh:audit:unsized-images", + "name": "lh:audit:non-composited-animations", "duration": 1, "entryType": "measure" }, { "startTime": 151, - "name": "lh:audit:valid-source-maps", + "name": "lh:audit:unsized-images", "duration": 1, "entryType": "measure" }, { "startTime": 152, - "name": "lh:audit:prioritize-lcp-image", + "name": "lh:audit:valid-source-maps", "duration": 1, "entryType": "measure" }, { "startTime": 153, - "name": "lh:audit:csp-xss", + "name": "lh:audit:prioritize-lcp-image", "duration": 1, "entryType": "measure" }, { "startTime": 154, - "name": "lh:audit:script-treemap-data", + "name": "lh:audit:csp-xss", "duration": 1, "entryType": "measure" }, { "startTime": 155, - "name": "lh:computed:ModuleDuplication", + "name": "lh:audit:script-treemap-data", "duration": 1, "entryType": "measure" }, { "startTime": 156, - "name": "lh:computed:UnusedJavascriptSummary", + "name": "lh:computed:ModuleDuplication", "duration": 1, "entryType": "measure" }, @@ -6402,702 +6423,708 @@ }, { "startTime": 170, - "name": "lh:audit:pwa-cross-browser", + "name": "lh:computed:UnusedJavascriptSummary", "duration": 1, "entryType": "measure" }, { "startTime": 171, - "name": "lh:audit:pwa-page-transitions", + "name": "lh:audit:pwa-cross-browser", "duration": 1, "entryType": "measure" }, { "startTime": 172, - "name": "lh:audit:pwa-each-page-has-url", + "name": "lh:audit:pwa-page-transitions", "duration": 1, "entryType": "measure" }, { "startTime": 173, - "name": "lh:audit:accesskeys", + "name": "lh:audit:pwa-each-page-has-url", "duration": 1, "entryType": "measure" }, { "startTime": 174, - "name": "lh:audit:aria-allowed-attr", + "name": "lh:audit:accesskeys", "duration": 1, "entryType": "measure" }, { "startTime": 175, - "name": "lh:audit:aria-allowed-role", + "name": "lh:audit:aria-allowed-attr", "duration": 1, "entryType": "measure" }, { "startTime": 176, - "name": "lh:audit:aria-command-name", + "name": "lh:audit:aria-allowed-role", "duration": 1, "entryType": "measure" }, { "startTime": 177, - "name": "lh:audit:aria-dialog-name", + "name": "lh:audit:aria-command-name", "duration": 1, "entryType": "measure" }, { "startTime": 178, - "name": "lh:audit:aria-hidden-body", + "name": "lh:audit:aria-dialog-name", "duration": 1, "entryType": "measure" }, { "startTime": 179, - "name": "lh:audit:aria-hidden-focus", + "name": "lh:audit:aria-hidden-body", "duration": 1, "entryType": "measure" }, { "startTime": 180, - "name": "lh:audit:aria-input-field-name", + "name": "lh:audit:aria-hidden-focus", "duration": 1, "entryType": "measure" }, { "startTime": 181, - "name": "lh:audit:aria-meter-name", + "name": "lh:audit:aria-input-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 182, - "name": "lh:audit:aria-progressbar-name", + "name": "lh:audit:aria-meter-name", "duration": 1, "entryType": "measure" }, { "startTime": 183, - "name": "lh:audit:aria-required-attr", + "name": "lh:audit:aria-progressbar-name", "duration": 1, "entryType": "measure" }, { "startTime": 184, - "name": "lh:audit:aria-required-children", + "name": "lh:audit:aria-required-attr", "duration": 1, "entryType": "measure" }, { "startTime": 185, - "name": "lh:audit:aria-required-parent", + "name": "lh:audit:aria-required-children", "duration": 1, "entryType": "measure" }, { "startTime": 186, - "name": "lh:audit:aria-roles", + "name": "lh:audit:aria-required-parent", "duration": 1, "entryType": "measure" }, { "startTime": 187, - "name": "lh:audit:aria-text", + "name": "lh:audit:aria-roles", "duration": 1, "entryType": "measure" }, { "startTime": 188, - "name": "lh:audit:aria-toggle-field-name", + "name": "lh:audit:aria-text", "duration": 1, "entryType": "measure" }, { "startTime": 189, - "name": "lh:audit:aria-tooltip-name", + "name": "lh:audit:aria-toggle-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 190, - "name": "lh:audit:aria-treeitem-name", + "name": "lh:audit:aria-tooltip-name", "duration": 1, "entryType": "measure" }, { "startTime": 191, - "name": "lh:audit:aria-valid-attr-value", + "name": "lh:audit:aria-treeitem-name", "duration": 1, "entryType": "measure" }, { "startTime": 192, - "name": "lh:audit:aria-valid-attr", + "name": "lh:audit:aria-valid-attr-value", "duration": 1, "entryType": "measure" }, { "startTime": 193, - "name": "lh:audit:button-name", + "name": "lh:audit:aria-valid-attr", "duration": 1, "entryType": "measure" }, { "startTime": 194, - "name": "lh:audit:bypass", + "name": "lh:audit:button-name", "duration": 1, "entryType": "measure" }, { "startTime": 195, - "name": "lh:audit:color-contrast", + "name": "lh:audit:bypass", "duration": 1, "entryType": "measure" }, { "startTime": 196, - "name": "lh:audit:definition-list", + "name": "lh:audit:color-contrast", "duration": 1, "entryType": "measure" }, { "startTime": 197, - "name": "lh:audit:dlitem", + "name": "lh:audit:definition-list", "duration": 1, "entryType": "measure" }, { "startTime": 198, - "name": "lh:audit:document-title", + "name": "lh:audit:dlitem", "duration": 1, "entryType": "measure" }, { "startTime": 199, - "name": "lh:audit:duplicate-id-active", + "name": "lh:audit:document-title", "duration": 1, "entryType": "measure" }, { "startTime": 200, - "name": "lh:audit:duplicate-id-aria", + "name": "lh:audit:duplicate-id-active", "duration": 1, "entryType": "measure" }, { "startTime": 201, - "name": "lh:audit:empty-heading", + "name": "lh:audit:duplicate-id-aria", "duration": 1, "entryType": "measure" }, { "startTime": 202, - "name": "lh:audit:form-field-multiple-labels", + "name": "lh:audit:empty-heading", "duration": 1, "entryType": "measure" }, { "startTime": 203, - "name": "lh:audit:frame-title", + "name": "lh:audit:form-field-multiple-labels", "duration": 1, "entryType": "measure" }, { "startTime": 204, - "name": "lh:audit:heading-order", + "name": "lh:audit:frame-title", "duration": 1, "entryType": "measure" }, { "startTime": 205, - "name": "lh:audit:html-has-lang", + "name": "lh:audit:heading-order", "duration": 1, "entryType": "measure" }, { "startTime": 206, - "name": "lh:audit:html-lang-valid", + "name": "lh:audit:html-has-lang", "duration": 1, "entryType": "measure" }, { "startTime": 207, - "name": "lh:audit:html-xml-lang-mismatch", + "name": "lh:audit:html-lang-valid", "duration": 1, "entryType": "measure" }, { "startTime": 208, - "name": "lh:audit:identical-links-same-purpose", + "name": "lh:audit:html-xml-lang-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 209, - "name": "lh:audit:image-alt", + "name": "lh:audit:identical-links-same-purpose", "duration": 1, "entryType": "measure" }, { "startTime": 210, - "name": "lh:audit:image-redundant-alt", + "name": "lh:audit:image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 211, - "name": "lh:audit:input-button-name", + "name": "lh:audit:image-redundant-alt", "duration": 1, "entryType": "measure" }, { "startTime": 212, - "name": "lh:audit:input-image-alt", + "name": "lh:audit:input-button-name", "duration": 1, "entryType": "measure" }, { "startTime": 213, - "name": "lh:audit:label-content-name-mismatch", + "name": "lh:audit:input-image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 214, - "name": "lh:audit:label", + "name": "lh:audit:label-content-name-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 215, - "name": "lh:audit:landmark-one-main", + "name": "lh:audit:label", "duration": 1, "entryType": "measure" }, { "startTime": 216, - "name": "lh:audit:link-name", + "name": "lh:audit:landmark-one-main", "duration": 1, "entryType": "measure" }, { "startTime": 217, - "name": "lh:audit:link-in-text-block", + "name": "lh:audit:link-name", "duration": 1, "entryType": "measure" }, { "startTime": 218, - "name": "lh:audit:list", + "name": "lh:audit:link-in-text-block", "duration": 1, "entryType": "measure" }, { "startTime": 219, - "name": "lh:audit:listitem", + "name": "lh:audit:list", "duration": 1, "entryType": "measure" }, { "startTime": 220, - "name": "lh:audit:meta-refresh", + "name": "lh:audit:listitem", "duration": 1, "entryType": "measure" }, { "startTime": 221, - "name": "lh:audit:meta-viewport", + "name": "lh:audit:meta-refresh", "duration": 1, "entryType": "measure" }, { "startTime": 222, - "name": "lh:audit:object-alt", + "name": "lh:audit:meta-viewport", "duration": 1, "entryType": "measure" }, { "startTime": 223, - "name": "lh:audit:select-name", + "name": "lh:audit:object-alt", "duration": 1, "entryType": "measure" }, { "startTime": 224, - "name": "lh:audit:skip-link", + "name": "lh:audit:select-name", "duration": 1, "entryType": "measure" }, { "startTime": 225, - "name": "lh:audit:tabindex", + "name": "lh:audit:skip-link", "duration": 1, "entryType": "measure" }, { "startTime": 226, - "name": "lh:audit:table-duplicate-name", + "name": "lh:audit:tabindex", "duration": 1, "entryType": "measure" }, { "startTime": 227, - "name": "lh:audit:table-fake-caption", + "name": "lh:audit:table-duplicate-name", "duration": 1, "entryType": "measure" }, { "startTime": 228, - "name": "lh:audit:target-size", + "name": "lh:audit:table-fake-caption", "duration": 1, "entryType": "measure" }, { "startTime": 229, - "name": "lh:audit:td-has-header", + "name": "lh:audit:target-size", "duration": 1, "entryType": "measure" }, { "startTime": 230, - "name": "lh:audit:td-headers-attr", + "name": "lh:audit:td-has-header", "duration": 1, "entryType": "measure" }, { "startTime": 231, - "name": "lh:audit:th-has-data-cells", + "name": "lh:audit:td-headers-attr", "duration": 1, "entryType": "measure" }, { "startTime": 232, - "name": "lh:audit:valid-lang", + "name": "lh:audit:th-has-data-cells", "duration": 1, "entryType": "measure" }, { "startTime": 233, - "name": "lh:audit:video-caption", + "name": "lh:audit:valid-lang", "duration": 1, "entryType": "measure" }, { "startTime": 234, - "name": "lh:audit:custom-controls-labels", + "name": "lh:audit:video-caption", "duration": 1, "entryType": "measure" }, { "startTime": 235, - "name": "lh:audit:custom-controls-roles", + "name": "lh:audit:custom-controls-labels", "duration": 1, "entryType": "measure" }, { "startTime": 236, - "name": "lh:audit:focus-traps", + "name": "lh:audit:custom-controls-roles", "duration": 1, "entryType": "measure" }, { "startTime": 237, - "name": "lh:audit:focusable-controls", + "name": "lh:audit:focus-traps", "duration": 1, "entryType": "measure" }, { "startTime": 238, - "name": "lh:audit:interactive-element-affordance", + "name": "lh:audit:focusable-controls", "duration": 1, "entryType": "measure" }, { "startTime": 239, - "name": "lh:audit:logical-tab-order", + "name": "lh:audit:interactive-element-affordance", "duration": 1, "entryType": "measure" }, { "startTime": 240, - "name": "lh:audit:managed-focus", + "name": "lh:audit:logical-tab-order", "duration": 1, "entryType": "measure" }, { "startTime": 241, - "name": "lh:audit:offscreen-content-hidden", + "name": "lh:audit:managed-focus", "duration": 1, "entryType": "measure" }, { "startTime": 242, - "name": "lh:audit:use-landmarks", + "name": "lh:audit:offscreen-content-hidden", "duration": 1, "entryType": "measure" }, { "startTime": 243, - "name": "lh:audit:visual-order-follows-dom", + "name": "lh:audit:use-landmarks", "duration": 1, "entryType": "measure" }, { "startTime": 244, - "name": "lh:audit:uses-long-cache-ttl", + "name": "lh:audit:visual-order-follows-dom", "duration": 1, "entryType": "measure" }, { "startTime": 245, - "name": "lh:audit:total-byte-weight", + "name": "lh:audit:uses-long-cache-ttl", "duration": 1, "entryType": "measure" }, { "startTime": 246, - "name": "lh:audit:offscreen-images", + "name": "lh:audit:total-byte-weight", "duration": 1, "entryType": "measure" }, { "startTime": 247, - "name": "lh:audit:render-blocking-resources", + "name": "lh:audit:offscreen-images", "duration": 1, "entryType": "measure" }, { "startTime": 248, - "name": "lh:computed:UnusedCSS", + "name": "lh:audit:render-blocking-resources", "duration": 1, "entryType": "measure" }, { "startTime": 249, - "name": "lh:computed:FirstContentfulPaint", + "name": "lh:computed:UnusedCSS", "duration": 1, "entryType": "measure" }, { "startTime": 250, - "name": "lh:audit:unminified-css", + "name": "lh:computed:FirstContentfulPaint", "duration": 1, "entryType": "measure" }, { "startTime": 251, - "name": "lh:audit:unminified-javascript", + "name": "lh:audit:unminified-css", "duration": 1, "entryType": "measure" }, { "startTime": 252, - "name": "lh:audit:unused-css-rules", + "name": "lh:audit:unminified-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 253, - "name": "lh:audit:unused-javascript", + "name": "lh:audit:unused-css-rules", "duration": 1, "entryType": "measure" }, { "startTime": 254, - "name": "lh:audit:modern-image-formats", + "name": "lh:audit:unused-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 255, - "name": "lh:audit:uses-optimized-images", + "name": "lh:audit:modern-image-formats", "duration": 1, "entryType": "measure" }, { "startTime": 256, - "name": "lh:audit:uses-text-compression", + "name": "lh:audit:uses-optimized-images", "duration": 1, "entryType": "measure" }, { "startTime": 257, - "name": "lh:audit:uses-responsive-images", + "name": "lh:audit:uses-text-compression", "duration": 1, "entryType": "measure" }, { "startTime": 258, - "name": "lh:computed:ImageRecords", + "name": "lh:audit:uses-responsive-images", "duration": 1, "entryType": "measure" }, { "startTime": 259, - "name": "lh:audit:efficient-animated-content", + "name": "lh:computed:ImageRecords", "duration": 1, "entryType": "measure" }, { "startTime": 260, - "name": "lh:audit:duplicated-javascript", + "name": "lh:audit:efficient-animated-content", "duration": 1, "entryType": "measure" }, { "startTime": 261, - "name": "lh:audit:legacy-javascript", + "name": "lh:audit:duplicated-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 262, - "name": "lh:audit:doctype", + "name": "lh:audit:legacy-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 263, - "name": "lh:audit:charset", + "name": "lh:audit:doctype", "duration": 1, "entryType": "measure" }, { "startTime": 264, - "name": "lh:audit:dom-size", + "name": "lh:audit:charset", "duration": 1, "entryType": "measure" }, { "startTime": 265, - "name": "lh:audit:geolocation-on-start", + "name": "lh:audit:dom-size", "duration": 1, "entryType": "measure" }, { "startTime": 266, - "name": "lh:audit:inspector-issues", + "name": "lh:audit:geolocation-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 267, - "name": "lh:audit:no-document-write", + "name": "lh:audit:inspector-issues", "duration": 1, "entryType": "measure" }, { "startTime": 268, - "name": "lh:audit:js-libraries", + "name": "lh:audit:no-document-write", "duration": 1, "entryType": "measure" }, { "startTime": 269, - "name": "lh:audit:notification-on-start", + "name": "lh:audit:js-libraries", "duration": 1, "entryType": "measure" }, { "startTime": 270, - "name": "lh:audit:paste-preventing-inputs", + "name": "lh:audit:notification-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 271, - "name": "lh:audit:uses-passive-event-listeners", + "name": "lh:audit:paste-preventing-inputs", "duration": 1, "entryType": "measure" }, { "startTime": 272, - "name": "lh:audit:meta-description", + "name": "lh:audit:uses-passive-event-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 273, - "name": "lh:audit:http-status-code", + "name": "lh:audit:meta-description", "duration": 1, "entryType": "measure" }, { "startTime": 274, - "name": "lh:audit:font-size", + "name": "lh:audit:http-status-code", "duration": 1, "entryType": "measure" }, { "startTime": 275, - "name": "lh:audit:link-text", + "name": "lh:audit:font-size", "duration": 1, "entryType": "measure" }, { "startTime": 276, - "name": "lh:audit:crawlable-anchors", + "name": "lh:audit:link-text", "duration": 1, "entryType": "measure" }, { "startTime": 277, - "name": "lh:audit:is-crawlable", + "name": "lh:audit:crawlable-anchors", "duration": 1, "entryType": "measure" }, { "startTime": 278, - "name": "lh:audit:robots-txt", + "name": "lh:audit:is-crawlable", "duration": 1, "entryType": "measure" }, { "startTime": 279, - "name": "lh:audit:tap-targets", + "name": "lh:audit:robots-txt", "duration": 1, "entryType": "measure" }, { "startTime": 280, - "name": "lh:audit:hreflang", + "name": "lh:audit:tap-targets", "duration": 1, "entryType": "measure" }, { "startTime": 281, - "name": "lh:audit:plugins", + "name": "lh:audit:hreflang", "duration": 1, "entryType": "measure" }, { "startTime": 282, - "name": "lh:audit:canonical", + "name": "lh:audit:plugins", "duration": 1, "entryType": "measure" }, { "startTime": 283, - "name": "lh:audit:structured-data", + "name": "lh:audit:canonical", "duration": 1, "entryType": "measure" }, { "startTime": 284, - "name": "lh:audit:bf-cache", + "name": "lh:audit:structured-data", "duration": 1, "entryType": "measure" }, { "startTime": 285, + "name": "lh:audit:bf-cache", + "duration": 1, + "entryType": "measure" + }, + { + "startTime": 286, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 286 + "total": 287 }, "i18n": { "rendererFormattedStrings": { @@ -7634,6 +7661,12 @@ "core/audits/layout-shift-elements.js | columnContribution": [ "audits[layout-shift-elements].details.headings[1].label" ], + "core/audits/layout-shifts.js | title": [ + "audits[layout-shifts].title" + ], + "core/audits/layout-shifts.js | description": [ + "audits[layout-shifts].description" + ], "core/audits/long-tasks.js | title": [ "audits[long-tasks].title" ], @@ -9915,6 +9948,22 @@ }, "guidanceLevel": 2 }, + "layout-shifts": { + "id": "layout-shifts", + "title": "Avoid large layout shifts", + "description": "These are the largest layout shifts observed on the page. Some layout shifts may not be included in the CLS metric value due to [windowing](https://web.dev/articles/cls#what_is_cls). [Learn how to improve CLS](https://web.dev/articles/optimize-cls)", + "score": null, + "scoreDisplayMode": "notApplicable", + "metricSavings": { + "CLS": 0.13125 + }, + "details": { + "type": "table", + "headings": [], + "items": [] + }, + "guidanceLevel": 2 + }, "long-tasks": { "id": "long-tasks", "title": "Avoid long main-thread tasks", @@ -10995,6 +11044,7 @@ "acronym": "CLS", "relevantAudits": [ "layout-shift-elements", + "layout-shifts", "non-composited-animations", "unsized-images" ] @@ -11080,6 +11130,10 @@ "id": "layout-shift-elements", "weight": 0 }, + { + "id": "layout-shifts", + "weight": 0 + }, { "id": "uses-passive-event-listeners", "weight": 0 @@ -11906,49 +11960,49 @@ }, { "startTime": 72, - "name": "lh:audit:long-tasks", + "name": "lh:audit:layout-shifts", "duration": 1, "entryType": "measure" }, { "startTime": 73, - "name": "lh:audit:no-unload-listeners", + "name": "lh:audit:long-tasks", "duration": 1, "entryType": "measure" }, { "startTime": 74, - "name": "lh:audit:non-composited-animations", + "name": "lh:audit:no-unload-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 75, - "name": "lh:audit:unsized-images", + "name": "lh:audit:non-composited-animations", "duration": 1, "entryType": "measure" }, { "startTime": 76, - "name": "lh:audit:valid-source-maps", + "name": "lh:audit:unsized-images", "duration": 1, "entryType": "measure" }, { "startTime": 77, - "name": "lh:audit:script-treemap-data", + "name": "lh:audit:valid-source-maps", "duration": 1, "entryType": "measure" }, { "startTime": 78, - "name": "lh:computed:ModuleDuplication", + "name": "lh:audit:script-treemap-data", "duration": 1, "entryType": "measure" }, { "startTime": 79, - "name": "lh:computed:UnusedJavascriptSummary", + "name": "lh:computed:ModuleDuplication", "duration": 1, "entryType": "measure" }, @@ -11966,138 +12020,144 @@ }, { "startTime": 82, - "name": "lh:audit:uses-long-cache-ttl", + "name": "lh:computed:UnusedJavascriptSummary", "duration": 1, "entryType": "measure" }, { "startTime": 83, - "name": "lh:audit:total-byte-weight", + "name": "lh:audit:uses-long-cache-ttl", "duration": 1, "entryType": "measure" }, { "startTime": 84, - "name": "lh:audit:unminified-css", + "name": "lh:audit:total-byte-weight", "duration": 1, "entryType": "measure" }, { "startTime": 85, - "name": "lh:computed:LoadSimulator", + "name": "lh:audit:unminified-css", "duration": 1, "entryType": "measure" }, { "startTime": 86, - "name": "lh:audit:unminified-javascript", + "name": "lh:computed:LoadSimulator", "duration": 1, "entryType": "measure" }, { "startTime": 87, - "name": "lh:audit:unused-css-rules", + "name": "lh:audit:unminified-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 88, - "name": "lh:computed:UnusedCSS", + "name": "lh:audit:unused-css-rules", "duration": 1, "entryType": "measure" }, { "startTime": 89, - "name": "lh:audit:unused-javascript", + "name": "lh:computed:UnusedCSS", "duration": 1, "entryType": "measure" }, { "startTime": 90, - "name": "lh:audit:modern-image-formats", + "name": "lh:audit:unused-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 91, - "name": "lh:audit:uses-optimized-images", + "name": "lh:audit:modern-image-formats", "duration": 1, "entryType": "measure" }, { "startTime": 92, - "name": "lh:audit:uses-text-compression", + "name": "lh:audit:uses-optimized-images", "duration": 1, "entryType": "measure" }, { "startTime": 93, - "name": "lh:audit:uses-responsive-images", + "name": "lh:audit:uses-text-compression", "duration": 1, "entryType": "measure" }, { "startTime": 94, - "name": "lh:computed:ImageRecords", + "name": "lh:audit:uses-responsive-images", "duration": 1, "entryType": "measure" }, { "startTime": 95, - "name": "lh:audit:efficient-animated-content", + "name": "lh:computed:ImageRecords", "duration": 1, "entryType": "measure" }, { "startTime": 96, - "name": "lh:audit:duplicated-javascript", + "name": "lh:audit:efficient-animated-content", "duration": 1, "entryType": "measure" }, { "startTime": 97, - "name": "lh:audit:legacy-javascript", + "name": "lh:audit:duplicated-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 98, - "name": "lh:audit:inspector-issues", + "name": "lh:audit:legacy-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 99, - "name": "lh:audit:no-document-write", + "name": "lh:audit:inspector-issues", "duration": 1, "entryType": "measure" }, { "startTime": 100, - "name": "lh:audit:uses-passive-event-listeners", + "name": "lh:audit:no-document-write", "duration": 1, "entryType": "measure" }, { "startTime": 101, - "name": "lh:audit:work-during-interaction", + "name": "lh:audit:uses-passive-event-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 102, - "name": "lh:audit:bf-cache", + "name": "lh:audit:work-during-interaction", "duration": 1, "entryType": "measure" }, { "startTime": 103, + "name": "lh:audit:bf-cache", + "duration": 1, + "entryType": "measure" + }, + { + "startTime": 104, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 104 + "total": 105 }, "i18n": { "rendererFormattedStrings": { @@ -12428,6 +12488,12 @@ "core/audits/layout-shift-elements.js | columnContribution": [ "audits[layout-shift-elements].details.headings[1].label" ], + "core/audits/layout-shifts.js | title": [ + "audits[layout-shifts].title" + ], + "core/audits/layout-shifts.js | description": [ + "audits[layout-shifts].description" + ], "core/audits/long-tasks.js | title": [ "audits[long-tasks].title" ], @@ -19668,6 +19734,22 @@ }, "guidanceLevel": 2 }, + "layout-shifts": { + "id": "layout-shifts", + "title": "Avoid large layout shifts", + "description": "These are the largest layout shifts observed on the page. Some layout shifts may not be included in the CLS metric value due to [windowing](https://web.dev/articles/cls#what_is_cls). [Learn how to improve CLS](https://web.dev/articles/optimize-cls)", + "score": null, + "scoreDisplayMode": "notApplicable", + "metricSavings": { + "CLS": 0 + }, + "details": { + "type": "table", + "headings": [], + "items": [] + }, + "guidanceLevel": 2 + }, "long-tasks": { "id": "long-tasks", "title": "Avoid long main-thread tasks", @@ -21929,6 +22011,7 @@ "acronym": "CLS", "relevantAudits": [ "layout-shift-elements", + "layout-shifts", "non-composited-animations", "unsized-images" ] @@ -22080,6 +22163,10 @@ "id": "layout-shift-elements", "weight": 0 }, + { + "id": "layout-shifts", + "weight": 0 + }, { "id": "uses-passive-event-listeners", "weight": 0 @@ -24221,61 +24308,61 @@ }, { "startTime": 145, - "name": "lh:audit:long-tasks", + "name": "lh:audit:layout-shifts", "duration": 1, "entryType": "measure" }, { "startTime": 146, - "name": "lh:audit:no-unload-listeners", + "name": "lh:audit:long-tasks", "duration": 1, "entryType": "measure" }, { "startTime": 147, - "name": "lh:audit:non-composited-animations", + "name": "lh:audit:no-unload-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 148, - "name": "lh:audit:unsized-images", + "name": "lh:audit:non-composited-animations", "duration": 1, "entryType": "measure" }, { "startTime": 149, - "name": "lh:audit:valid-source-maps", + "name": "lh:audit:unsized-images", "duration": 1, "entryType": "measure" }, { "startTime": 150, - "name": "lh:audit:prioritize-lcp-image", + "name": "lh:audit:valid-source-maps", "duration": 1, "entryType": "measure" }, { "startTime": 151, - "name": "lh:audit:csp-xss", + "name": "lh:audit:prioritize-lcp-image", "duration": 1, "entryType": "measure" }, { "startTime": 152, - "name": "lh:audit:script-treemap-data", + "name": "lh:audit:csp-xss", "duration": 1, "entryType": "measure" }, { "startTime": 153, - "name": "lh:computed:ModuleDuplication", + "name": "lh:audit:script-treemap-data", "duration": 1, "entryType": "measure" }, { "startTime": 154, - "name": "lh:computed:UnusedJavascriptSummary", + "name": "lh:computed:ModuleDuplication", "duration": 1, "entryType": "measure" }, @@ -24353,702 +24440,708 @@ }, { "startTime": 167, - "name": "lh:audit:pwa-cross-browser", + "name": "lh:computed:UnusedJavascriptSummary", "duration": 1, "entryType": "measure" }, { "startTime": 168, - "name": "lh:audit:pwa-page-transitions", + "name": "lh:audit:pwa-cross-browser", "duration": 1, "entryType": "measure" }, { "startTime": 169, - "name": "lh:audit:pwa-each-page-has-url", + "name": "lh:audit:pwa-page-transitions", "duration": 1, "entryType": "measure" }, { "startTime": 170, - "name": "lh:audit:accesskeys", + "name": "lh:audit:pwa-each-page-has-url", "duration": 1, "entryType": "measure" }, { "startTime": 171, - "name": "lh:audit:aria-allowed-attr", + "name": "lh:audit:accesskeys", "duration": 1, "entryType": "measure" }, { "startTime": 172, - "name": "lh:audit:aria-allowed-role", + "name": "lh:audit:aria-allowed-attr", "duration": 1, "entryType": "measure" }, { "startTime": 173, - "name": "lh:audit:aria-command-name", + "name": "lh:audit:aria-allowed-role", "duration": 1, "entryType": "measure" }, { "startTime": 174, - "name": "lh:audit:aria-dialog-name", + "name": "lh:audit:aria-command-name", "duration": 1, "entryType": "measure" }, { "startTime": 175, - "name": "lh:audit:aria-hidden-body", + "name": "lh:audit:aria-dialog-name", "duration": 1, "entryType": "measure" }, { "startTime": 176, - "name": "lh:audit:aria-hidden-focus", + "name": "lh:audit:aria-hidden-body", "duration": 1, "entryType": "measure" }, { "startTime": 177, - "name": "lh:audit:aria-input-field-name", + "name": "lh:audit:aria-hidden-focus", "duration": 1, "entryType": "measure" }, { "startTime": 178, - "name": "lh:audit:aria-meter-name", + "name": "lh:audit:aria-input-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 179, - "name": "lh:audit:aria-progressbar-name", + "name": "lh:audit:aria-meter-name", "duration": 1, "entryType": "measure" }, { "startTime": 180, - "name": "lh:audit:aria-required-attr", + "name": "lh:audit:aria-progressbar-name", "duration": 1, "entryType": "measure" }, { "startTime": 181, - "name": "lh:audit:aria-required-children", + "name": "lh:audit:aria-required-attr", "duration": 1, "entryType": "measure" }, { "startTime": 182, - "name": "lh:audit:aria-required-parent", + "name": "lh:audit:aria-required-children", "duration": 1, "entryType": "measure" }, { "startTime": 183, - "name": "lh:audit:aria-roles", + "name": "lh:audit:aria-required-parent", "duration": 1, "entryType": "measure" }, { "startTime": 184, - "name": "lh:audit:aria-text", + "name": "lh:audit:aria-roles", "duration": 1, "entryType": "measure" }, { "startTime": 185, - "name": "lh:audit:aria-toggle-field-name", + "name": "lh:audit:aria-text", "duration": 1, "entryType": "measure" }, { "startTime": 186, - "name": "lh:audit:aria-tooltip-name", + "name": "lh:audit:aria-toggle-field-name", "duration": 1, "entryType": "measure" }, { "startTime": 187, - "name": "lh:audit:aria-treeitem-name", + "name": "lh:audit:aria-tooltip-name", "duration": 1, "entryType": "measure" }, { "startTime": 188, - "name": "lh:audit:aria-valid-attr-value", + "name": "lh:audit:aria-treeitem-name", "duration": 1, "entryType": "measure" }, { "startTime": 189, - "name": "lh:audit:aria-valid-attr", + "name": "lh:audit:aria-valid-attr-value", "duration": 1, "entryType": "measure" }, { "startTime": 190, - "name": "lh:audit:button-name", + "name": "lh:audit:aria-valid-attr", "duration": 1, "entryType": "measure" }, { "startTime": 191, - "name": "lh:audit:bypass", + "name": "lh:audit:button-name", "duration": 1, "entryType": "measure" }, { "startTime": 192, - "name": "lh:audit:color-contrast", + "name": "lh:audit:bypass", "duration": 1, "entryType": "measure" }, { "startTime": 193, - "name": "lh:audit:definition-list", + "name": "lh:audit:color-contrast", "duration": 1, "entryType": "measure" }, { "startTime": 194, - "name": "lh:audit:dlitem", + "name": "lh:audit:definition-list", "duration": 1, "entryType": "measure" }, { "startTime": 195, - "name": "lh:audit:document-title", + "name": "lh:audit:dlitem", "duration": 1, "entryType": "measure" }, { "startTime": 196, - "name": "lh:audit:duplicate-id-active", + "name": "lh:audit:document-title", "duration": 1, "entryType": "measure" }, { "startTime": 197, - "name": "lh:audit:duplicate-id-aria", + "name": "lh:audit:duplicate-id-active", "duration": 1, "entryType": "measure" }, { "startTime": 198, - "name": "lh:audit:empty-heading", + "name": "lh:audit:duplicate-id-aria", "duration": 1, "entryType": "measure" }, { "startTime": 199, - "name": "lh:audit:form-field-multiple-labels", + "name": "lh:audit:empty-heading", "duration": 1, "entryType": "measure" }, { "startTime": 200, - "name": "lh:audit:frame-title", + "name": "lh:audit:form-field-multiple-labels", "duration": 1, "entryType": "measure" }, { "startTime": 201, - "name": "lh:audit:heading-order", + "name": "lh:audit:frame-title", "duration": 1, "entryType": "measure" }, { "startTime": 202, - "name": "lh:audit:html-has-lang", + "name": "lh:audit:heading-order", "duration": 1, "entryType": "measure" }, { "startTime": 203, - "name": "lh:audit:html-lang-valid", + "name": "lh:audit:html-has-lang", "duration": 1, "entryType": "measure" }, { "startTime": 204, - "name": "lh:audit:html-xml-lang-mismatch", + "name": "lh:audit:html-lang-valid", "duration": 1, "entryType": "measure" }, { "startTime": 205, - "name": "lh:audit:identical-links-same-purpose", + "name": "lh:audit:html-xml-lang-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 206, - "name": "lh:audit:image-alt", + "name": "lh:audit:identical-links-same-purpose", "duration": 1, "entryType": "measure" }, { "startTime": 207, - "name": "lh:audit:image-redundant-alt", + "name": "lh:audit:image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 208, - "name": "lh:audit:input-button-name", + "name": "lh:audit:image-redundant-alt", "duration": 1, "entryType": "measure" }, { "startTime": 209, - "name": "lh:audit:input-image-alt", + "name": "lh:audit:input-button-name", "duration": 1, "entryType": "measure" }, { "startTime": 210, - "name": "lh:audit:label-content-name-mismatch", + "name": "lh:audit:input-image-alt", "duration": 1, "entryType": "measure" }, { "startTime": 211, - "name": "lh:audit:label", + "name": "lh:audit:label-content-name-mismatch", "duration": 1, "entryType": "measure" }, { "startTime": 212, - "name": "lh:audit:landmark-one-main", + "name": "lh:audit:label", "duration": 1, "entryType": "measure" }, { "startTime": 213, - "name": "lh:audit:link-name", + "name": "lh:audit:landmark-one-main", "duration": 1, "entryType": "measure" }, { "startTime": 214, - "name": "lh:audit:link-in-text-block", + "name": "lh:audit:link-name", "duration": 1, "entryType": "measure" }, { "startTime": 215, - "name": "lh:audit:list", + "name": "lh:audit:link-in-text-block", "duration": 1, "entryType": "measure" }, { "startTime": 216, - "name": "lh:audit:listitem", + "name": "lh:audit:list", "duration": 1, "entryType": "measure" }, { "startTime": 217, - "name": "lh:audit:meta-refresh", + "name": "lh:audit:listitem", "duration": 1, "entryType": "measure" }, { "startTime": 218, - "name": "lh:audit:meta-viewport", + "name": "lh:audit:meta-refresh", "duration": 1, "entryType": "measure" }, { "startTime": 219, - "name": "lh:audit:object-alt", + "name": "lh:audit:meta-viewport", "duration": 1, "entryType": "measure" }, { "startTime": 220, - "name": "lh:audit:select-name", + "name": "lh:audit:object-alt", "duration": 1, "entryType": "measure" }, { "startTime": 221, - "name": "lh:audit:skip-link", + "name": "lh:audit:select-name", "duration": 1, "entryType": "measure" }, { "startTime": 222, - "name": "lh:audit:tabindex", + "name": "lh:audit:skip-link", "duration": 1, "entryType": "measure" }, { "startTime": 223, - "name": "lh:audit:table-duplicate-name", + "name": "lh:audit:tabindex", "duration": 1, "entryType": "measure" }, { "startTime": 224, - "name": "lh:audit:table-fake-caption", + "name": "lh:audit:table-duplicate-name", "duration": 1, "entryType": "measure" }, { "startTime": 225, - "name": "lh:audit:target-size", + "name": "lh:audit:table-fake-caption", "duration": 1, "entryType": "measure" }, { "startTime": 226, - "name": "lh:audit:td-has-header", + "name": "lh:audit:target-size", "duration": 1, "entryType": "measure" }, { "startTime": 227, - "name": "lh:audit:td-headers-attr", + "name": "lh:audit:td-has-header", "duration": 1, "entryType": "measure" }, { "startTime": 228, - "name": "lh:audit:th-has-data-cells", + "name": "lh:audit:td-headers-attr", "duration": 1, "entryType": "measure" }, { "startTime": 229, - "name": "lh:audit:valid-lang", + "name": "lh:audit:th-has-data-cells", "duration": 1, "entryType": "measure" }, { "startTime": 230, - "name": "lh:audit:video-caption", + "name": "lh:audit:valid-lang", "duration": 1, "entryType": "measure" }, { "startTime": 231, - "name": "lh:audit:custom-controls-labels", + "name": "lh:audit:video-caption", "duration": 1, "entryType": "measure" }, { "startTime": 232, - "name": "lh:audit:custom-controls-roles", + "name": "lh:audit:custom-controls-labels", "duration": 1, "entryType": "measure" }, { "startTime": 233, - "name": "lh:audit:focus-traps", + "name": "lh:audit:custom-controls-roles", "duration": 1, "entryType": "measure" }, { "startTime": 234, - "name": "lh:audit:focusable-controls", + "name": "lh:audit:focus-traps", "duration": 1, "entryType": "measure" }, { "startTime": 235, - "name": "lh:audit:interactive-element-affordance", + "name": "lh:audit:focusable-controls", "duration": 1, "entryType": "measure" }, { "startTime": 236, - "name": "lh:audit:logical-tab-order", + "name": "lh:audit:interactive-element-affordance", "duration": 1, "entryType": "measure" }, { "startTime": 237, - "name": "lh:audit:managed-focus", + "name": "lh:audit:logical-tab-order", "duration": 1, "entryType": "measure" }, { "startTime": 238, - "name": "lh:audit:offscreen-content-hidden", + "name": "lh:audit:managed-focus", "duration": 1, "entryType": "measure" }, { "startTime": 239, - "name": "lh:audit:use-landmarks", + "name": "lh:audit:offscreen-content-hidden", "duration": 1, "entryType": "measure" }, { "startTime": 240, - "name": "lh:audit:visual-order-follows-dom", + "name": "lh:audit:use-landmarks", "duration": 1, "entryType": "measure" }, { "startTime": 241, - "name": "lh:audit:uses-long-cache-ttl", + "name": "lh:audit:visual-order-follows-dom", "duration": 1, "entryType": "measure" }, { "startTime": 242, - "name": "lh:audit:total-byte-weight", + "name": "lh:audit:uses-long-cache-ttl", "duration": 1, "entryType": "measure" }, { "startTime": 243, - "name": "lh:audit:offscreen-images", + "name": "lh:audit:total-byte-weight", "duration": 1, "entryType": "measure" }, { "startTime": 244, - "name": "lh:audit:render-blocking-resources", + "name": "lh:audit:offscreen-images", "duration": 1, "entryType": "measure" }, { "startTime": 245, - "name": "lh:computed:UnusedCSS", + "name": "lh:audit:render-blocking-resources", "duration": 1, "entryType": "measure" }, { "startTime": 246, - "name": "lh:computed:FirstContentfulPaint", + "name": "lh:computed:UnusedCSS", "duration": 1, "entryType": "measure" }, { "startTime": 247, - "name": "lh:audit:unminified-css", + "name": "lh:computed:FirstContentfulPaint", "duration": 1, "entryType": "measure" }, { "startTime": 248, - "name": "lh:audit:unminified-javascript", + "name": "lh:audit:unminified-css", "duration": 1, "entryType": "measure" }, { "startTime": 249, - "name": "lh:audit:unused-css-rules", + "name": "lh:audit:unminified-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 250, - "name": "lh:audit:unused-javascript", + "name": "lh:audit:unused-css-rules", "duration": 1, "entryType": "measure" }, { "startTime": 251, - "name": "lh:audit:modern-image-formats", + "name": "lh:audit:unused-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 252, - "name": "lh:audit:uses-optimized-images", + "name": "lh:audit:modern-image-formats", "duration": 1, "entryType": "measure" }, { "startTime": 253, - "name": "lh:audit:uses-text-compression", + "name": "lh:audit:uses-optimized-images", "duration": 1, "entryType": "measure" }, { "startTime": 254, - "name": "lh:audit:uses-responsive-images", + "name": "lh:audit:uses-text-compression", "duration": 1, "entryType": "measure" }, { "startTime": 255, - "name": "lh:computed:ImageRecords", + "name": "lh:audit:uses-responsive-images", "duration": 1, "entryType": "measure" }, { "startTime": 256, - "name": "lh:audit:efficient-animated-content", + "name": "lh:computed:ImageRecords", "duration": 1, "entryType": "measure" }, { "startTime": 257, - "name": "lh:audit:duplicated-javascript", + "name": "lh:audit:efficient-animated-content", "duration": 1, "entryType": "measure" }, { "startTime": 258, - "name": "lh:audit:legacy-javascript", + "name": "lh:audit:duplicated-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 259, - "name": "lh:audit:doctype", + "name": "lh:audit:legacy-javascript", "duration": 1, "entryType": "measure" }, { "startTime": 260, - "name": "lh:audit:charset", + "name": "lh:audit:doctype", "duration": 1, "entryType": "measure" }, { "startTime": 261, - "name": "lh:audit:dom-size", + "name": "lh:audit:charset", "duration": 1, "entryType": "measure" }, { "startTime": 262, - "name": "lh:audit:geolocation-on-start", + "name": "lh:audit:dom-size", "duration": 1, "entryType": "measure" }, { "startTime": 263, - "name": "lh:audit:inspector-issues", + "name": "lh:audit:geolocation-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 264, - "name": "lh:audit:no-document-write", + "name": "lh:audit:inspector-issues", "duration": 1, "entryType": "measure" }, { "startTime": 265, - "name": "lh:audit:js-libraries", + "name": "lh:audit:no-document-write", "duration": 1, "entryType": "measure" }, { "startTime": 266, - "name": "lh:audit:notification-on-start", + "name": "lh:audit:js-libraries", "duration": 1, "entryType": "measure" }, { "startTime": 267, - "name": "lh:audit:paste-preventing-inputs", + "name": "lh:audit:notification-on-start", "duration": 1, "entryType": "measure" }, { "startTime": 268, - "name": "lh:audit:uses-passive-event-listeners", + "name": "lh:audit:paste-preventing-inputs", "duration": 1, "entryType": "measure" }, { "startTime": 269, - "name": "lh:audit:meta-description", + "name": "lh:audit:uses-passive-event-listeners", "duration": 1, "entryType": "measure" }, { "startTime": 270, - "name": "lh:audit:http-status-code", + "name": "lh:audit:meta-description", "duration": 1, "entryType": "measure" }, { "startTime": 271, - "name": "lh:audit:font-size", + "name": "lh:audit:http-status-code", "duration": 1, "entryType": "measure" }, { "startTime": 272, - "name": "lh:audit:link-text", + "name": "lh:audit:font-size", "duration": 1, "entryType": "measure" }, { "startTime": 273, - "name": "lh:audit:crawlable-anchors", + "name": "lh:audit:link-text", "duration": 1, "entryType": "measure" }, { "startTime": 274, - "name": "lh:audit:is-crawlable", + "name": "lh:audit:crawlable-anchors", "duration": 1, "entryType": "measure" }, { "startTime": 275, - "name": "lh:audit:robots-txt", + "name": "lh:audit:is-crawlable", "duration": 1, "entryType": "measure" }, { "startTime": 276, - "name": "lh:audit:tap-targets", + "name": "lh:audit:robots-txt", "duration": 1, "entryType": "measure" }, { "startTime": 277, - "name": "lh:audit:hreflang", + "name": "lh:audit:tap-targets", "duration": 1, "entryType": "measure" }, { "startTime": 278, - "name": "lh:audit:plugins", + "name": "lh:audit:hreflang", "duration": 1, "entryType": "measure" }, { "startTime": 279, - "name": "lh:audit:canonical", + "name": "lh:audit:plugins", "duration": 1, "entryType": "measure" }, { "startTime": 280, - "name": "lh:audit:structured-data", + "name": "lh:audit:canonical", "duration": 1, "entryType": "measure" }, { "startTime": 281, - "name": "lh:audit:bf-cache", + "name": "lh:audit:structured-data", "duration": 1, "entryType": "measure" }, { "startTime": 282, + "name": "lh:audit:bf-cache", + "duration": 1, + "entryType": "measure" + }, + { + "startTime": 283, "name": "lh:runner:generate", "duration": 1, "entryType": "measure" } ], - "total": 283 + "total": 284 }, "i18n": { "rendererFormattedStrings": { @@ -25575,6 +25668,12 @@ "core/audits/layout-shift-elements.js | description": [ "audits[layout-shift-elements].description" ], + "core/audits/layout-shifts.js | title": [ + "audits[layout-shifts].title" + ], + "core/audits/layout-shifts.js | description": [ + "audits[layout-shifts].description" + ], "core/audits/long-tasks.js | title": [ "audits[long-tasks].title" ], diff --git a/core/test/gather/gatherers/trace-elements-test.js b/core/test/gather/gatherers/trace-elements-test.js index 100a31e524fd..cf739410bbf5 100644 --- a/core/test/gather/gatherers/trace-elements-test.js +++ b/core/test/gather/gatherers/trace-elements-test.js @@ -93,8 +93,8 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { const result = await TraceElementsGatherer.getTopLayoutShiftElements(trace, {computedCache: new Map()}); expect(result).toEqual([ - {nodeId: 25}, // score: 0.6 - {nodeId: 60}, // score: 0.4 + 25, // score: 0.6 + 60, // score: 0.4 ]); }); @@ -156,21 +156,21 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { const result = await TraceElementsGatherer.getTopLayoutShiftElements(trace, {computedCache: new Map()}); expect(result).toEqual([ - {nodeId: 3}, // score: 1.0 - {nodeId: 1}, // score: 0.5 - {nodeId: 2}, // score: 0.5 - {nodeId: 6}, // score: 0.25 - {nodeId: 7}, // score: 0.25 - {nodeId: 4}, // score: 0.125 - {nodeId: 5}, // score: 0.125 - {nodeId: 8}, // score: 0.1 - {nodeId: 9}, // score: 0.1 - {nodeId: 10}, // score: 0.1 - {nodeId: 11}, // score: 0.1 - {nodeId: 12}, // score: 0.1 - {nodeId: 13}, // score: 0.1 - {nodeId: 14}, // score: 0.1 - {nodeId: 15}, // score: 0.1 + 3, // score: 1.0 + 1, // score: 0.5 + 2, // score: 0.5 + 6, // score: 0.25 + 7, // score: 0.25 + 4, // score: 0.125 + 5, // score: 0.125 + 8, // score: 0.1 + 9, // score: 0.1 + 10, // score: 0.1 + 11, // score: 0.1 + 12, // score: 0.1 + 13, // score: 0.1 + 14, // score: 0.1 + 15, // score: 0.1 ]); }); }); diff --git a/core/test/gather/gatherers/trace-test.js b/core/test/gather/gatherers/trace-test.js index 141df9606409..c3c1cf3fd08a 100644 --- a/core/test/gather/gatherers/trace-test.js +++ b/core/test/gather/gatherers/trace-test.js @@ -23,6 +23,8 @@ describe('TraceGatherer', () => { describe('startSensitiveInstrumentation', () => { beforeEach(() => { context.driver.defaultSession.sendCommand + .mockResponse('DOM.enable') + .mockResponse('CSS.enable') .mockResponse('Page.enable') .mockResponse('Tracing.start'); }); @@ -70,14 +72,25 @@ describe('TraceGatherer', () => { gatherer.stopSensitiveInstrumentation(context.asContext()) ); + /** @param {number} data */ + const makeTraceEvent = (data) => ({name: 'fake', data}); + const dataListener = session.on.findListener('Tracing.dataCollected'); const completeListener = session.once.findListener('Tracing.tracingComplete'); - dataListener({value: [1, 2, 3]}); + dataListener({value: [ + makeTraceEvent(1), + makeTraceEvent(2), + makeTraceEvent(3), + ]}); await flushAllTimersAndMicrotasks(); expect(stopPromise).not.toBeDone(); - dataListener({value: [4, 5, 6]}); + dataListener({value: [ + makeTraceEvent(4), + makeTraceEvent(5), + makeTraceEvent(6), + ]}); await flushAllTimersAndMicrotasks(); expect(stopPromise).not.toBeDone(); @@ -87,7 +100,14 @@ describe('TraceGatherer', () => { expect(session.off).toHaveBeenCalled(); await stopPromise; - expect(await gatherer.getArtifact()).toEqual({traceEvents: [1, 2, 3, 4, 5, 6]}); + expect(await gatherer.getArtifact()).toMatchObject({traceEvents: [ + makeTraceEvent(1), + makeTraceEvent(2), + makeTraceEvent(3), + makeTraceEvent(4), + makeTraceEvent(5), + makeTraceEvent(6), + ]}); }); }); }); diff --git a/core/test/results/sample_v2.json b/core/test/results/sample_v2.json index 5ec1b015912e..4f5f4ba2b1c2 100644 --- a/core/test/results/sample_v2.json +++ b/core/test/results/sample_v2.json @@ -2647,6 +2647,22 @@ }, "guidanceLevel": 2 }, + "layout-shifts": { + "id": "layout-shifts", + "title": "Avoid large layout shifts", + "description": "These are the largest layout shifts observed on the page. Some layout shifts may not be included in the CLS metric value due to [windowing](https://web.dev/articles/cls#what_is_cls). [Learn how to improve CLS](https://web.dev/articles/optimize-cls)", + "score": null, + "scoreDisplayMode": "notApplicable", + "metricSavings": { + "CLS": 0.13570762803819444 + }, + "details": { + "type": "table", + "headings": [], + "items": [] + }, + "guidanceLevel": 2 + }, "long-tasks": { "id": "long-tasks", "title": "Avoid long main-thread tasks", @@ -6093,6 +6109,7 @@ "acronym": "CLS", "relevantAudits": [ "layout-shift-elements", + "layout-shifts", "non-composited-animations", "unsized-images" ] @@ -6248,6 +6265,10 @@ "id": "layout-shift-elements", "weight": 0 }, + { + "id": "layout-shifts", + "weight": 0 + }, { "id": "uses-passive-event-listeners", "weight": 0 @@ -8557,6 +8578,12 @@ "duration": 100, "entryType": "measure" }, + { + "startTime": 0, + "name": "lh:audit:layout-shifts", + "duration": 100, + "entryType": "measure" + }, { "startTime": 0, "name": "lh:audit:long-tasks", @@ -10088,6 +10115,12 @@ "core/audits/layout-shift-elements.js | columnContribution": [ "audits[layout-shift-elements].details.headings[1].label" ], + "core/audits/layout-shifts.js | title": [ + "audits[layout-shifts].title" + ], + "core/audits/layout-shifts.js | description": [ + "audits[layout-shifts].description" + ], "core/audits/long-tasks.js | title": [ "audits[long-tasks].title" ], diff --git a/shared/localization/locales/en-US.json b/shared/localization/locales/en-US.json index b029761c916c..d09915c30614 100644 --- a/shared/localization/locales/en-US.json +++ b/shared/localization/locales/en-US.json @@ -1166,6 +1166,18 @@ "core/audits/layout-shift-elements.js | title": { "message": "Avoid large layout shifts" }, + "core/audits/layout-shifts.js | columnScore": { + "message": "Layout shift score" + }, + "core/audits/layout-shifts.js | description": { + "message": "These are the largest layout shifts observed on the page. Some layout shifts may not be included in the CLS metric value due to [windowing](https://web.dev/articles/cls#what_is_cls). [Learn how to improve CLS](https://web.dev/articles/optimize-cls)" + }, + "core/audits/layout-shifts.js | rootCauseUnsizedMedia": { + "message": "The size of an element changed when CSS was applied" + }, + "core/audits/layout-shifts.js | title": { + "message": "Avoid large layout shifts" + }, "core/audits/lcp-lazy-loaded.js | description": { "message": "Above-the-fold images that are lazily loaded render later in the page lifecycle, which can delay the largest contentful paint. [Learn more about optimal lazy loading](https://web.dev/articles/lcp-lazy-loading)." }, diff --git a/shared/localization/locales/en-XL.json b/shared/localization/locales/en-XL.json index 4f8a3a46fce2..a7690cbfa8f5 100644 --- a/shared/localization/locales/en-XL.json +++ b/shared/localization/locales/en-XL.json @@ -1166,6 +1166,18 @@ "core/audits/layout-shift-elements.js | title": { "message": "Âv́ôíd̂ ĺâŕĝé l̂áŷóût́ ŝh́îf́t̂ś" }, + "core/audits/layout-shifts.js | columnScore": { + "message": "L̂áŷóût́ ŝh́îf́t̂ śĉór̂é" + }, + "core/audits/layout-shifts.js | description": { + "message": "T̂h́êśê ár̂é t̂h́ê ĺâŕĝéŝt́ l̂áŷóût́ ŝh́îf́t̂ś ôb́ŝér̂v́êd́ ôń t̂h́ê ṕâǵê. Śôḿê ĺâýôút̂ śĥíf̂t́ŝ ḿâý n̂ót̂ b́ê ín̂ćl̂úd̂éd̂ ín̂ t́ĥé ĈĹŜ ḿêt́r̂íĉ v́âĺûé d̂úê t́ô [ẃîńd̂óŵín̂ǵ](https://web.dev/articles/cls#what_is_cls). [L̂éâŕn̂ h́ôẃ t̂ó îḿp̂ŕôv́ê ĆL̂Ś](https://web.dev/articles/optimize-cls)" + }, + "core/audits/layout-shifts.js | rootCauseUnsizedMedia": { + "message": "T̂h́ê śîźê óf̂ án̂ él̂ém̂én̂t́ ĉh́âńĝéd̂ ẃĥén̂ ĆŜŚ ŵáŝ áp̂ṕl̂íêd́" + }, + "core/audits/layout-shifts.js | title": { + "message": "Âv́ôíd̂ ĺâŕĝé l̂áŷóût́ ŝh́îf́t̂ś" + }, "core/audits/lcp-lazy-loaded.js | description": { "message": "Âb́ôv́ê-t́ĥé-f̂ól̂d́ îḿâǵêś t̂h́ât́ âŕê ĺâźîĺŷ ĺôád̂éd̂ ŕêńd̂ér̂ ĺât́êŕ îń t̂h́ê ṕâǵê ĺîf́êćŷćl̂é, ŵh́îćĥ ćâń d̂él̂áŷ t́ĥé l̂ár̂ǵêśt̂ ćôńt̂én̂t́f̂úl̂ ṕâín̂t́. [L̂éâŕn̂ ḿôŕê áb̂óût́ ôṕt̂ím̂ál̂ ĺâźŷ ĺôád̂ín̂ǵ](https://web.dev/articles/lcp-lazy-loading)." }, diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index 78253ad7b102..a62f6a53bd42 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -23,6 +23,7 @@ import LHResult from './lhr/lhr.js' import Protocol from './protocol.js'; import Util from './utility-types.js'; import Audit from './audit.js'; +import { TraceProcessor, RootCauses, LayoutShiftRootCauses } from './trace-engine.js'; export type Artifacts = BaseArtifacts & GathererArtifacts; @@ -932,11 +933,19 @@ declare module Artifacts { } } +export interface TraceEngineRootCauses { + layoutShifts: Record; +} + export interface Trace { traceEvents: TraceEvent[]; metadata?: { 'cpu-family'?: number; }; + traceEngineResult?: { + data: TraceProcessor['data']; + rootCauses: TraceEngineRootCauses; + }; [futureProps: string]: any; } diff --git a/types/lh.d.ts b/types/lh.d.ts index a97573075881..d23d3f9ef950 100644 --- a/types/lh.d.ts +++ b/types/lh.d.ts @@ -38,6 +38,7 @@ export import BaseArtifacts = Artifacts_.BaseArtifacts; export import GathererArtifacts = Artifacts_.GathererArtifacts; export import DevtoolsLog = Artifacts_.DevtoolsLog; export import Trace = Artifacts_.Trace; +export import TraceEngineRootCauses = Artifacts_.TraceEngineRootCauses; export import TraceCpuProfile = Artifacts_.TraceCpuProfile; export import TraceEvent = Artifacts_.TraceEvent; diff --git a/types/trace-engine.d.ts b/types/trace-engine.d.ts new file mode 100644 index 000000000000..4a16c7c79313 --- /dev/null +++ b/types/trace-engine.d.ts @@ -0,0 +1,1513 @@ +/** + * @license + * Copyright 2023 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +// This part is just the subset of types we need for the main API. + +type LayoutShiftTraceEvent = LH.TraceEvent & { + args: {data: { + impacted_nodes: LH.Artifacts.TraceImpactedNode[], + weighted_score_delta: number; + }}, +} + +export class TraceProcessor { + constructor(handlers: any); + createWithAllHandlers(): TraceProcessor; + parse(traceEvents: any[]): Promise; + data: { + LayoutShifts: { + clusters: Array<{ + events: LayoutShiftTraceEvent[]; + }>; + sessionMaxScore: number; + }; + }; +} + +export const TraceHandlers: Record; + +interface CSSDimensions { + width?: string; + height?: string; + aspectRatio?: string; +} + +type RootCauseRequest = { + request: TraceEventSyntheticNetworkRequest; + initiator?: LH.Crdp.Network.Initiator; +} + +export type LayoutShiftRootCauses = { + fontChanges: Array; + iframes: Array<{ + iframe: LH.Crdp.DOM.Node; + }>; + renderBlockingRequests: Array; + unsizedMedia: Array<{ + node: LH.Crdp.DOM.Node; + authoredDimensions?: CSSDimensions; + computedDimensions: CSSDimensions; + }>; +}; + +export class RootCauses { + constructor(protocolInterface: any); + + layoutShifts: {rootCausesForEvent(data: any, event: any): Promise}; +} + +// The rest of this file is pulled from CDT frontend +// https://source.chromium.org/chromium/chromium/src/+/main:third_party/devtools-frontend/src/front_end/models/trace/types/TraceEvents.ts;l=297?q=TraceEventSyntheticNetworkRequest&ss=chromium + +// Copyright 2022 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +export type MicroSeconds = number&{_tag: 'MicroSeconds'}; + +export type MilliSeconds = number&{_tag: 'MilliSeconds'}; +export type Seconds = number&{_tag: 'Seconds'}; + +export const enum TimeUnit { + MICROSECONDS = 0, + MILLISECONDS = 1, + SECONDS = 2, + MINUTES = 3, +} + +// Other types. + +export interface TraceWindow { + min: TimeFormat; + max: TimeFormat; + range: TimeFormat; +} + +export type TraceWindowMicroSeconds = TraceWindow; +export type TraceWindowMilliSeconds = TraceWindow; + +// Trace Events. +export const enum Phase { + // Standard + BEGIN = 'B', + END = 'E', + COMPLETE = 'X', + INSTANT = 'I', + COUNTER = 'C', + + // Async + ASYNC_NESTABLE_START = 'b', + ASYNC_NESTABLE_INSTANT = 'n', + ASYNC_NESTABLE_END = 'e', + ASYNC_STEP_INTO = 'T', + ASYNC_BEGIN = 'S', + ASYNC_END = 'F', + ASYNC_STEP_PAST = 'p', + + // Flow + FLOW_START = 's', + FLOW_STEP = 't', + FLOW_END = 'f', + + // Sample + SAMPLE = 'P', + + // Object + OBJECT_CREATED = 'N', + OBJECT_SNAPSHOT = 'O', + OBJECT_DESTROYED = 'D', + + // Metadata + METADATA = 'M', + + // Memory Dump + MEMORY_DUMP_GLOBAL = 'V', + MEMORY_DUMP_PROCESS = 'v', + + // Mark + MARK = 'R', + + // Clock sync + CLOCK_SYNC = 'c', +} + +export const enum TraceEventScope { + THREAD = 't', + PROCESS = 'p', + GLOBAL = 'g', +} + +export interface TraceEventData { + args?: TraceEventArgs; + cat: string; + name: string; + ph: Phase; + pid: ProcessID; + tid: ThreadID; + tts?: MicroSeconds; + ts: MicroSeconds; + tdur?: MicroSeconds; + dur?: MicroSeconds; +} + +export interface TraceEventArgs { + data?: TraceEventArgsData; +} + +export interface TraceEventArgsData { + stackTrace?: TraceEventCallFrame[]; + navigationId?: string; + frame?: string; +} + +export interface TraceEventCallFrame { + codeType?: string; + functionName: string; + scriptId: number; + columnNumber: number; + lineNumber: number; + url: string; +} + +export interface TraceFrame { + frame: string; + name: string; + processId: ProcessID; + url: string; + parent?: string; +} + +// Sample events. + +export interface TraceEventSample extends TraceEventData { + ph: Phase.SAMPLE; +} + +/** + * A fake trace event created to support CDP.Profiler.Profiles in the + * trace engine. + */ +export interface SyntheticTraceEventCpuProfile extends TraceEventInstant { + name: 'CpuProfile'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + cpuProfile: LH.Crdp.Profiler.Profile, + }, + }; +} + +export interface TraceEventProfile extends TraceEventSample { + name: 'Profile'; + id: ProfileID; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + startTime: MicroSeconds, + }, + }; +} + +export interface TraceEventProfileChunk extends TraceEventSample { + name: 'ProfileChunk'; + id: ProfileID; + args: TraceEventArgs&{ + // `data` is only missing in "fake" traces + data?: TraceEventArgsData & { + cpuProfile?: TraceEventPartialProfile, + timeDeltas?: MicroSeconds[], + lines?: MicroSeconds[], + }, + }; +} + +export interface TraceEventPartialProfile { + nodes?: TraceEventPartialNode[]; + samples: CallFrameID[]; +} + +export interface TraceEventPartialNode { + callFrame: TraceEventCallFrame; + id: CallFrameID; + parent?: CallFrameID; +} + +// Complete events. + +export interface TraceEventComplete extends TraceEventData { + ph: Phase.COMPLETE; + dur: MicroSeconds; +} + +export interface TraceEventFireIdleCallback extends TraceEventComplete { + name: KnownEventName.FireIdleCallback; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + allottedMilliseconds: MilliSeconds, + frame: string, + id: number, + timedOut: boolean, + }, + }; +} + +export interface TraceEventDispatch extends TraceEventComplete { + name: 'EventDispatch'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + type: string, + }, + }; +} + +export interface TraceEventParseHTML extends TraceEventComplete { + name: 'ParseHTML'; + args: TraceEventArgs&{ + beginData: { + frame: string, + startLine: number, + url: string, + }, + endData?: { + endLine: number, + }, + }; +} + +export interface TraceEventBegin extends TraceEventData { + ph: Phase.BEGIN; +} + +export interface TraceEventEnd extends TraceEventData { + ph: Phase.END; +} + +/** + * This denotes a complete event created from a pair of begin and end + * events. For practicality, instead of always having to look for the + * end event corresponding to a begin event, we create a synthetic + * complete event that comprises the data of both from the beginning in + * the RendererHandler. + */ +export type TraceEventSyntheticCompleteEvent = TraceEventComplete; + +export interface TraceEventEventTiming extends TraceEventData { + ph: Phase.ASYNC_NESTABLE_START|Phase.ASYNC_NESTABLE_END; + name: KnownEventName.EventTiming; + id: string; + args: TraceEventArgs&{ + frame: string, + data?: TraceEventArgsData&{ + cancelable: boolean, + duration: MilliSeconds, + processingEnd: MilliSeconds, + processingStart: MilliSeconds, + timeStamp: MilliSeconds, + interactionId?: number, type: string, + }, + }; +} + +export interface TraceEventEventTimingBegin extends TraceEventEventTiming { + ph: Phase.ASYNC_NESTABLE_START; +} +export interface TraceEventEventTimingEnd extends TraceEventEventTiming { + ph: Phase.ASYNC_NESTABLE_END; +} + +export interface TraceEventGPUTask extends TraceEventComplete { + name: 'GPUTask'; + args: TraceEventArgs&{ + data?: TraceEventArgsData & { + /* eslint-disable @typescript-eslint/naming-convention */ + renderer_pid: ProcessID, + used_bytes: number, + /* eslint-enable @typescript-eslint/naming-convention */ + }, + }; +} + +export interface TraceEventSyntheticNetworkRedirect { + url: string; + priority: string; + requestMethod?: string; + ts: MicroSeconds; + dur: MicroSeconds; +} + +// TraceEventProcessedArgsData is used to store the processed data of a network +// request. Which is used to distinguish from the date we extract from the +// trace event directly. +interface TraceEventSyntheticArgsData { + dnsLookup: MicroSeconds; + download: MicroSeconds; + downloadStart: MicroSeconds; + finishTime: MicroSeconds; + initialConnection: MicroSeconds; + isDiskCached: boolean; + isHttps: boolean; + isMemoryCached: boolean; + isPushedResource: boolean; + networkDuration: MicroSeconds; + processingDuration: MicroSeconds; + proxyNegotiation: MicroSeconds; + queueing: MicroSeconds; + redirectionDuration: MicroSeconds; + requestSent: MicroSeconds; + sendStartTime: MicroSeconds; + ssl: MicroSeconds; + stalled: MicroSeconds; + totalTime: MicroSeconds; + waiting: MicroSeconds; +} + +export interface TraceEventSyntheticNetworkRequest extends TraceEventComplete { + args: TraceEventArgs&{ + data: TraceEventArgsData & { + syntheticData: TraceEventSyntheticArgsData, + // All fields below are from TraceEventsForNetworkRequest, + // Required fields + decodedBodyLength: number, + encodedDataLength: number, + frame: string, + fromServiceWorker: boolean, + host: string, + mimeType: string, + pathname: string, + search: string, + priority: Priority, + initialPriority: Priority, + protocol: string, + redirects: TraceEventSyntheticNetworkRedirect[], + renderBlocking: RenderBlocking, + requestId: string, + requestingFrameUrl: string, + statusCode: number, + url: string, + // Optional fields + requestMethod?: string, + timing?: TraceEventResourceReceiveResponseTimingData, + }, + }; + cat: 'loading'; + name: 'SyntheticNetworkRequest'; + ph: Phase.COMPLETE; + dur: MicroSeconds; + tdur: MicroSeconds; + ts: MicroSeconds; + tts: MicroSeconds; + pid: ProcessID; + tid: ThreadID; +} + +export const enum AuctionWorkletType { + BIDDER = 'bidder', + SELLER = 'seller', + // Not expected to be used, but here as a fallback in case new types get + // added and we have yet to update the trace engine. + UNKNOWN = 'unknown', +} + +export interface SyntheticAuctionWorkletEvent extends TraceEventInstant { + name: 'SyntheticAuctionWorkletEvent'; + // The PID that the AuctionWorklet is running in. + pid: ProcessID; + // URL + host: string; + // An ID used to pair up runningInProcessEvents with doneWithProcessEvents + target: string; + type: AuctionWorkletType; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + // There are two threads for a worklet that we care about, so we gather + // the thread_name events so we can know the PID and TID for them (and + // hence display the right events in the track for each thread) + utilityThread: TraceEventThreadName, + v8HelperThread: TraceEventThreadName, + } & + ( + // This type looks odd, but this is because these events could either have: + // 1. Just the DoneWithProcess event + // 2. Just the RunningInProcess event + // 3. Both events + // But crucially it cannot have both events missing, hence listing all the + // allowed cases. + // Clang is disabled as the combination of nested types and optional + // properties cause it to weirdly indent some of the properties and make it + // very unreadable. + // clang-format off + { + runningInProcessEvent: TraceEventAuctionWorkletRunningInProcess, + doneWithProcessEvent: TraceEventAuctionWorkletDoneWithProcess, + } | + { + runningInProcessEvent?: TraceEventAuctionWorkletRunningInProcess, + doneWithProcessEvent: TraceEventAuctionWorkletDoneWithProcess, + } | + { + doneWithProcessEvent?: TraceEventAuctionWorkletDoneWithProcess, + runningInProcessEvent: TraceEventAuctionWorkletRunningInProcess, + + }), + // clang-format on + }; +} +export interface TraceEventAuctionWorkletRunningInProcess extends TraceEventData { + name: 'AuctionWorkletRunningInProcess'; + ph: Phase.INSTANT; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + host: string, + pid: ProcessID, + target: string, + type: AuctionWorkletType, + }, + }; +} +export interface TraceEventAuctionWorkletDoneWithProcess extends TraceEventData { + name: 'AuctionWorkletDoneWithProcess'; + ph: Phase.INSTANT; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + host: string, + pid: ProcessID, + target: string, + type: AuctionWorkletType, + }, + }; +} + +// Snapshot events. + +export interface TraceEventSnapshot extends TraceEventData { + args: TraceEventArgs&{ + snapshot: string, + }; + name: 'Screenshot'; + cat: 'disabled-by-default-devtools.screenshot'; + ph: Phase.OBJECT_SNAPSHOT|Phase.INSTANT; // In Oct 2023, the phase was changed to Instant. crbug.com/798755 +} + +// Animation events. + +export interface TraceEventAnimation extends TraceEventData { + args: TraceEventArgs&{ + id?: string, + name?: string, + nodeId?: number, + nodeName?: string, + state?: string, + compositeFailed?: number, + unsupportedProperties?: string[], + }; + name: 'Animation'; + id2?: { + local?: string, + }; + ph: Phase.ASYNC_NESTABLE_START|Phase.ASYNC_NESTABLE_END; +} + +// Metadata events. + +export interface TraceEventMetadata extends TraceEventData { + ph: Phase.METADATA; + args: TraceEventArgs&{ + name?: string, + uptime?: string, + }; +} + +export interface TraceEventThreadName extends TraceEventMetadata { + name: KnownEventName.ThreadName; + args: TraceEventArgs&{ + name?: string, + }; +} + +export interface TraceEventProcessName extends TraceEventMetadata { + name: 'process_name'; +} + +// Mark events. + +export interface TraceEventMark extends TraceEventData { + ph: Phase.MARK; +} + +export interface TraceEventNavigationStart extends TraceEventMark { + name: 'navigationStart'; + args: TraceEventArgs&{ + data?: TraceEventArgsData & { + documentLoaderURL: string, + isLoadingMainFrame: boolean, + // isOutermostMainFrame was introduced in crrev.com/c/3625434 and exists + // because of Fenced Frames + // [github.com/WICG/fenced-frame/tree/master/explainer]. + // Fenced frames introduce a situation where isLoadingMainFrame could be + // true for a navigation, but that navigation be within an embedded "main + // frame", and therefore it wouldn't be on the top level main frame. + // In situations where we need to distinguish that, we can rely on + // isOutermostMainFrame, which will only be true for navigations on the + // top level main frame. + + // This flag is optional as it was introduced in May 2022; so users + // reasonably may import traces from before that date that do not have + // this field present. + isOutermostMainFrame?: boolean, navigationId: string, + }, + frame: string, + }; +} + +export interface TraceEventFirstContentfulPaint extends TraceEventMark { + name: 'firstContentfulPaint'; + args: TraceEventArgs&{ + frame: string, + data?: TraceEventArgsData&{ + navigationId: string, + }, + }; +} + +export interface TraceEventFirstPaint extends TraceEventMark { + name: 'firstPaint'; + args: TraceEventArgs&{ + frame: string, + data?: TraceEventArgsData&{ + navigationId: string, + }, + }; +} + +export type PageLoadEvent = TraceEventFirstContentfulPaint|TraceEventMarkDOMContent|TraceEventInteractiveTime| + TraceEventLargestContentfulPaintCandidate|TraceEventLayoutShift|TraceEventFirstPaint|TraceEventMarkLoad| + TraceEventNavigationStart; + +export interface TraceEventLargestContentfulPaintCandidate extends TraceEventMark { + name: 'largestContentfulPaint::Candidate'; + args: TraceEventArgs&{ + frame: string, + data?: TraceEventArgsData&{ + candidateIndex: number, + isOutermostMainFrame: boolean, + isMainFrame: boolean, + navigationId: string, + nodeId: LH.Crdp.DOM.BackendNodeId, + type?: string, + }, + }; +} +export interface TraceEventLargestImagePaintCandidate extends TraceEventMark { + name: 'LargestImagePaint::Candidate'; + args: TraceEventArgs&{ + frame: string, + data?: TraceEventArgsData&{ + candidateIndex: number, + imageUrl: string, + // eslint-disable-next-line @typescript-eslint/naming-convention + DOMNodeId: LH.Crdp.DOM.BackendNodeId, + }, + }; +} +export interface TraceEventLargestTextPaintCandidate extends TraceEventMark { + name: 'LargestTextPaint::Candidate'; + args: TraceEventArgs&{ + frame: string, + data?: TraceEventArgsData&{ + candidateIndex: number, + // eslint-disable-next-line @typescript-eslint/naming-convention + DOMNodeId: LH.Crdp.DOM.BackendNodeId, + }, + }; +} + +export interface TraceEventInteractiveTime extends TraceEventMark { + name: 'InteractiveTime'; + args: TraceEventArgs&{ + args: { + // eslint-disable-next-line @typescript-eslint/naming-convention + total_blocking_time_ms: number, + }, + frame: string, + }; +} + +// Instant events. + +export interface TraceEventInstant extends TraceEventData { + ph: Phase.INSTANT; + s: TraceEventScope; +} + +export interface TraceEventUpdateCounters extends TraceEventInstant { + name: 'UpdateCounters'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + documents: number, + jsEventListeners: number, + jsHeapSizeUsed: number, + nodes: number, + gpuMemoryLimitKB?: number, + }, + }; +} + +export type TraceEventRendererEvent = TraceEventInstant|TraceEventComplete; + +export interface TraceEventTracingStartedInBrowser extends TraceEventInstant { + name: KnownEventName.TracingStartedInBrowser; + args: TraceEventArgs&{ + data?: TraceEventArgsData & { + frameTreeNodeId: number, + // Frames can only missing in "fake" traces + frames?: TraceFrame[], persistentIds: boolean, + }, + }; +} + +export interface TraceEventTracingSessionIdForWorker extends TraceEventInstant { + name: 'TracingSessionIdForWorker'; + args: TraceEventArgs&{ + data?: TraceEventArgsData & { + url: string, + workerId: WorkerId, + workerThreadId: ThreadID, + frame: string, + }, + }; +} + +export interface TraceEventFrameCommittedInBrowser extends TraceEventInstant { + name: 'FrameCommittedInBrowser'; + args: TraceEventArgs&{ + data?: TraceEventArgsData & TraceFrame, + }; +} + +export interface TraceEventMainFrameViewport extends TraceEventInstant { + name: 'PaintTimingVisualizer::Viewport'; + args: { + data: TraceEventArgsData&{ + // eslint-disable-next-line @typescript-eslint/naming-convention + viewport_rect: number[], + }, + }; +} + +export interface TraceEventCommitLoad extends TraceEventInstant { + name: 'CommitLoad'; + args: TraceEventArgs&{ + data?: TraceEventArgsData & { + frame: string, + isMainFrame: boolean, + name: string, + nodeId: number, + page: string, + parent: string, + url: string, + }, + }; +} + +export interface TraceEventMarkDOMContent extends TraceEventInstant { + name: 'MarkDOMContent'; + args: TraceEventArgs&{ + data?: TraceEventArgsData & { + frame: string, + isMainFrame: boolean, + page: string, + }, + }; +} + +export interface TraceEventMarkLoad extends TraceEventInstant { + name: 'MarkLoad'; + args: TraceEventArgs&{ + data?: TraceEventArgsData & { + frame: string, + isMainFrame: boolean, + page: string, + }, + }; +} + +export interface TraceEventAsync extends TraceEventData { + ph: Phase.ASYNC_NESTABLE_START|Phase.ASYNC_NESTABLE_INSTANT|Phase.ASYNC_NESTABLE_END|Phase.ASYNC_STEP_INTO| + Phase.ASYNC_BEGIN|Phase.ASYNC_END|Phase.ASYNC_STEP_PAST; +} + +export type TraceRect = [number, number, number, number]; +export type TraceImpactedNode = { + // These keys come from the trace data, so we have to use underscores. + /* eslint-disable @typescript-eslint/naming-convention */ + new_rect: TraceRect, + node_id: LH.Crdp.DOM.BackendNodeId, + old_rect: TraceRect, + /* eslint-enable @typescript-eslint/naming-convention */ +}; + +type LayoutShiftData = TraceEventArgsData&{ + // These keys come from the trace data, so we have to use underscores. + /* eslint-disable @typescript-eslint/naming-convention */ + cumulative_score: number, + frame_max_distance: number, + had_recent_input: boolean, + impacted_nodes: TraceImpactedNode[] | undefined, + is_main_frame: boolean, + overall_max_distance: number, + region_rects: TraceRect[], + score: number, + weighted_score_delta: number, + /* eslint-enable @typescript-eslint/naming-convention */ +}; +// These keys come from the trace data, so we have to use underscores. +export interface TraceEventLayoutShift extends TraceEventInstant { + name: 'LayoutShift'; + normalized?: boolean; + args: TraceEventArgs&{ + frame: string, + data?: LayoutShiftData, + }; +} + +interface LayoutShiftSessionWindowData { + // The sum of the weighted score of all the shifts + // that belong to a session window. + cumulativeWindowScore: number; + // A consecutive generated in the frontend to + // to identify a session window. + id: number; +} +export interface LayoutShiftParsedData { + screenshotSource?: string; + timeFromNavigation?: MicroSeconds; + // The sum of the weighted scores of the shifts that + // belong to a session window up until this shift + // (inclusive). + cumulativeWeightedScoreInWindow: number; + sessionWindowData: LayoutShiftSessionWindowData; +} +export interface SyntheticLayoutShift extends TraceEventLayoutShift { + args: TraceEventArgs&{ + frame: string, + data?: LayoutShiftData&{ + rawEvent: TraceEventLayoutShift, + }, + }; + parsedData: LayoutShiftParsedData; +} + +export type Priority = 'Low'|'High'|'Medium'|'VeryHigh'|'Highest'; +export type RenderBlocking = 'blocking'|'non_blocking'|'in_body_parser_blocking'|'potentially_blocking'; +export interface TraceEventResourceSendRequest extends TraceEventInstant { + name: 'ResourceSendRequest'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + frame: string, + requestId: string, + url: string, + priority: Priority, + // TODO(crbug.com/1457985): change requestMethod to enum when confirm in the backend code. + requestMethod?: string, + renderBlocking?: RenderBlocking, + }, + }; +} + +export interface TraceEventResourceChangePriority extends TraceEventInstant { + name: 'ResourceChangePriority'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + requestId: string, + priority: Priority, + }, + }; +} + +export interface TraceEventResourceWillSendRequest extends TraceEventInstant { + name: 'ResourceWillSendRequest'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + requestId: string, + }, + }; +} + +export interface TraceEventResourceFinish extends TraceEventInstant { + name: 'ResourceFinish'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + decodedBodyLength: number, + didFail: boolean, + encodedDataLength: number, + finishTime: Seconds, + requestId: string, + }, + }; +} + +export interface TraceEventResourceReceivedData extends TraceEventInstant { + name: 'ResourceReceivedData'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + encodedDataLength: number, + frame: string, + requestId: string, + }, + }; +} + +interface TraceEventResourceReceiveResponseTimingData { + connectEnd: MilliSeconds; + connectStart: MilliSeconds; + dnsEnd: MilliSeconds; + dnsStart: MilliSeconds; + proxyEnd: MilliSeconds; + proxyStart: MilliSeconds; + pushEnd: MilliSeconds; + pushStart: MilliSeconds; + receiveHeadersEnd: MilliSeconds; + requestTime: Seconds; + sendEnd: MilliSeconds; + sendStart: MilliSeconds; + sslEnd: MilliSeconds; + sslStart: MilliSeconds; + workerReady: MilliSeconds; + workerStart: MilliSeconds; +} + +export interface TraceEventResourceReceiveResponse extends TraceEventInstant { + name: 'ResourceReceiveResponse'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + encodedDataLength: number, + frame: string, + fromCache: boolean, + fromServiceWorker: boolean, + mimeType: string, + requestId: string, + responseTime: MilliSeconds, + statusCode: number, + timing: TraceEventResourceReceiveResponseTimingData, + }, + }; +} + +export interface TraceEventResourceMarkAsCached extends TraceEventInstant { + name: 'ResourceMarkAsCached'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + requestId: string, + }, + }; +} + +export const enum LayoutInvalidationReason { + SIZE_CHANGED = 'Size changed', + ATTRIBUTE = 'Attribute', + ADDED_TO_LAYOUT = 'Added to layout', + SCROLLBAR_CHANGED = 'Scrollbar changed', + REMOVED_FROM_LAYOUT = 'Removed from layout', + STYLE_CHANGED = 'Style changed', + FONTS_CHANGED = 'Fonts changed', + UNKNOWN = 'Unknown', +} + +export interface TraceEventLayoutInvalidationTracking extends TraceEventInstant { + name: KnownEventName.LayoutInvalidationTracking; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + frame: string, + nodeId: LH.Crdp.DOM.BackendNodeId, + reason: LayoutInvalidationReason, + nodeName?: string, + }, + }; +} + +export interface TraceEventScheduleStyleInvalidationTracking extends TraceEventInstant { + name: KnownEventName.ScheduleStyleInvalidationTracking; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + frame: string, + nodeId: LH.Crdp.DOM.BackendNodeId, + invalidationSet?: string, + invalidatedSelectorId?: string, + reason?: LayoutInvalidationReason, + changedClass?: string, + nodeName?: string, + stackTrace?: TraceEventCallFrame[], + }, + }; +} + +export const enum StyleRecalcInvalidationReason { + ANIMATION = 'Animation', +} + +export interface TraceEventStyleRecalcInvalidation extends TraceEventInstant { + name: 'StyleRecalcInvalidationTracking'; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + frame: string, + nodeId: LH.Crdp.DOM.BackendNodeId, + reason: StyleRecalcInvalidationReason, + subtree: boolean, + nodeName?: string, + extraData?: string, + }, + }; +} +export interface TraceEventScheduleStyleRecalculation extends TraceEventInstant { + name: KnownEventName.ScheduleStyleRecalculation; + args: TraceEventArgs&{ + data: { + frame: string, + }, + }; +} +export interface TraceEventPrePaint extends TraceEventComplete { + name: 'PrePaint'; +} + +export type TraceEventNestableAsync = TraceEventNestableAsyncBegin|TraceEventNestableAsyncEnd; +export interface TraceEventNestableAsyncBegin extends TraceEventData { + ph: Phase.ASYNC_NESTABLE_START; + // The id2 field gives flexibility to explicitly specify if an event + // id is global among processes or process local. However not all + // events use it, so both kind of ids need to be marked as optional. + id2?: {local?: string, global?: string}; + id?: string; +} + +export interface TraceEventNestableAsyncEnd extends TraceEventData { + ph: Phase.ASYNC_NESTABLE_END; + id2?: {local?: string, global?: string}; + id?: string; +} + +export type TraceEventAsyncPerformanceMeasure = TraceEventPerformanceMeasureBegin|TraceEventPerformanceMeasureEnd; + +export interface TraceEventPerformanceMeasureBegin extends TraceEventNestableAsyncBegin { + cat: 'blink.user_timing'; + id: string; +} + +export interface TraceEventPerformanceMeasureEnd extends TraceEventNestableAsyncEnd { + cat: 'blink.user_timing'; + id: string; +} + +export interface TraceEventConsoleTimeBegin extends TraceEventNestableAsyncBegin { + cat: 'blink.console'; + id2: { + local: string, + }; +} + +export interface TraceEventConsoleTimeEnd extends TraceEventNestableAsyncEnd { + cat: 'blink.console'; + id2: { + local: string, + }; +} + +export interface TraceEventTimeStamp extends TraceEventData { + cat: 'devtools.timeline'; + name: 'TimeStamp'; + ph: Phase.INSTANT; + id: string; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + frame: string, + message: string, + }, + }; +} + +export interface TraceEventPerformanceMark extends TraceEventData { + cat: 'blink.user_timing'; + ph: Phase.INSTANT|Phase.MARK; + id: string; +} + +// Nestable async events with a duration are made up of two distinct +// events: the begin, and the end. We need both of them to be able to +// display the right information, so we create these synthetic events. +export interface TraceEventSyntheticNestableAsyncEvent extends TraceEventData { + id?: string; + id2?: {local?: string, global?: string}; + dur: MicroSeconds; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + beginEvent: TraceEventNestableAsyncBegin, + endEvent: TraceEventNestableAsyncEnd, + }, + }; +} + +export interface TraceEventSyntheticUserTiming extends TraceEventSyntheticNestableAsyncEvent { + id: string; + dur: MicroSeconds; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + beginEvent: TraceEventPerformanceMeasureBegin, + endEvent: TraceEventPerformanceMeasureEnd, + }, + }; +} + +export interface TraceEventSyntheticConsoleTiming extends TraceEventSyntheticNestableAsyncEvent { + id2: {local: string}; + dur: MicroSeconds; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + beginEvent: TraceEventConsoleTimeBegin, + endEvent: TraceEventConsoleTimeEnd, + }, + }; +} + +export interface SyntheticInteractionEvent extends TraceEventSyntheticNestableAsyncEvent { + // InteractionID and type are available within the beginEvent's data, but we + // put them on the top level for ease of access. + interactionId: number; + type: string; + // This is equivalent to startEvent.ts; + ts: MicroSeconds; + // This duration can be calculated via endEvent.ts - startEvent.ts, but we do + // that and put it here to make it easier. This also makes these events + // consistent with real events that have a dur field. + dur: MicroSeconds; + // These values are provided in the startEvent's args.data field as + // millisecond values, but during the handler phase we parse these into + // microseconds and put them on the top level for easy access. + processingStart: MicroSeconds; + processingEnd: MicroSeconds; + // These 3 values represent the breakdown of the parts of an interaction: + // 1. inputDelay: time from the user clicking to the input being handled + inputDelay: MicroSeconds; + // 2. mainThreadHandling: time spent processing the event handler + mainThreadHandling: MicroSeconds; + // 3. presentationDelay: delay between the event being processed and the frame being rendered + presentationDelay: MicroSeconds; + args: TraceEventArgs&{ + data: TraceEventArgsData & { + beginEvent: TraceEventEventTimingBegin, + endEvent: TraceEventEventTimingEnd, + }, + }; +} + +/** + * An event created synthetically in the frontend that has a self time + * (the time spent running the task itself). + */ +export interface SyntheticEventWithSelfTime extends TraceEventData { + selfTime?: MicroSeconds; +} + +/** + * A profile call created in the frontend from samples disguised as a + * trace event. + */ +export interface TraceEventSyntheticProfileCall extends SyntheticEventWithSelfTime { + callFrame: LH.Crdp.Runtime.CallFrame; + nodeId: LH.Crdp.integer; +} + +/** + * A trace event augmented synthetically in the frontend to contain + * its self time. + */ +export type SyntheticRendererEvent = TraceEventRendererEvent&SyntheticEventWithSelfTime; + +export type TraceEntry = SyntheticRendererEvent|TraceEventSyntheticProfileCall; + +// Events relating to frames. + +export interface TraceEventDrawFrame extends TraceEventInstant { + name: KnownEventName.DrawFrame; + args: TraceEventArgs&{ + layerTreeId: number, + frameSeqId: number, + }; +} + +export interface TraceEventLegacyDrawFrameBegin extends TraceEventAsync { + name: KnownEventName.DrawFrame; + ph: Phase.ASYNC_NESTABLE_START; + args: TraceEventArgs&{ + layerTreeId: number, + frameSeqId: number, + }; +} + +export interface TraceEventBeginFrame extends TraceEventInstant { + name: KnownEventName.BeginFrame; + args: TraceEventArgs&{ + layerTreeId: number, + frameSeqId: number, + }; +} + +export interface TraceEventDroppedFrame extends TraceEventInstant { + name: KnownEventName.DroppedFrame; + args: TraceEventArgs&{ + layerTreeId: number, + frameSeqId: number, + hasPartialUpdate?: boolean, + }; +} + +export interface TraceEventRequestMainThreadFrame extends TraceEventInstant { + name: KnownEventName.RequestMainThreadFrame; + args: TraceEventArgs&{ + layerTreeId: number, + }; +} + +export interface TraceEventBeginMainThreadFrame extends TraceEventInstant { + name: KnownEventName.BeginMainThreadFrame; + args: TraceEventArgs&{ + layerTreeId: number, + data: TraceEventArgsData&{ + frameId?: number, + }, + }; +} + +export interface TraceEventNeedsBeginFrameChanged extends TraceEventInstant { + name: KnownEventName.NeedsBeginFrameChanged; + args: TraceEventArgs&{ + layerTreeId: number, + data: TraceEventArgsData&{ + needsBeginFrame: number, + }, + }; +} + +export interface TraceEventCommit extends TraceEventInstant { + name: KnownEventName.Commit; + args: TraceEventArgs&{ + layerTreeId: number, + frameSeqId: number, + }; +} + +export interface TraceEventRasterTask extends TraceEventComplete { + name: KnownEventName.RasterTask; + args: TraceEventArgs&{ + tileData?: { + layerId: number, + sourceFrameNumber: number, + tileId: { + // eslint-disable-next-line @typescript-eslint/naming-convention + id_ref: string, + }, + tileResolution: string, + }, + }; +} + +// CompositeLayers has been replaced by "Commit", but we support both to not break old traces being imported. +export interface TraceEventCompositeLayers extends TraceEventInstant { + name: KnownEventName.CompositeLayers; + args: TraceEventArgs&{ + layerTreeId: number, + }; +} + +export interface TraceEventActivateLayerTree extends TraceEventInstant { + name: KnownEventName.ActivateLayerTree; + args: TraceEventArgs&{ + layerTreeId: number, + frameId: number, + }; +} + + +export interface TraceEventUpdateLayoutTree extends TraceEventComplete { + name: KnownEventName.UpdateLayoutTree; + args: TraceEventArgs&{ + elementCount: number, + beginData?: { + frame: string, + }, + }; +} + +export interface TraceEventLayout extends TraceEventComplete { + name: KnownEventName.Layout; + args: TraceEventArgs&{ + beginData: { + frame: string, + dirtyObjects: number, + partialLayout: boolean, + totalObjects: number, + }, + endData: { + layoutRoots: Array<{ + depth: number, + nodeId: LH.Crdp.DOM.BackendNodeId, + quads: number[][], + }>, + }, + }; +} + + +export class ProfileIdTag { + readonly #profileIdTag: (symbol|undefined); + } + export type ProfileID = string&ProfileIdTag; + + export class CallFrameIdTag { + readonly #callFrameIdTag: (symbol|undefined); + } + export type CallFrameID = number&CallFrameIdTag; + + + export class ProcessIdTag { + readonly #processIdTag: (symbol|undefined); + } + export type ProcessID = number&ProcessIdTag; + + + export class ThreadIdTag { + readonly #threadIdTag: (symbol|undefined); + } + export type ThreadID = number&ThreadIdTag; + + export class WorkerIdTag { + readonly #workerIdTag: (symbol|undefined); + } + export type WorkerId = string&WorkerIdTag; + +/** + * This is an exhaustive list of events we track in the Performance + * panel. Note not all of them are necessarliry shown in the flame + * chart, some of them we only use for parsing. + * TODO(crbug.com/1428024): Complete this enum. + */ +export const enum KnownEventName { + /* Metadata */ + ThreadName = 'thread_name', + + /* Task */ + Program = 'Program', + RunTask = 'RunTask', + AsyncTask = 'AsyncTask', + RunMicrotasks = 'RunMicrotasks', + + /* Load */ + XHRLoad = 'XHRLoad', + XHRReadyStateChange = 'XHRReadyStateChange', + /* Parse */ + ParseHTML = 'ParseHTML', + ParseCSS = 'ParseAuthorStyleSheet', + /* V8 */ + CompileCode = 'V8.CompileCode', + CompileModule = 'V8.CompileModule', + // Although V8 emits the V8.CompileScript event, the event that actually + // contains the useful information about the script (URL, etc), is contained + // in the v8.compile event. + // Yes, it is all lowercase compared to all the rest of the V8... events, + // that is not a typo :) + Compile = 'v8.compile', + CompileScript = 'V8.CompileScript', + Optimize = 'V8.OptimizeCode', + WasmStreamFromResponseCallback = 'v8.wasm.streamFromResponseCallback', + WasmCompiledModule = 'v8.wasm.compiledModule', + WasmCachedModule = 'v8.wasm.cachedModule', + WasmModuleCacheHit = 'v8.wasm.moduleCacheHit', + WasmModuleCacheInvalid = 'v8.wasm.moduleCacheInvalid', + /* Js */ + ProfileCall = 'ProfileCall', + EvaluateScript = 'EvaluateScript', + FunctionCall = 'FunctionCall', + EventDispatch = 'EventDispatch', + EvaluateModule = 'v8.evaluateModule', + RequestMainThreadFrame = 'RequestMainThreadFrame', + RequestAnimationFrame = 'RequestAnimationFrame', + CancelAnimationFrame = 'CancelAnimationFrame', + FireAnimationFrame = 'FireAnimationFrame', + RequestIdleCallback = 'RequestIdleCallback', + CancelIdleCallback = 'CancelIdleCallback', + FireIdleCallback = 'FireIdleCallback', + TimerInstall = 'TimerInstall', + TimerRemove = 'TimerRemove', + TimerFire = 'TimerFire', + WebSocketCreate = 'WebSocketCreate', + WebSocketSendHandshake = 'WebSocketSendHandshakeRequest', + WebSocketReceiveHandshake = 'WebSocketReceiveHandshakeResponse', + WebSocketDestroy = 'WebSocketDestroy', + CryptoDoEncrypt = 'DoEncrypt', + CryptoDoEncryptReply = 'DoEncryptReply', + CryptoDoDecrypt = 'DoDecrypt', + CryptoDoDecryptReply = 'DoDecryptReply', + CryptoDoDigest = 'DoDigest', + CryptoDoDigestReply = 'DoDigestReply', + CryptoDoSign = 'DoSign', + CryptoDoSignReply = 'DoSignReply', + CryptoDoVerify = 'DoVerify', + CryptoDoVerifyReply = 'DoVerifyReply', + V8Execute = 'V8.Execute', + + /* Gc */ + GC = 'GCEvent', + DOMGC = 'BlinkGC.AtomicPhase', + IncrementalGCMarking = 'V8.GCIncrementalMarking', + MajorGC = 'MajorGC', + MinorGC = 'MinorGC', + GCCollectGarbage = 'BlinkGC.AtomicPhase', + + /* Layout */ + ScheduleStyleRecalculation = 'ScheduleStyleRecalculation', + RecalculateStyles = 'RecalculateStyles', + Layout = 'Layout', + UpdateLayoutTree = 'UpdateLayoutTree', + InvalidateLayout = 'InvalidateLayout', + LayoutInvalidationTracking = 'LayoutInvalidationTracking', + ComputeIntersections = 'ComputeIntersections', + HitTest = 'HitTest', + PrePaint = 'PrePaint', + Layerize = 'Layerize', + LayoutShift = 'LayoutShift', + UpdateLayerTree = 'UpdateLayerTree', + ScheduleStyleInvalidationTracking = 'ScheduleStyleInvalidationTracking', + StyleRecalcInvalidationTracking = 'StyleRecalcInvalidationTracking', + StyleInvalidatorInvalidationTracking = 'StyleInvalidatorInvalidationTracking', + + /* Paint */ + ScrollLayer = 'ScrollLayer', + UpdateLayer = 'UpdateLayer', + PaintSetup = 'PaintSetup', + Paint = 'Paint', + PaintImage = 'PaintImage', + Commit = 'Commit', + CompositeLayers = 'CompositeLayers', + RasterTask = 'RasterTask', + ImageDecodeTask = 'ImageDecodeTask', + ImageUploadTask = 'ImageUploadTask', + DecodeImage = 'Decode Image', + ResizeImage = 'Resize Image', + DrawLazyPixelRef = 'Draw LazyPixelRef', + DecodeLazyPixelRef = 'Decode LazyPixelRef', + GPUTask = 'GPUTask', + Rasterize = 'Rasterize', + EventTiming = 'EventTiming', + + /* Compile */ + OptimizeCode = 'V8.OptimizeCode', + CacheScript = 'v8.produceCache', + CacheModule = 'v8.produceModuleCache', + // V8Sample events are coming from tracing and contain raw stacks with function addresses. + // After being processed with help of JitCodeAdded and JitCodeMoved events they + // get translated into function infos and stored as stacks in JSSample events. + V8Sample = 'V8Sample', + JitCodeAdded = 'JitCodeAdded', + JitCodeMoved = 'JitCodeMoved', + StreamingCompileScript = 'v8.parseOnBackground', + StreamingCompileScriptWaiting = 'v8.parseOnBackgroundWaiting', + StreamingCompileScriptParsing = 'v8.parseOnBackgroundParsing', + BackgroundDeserialize = 'v8.deserializeOnBackground', + FinalizeDeserialization = 'V8.FinalizeDeserialization', + + /* Markers */ + CommitLoad = 'CommitLoad', + MarkLoad = 'MarkLoad', + MarkDOMContent = 'MarkDOMContent', + MarkFirstPaint = 'firstPaint', + MarkFCP = 'firstContentfulPaint', + MarkLCPCandidate = 'largestContentfulPaint::Candidate', + MarkLCPInvalidate = 'largestContentfulPaint::Invalidate', + NavigationStart = 'navigationStart', + TimeStamp = 'TimeStamp', + ConsoleTime = 'ConsoleTime', + UserTiming = 'UserTiming', + InteractiveTime = 'InteractiveTime', + + /* Frames */ + BeginFrame = 'BeginFrame', + NeedsBeginFrameChanged = 'NeedsBeginFrameChanged', + BeginMainThreadFrame = 'BeginMainThreadFrame', + ActivateLayerTree = 'ActivateLayerTree', + DrawFrame = 'DrawFrame', + DroppedFrame = 'DroppedFrame', + FrameStartedLoading = 'FrameStartedLoading', + + /* Network request events */ + ResourceWillSendRequest = 'ResourceWillSendRequest', + ResourceSendRequest = 'ResourceSendRequest', + ResourceReceiveResponse = 'ResourceReceiveResponse', + ResourceReceivedData = 'ResourceReceivedData', + ResourceFinish = 'ResourceFinish', + ResourceMarkAsCached = 'ResourceMarkAsCached', + + /* Web sockets */ + WebSocketSendHandshakeRequest = 'WebSocketSendHandshakeRequest', + WebSocketReceiveHandshakeResponse = 'WebSocketReceiveHandshakeResponse', + + /* CPU Profiling */ + Profile = 'Profile', + StartProfiling = 'CpuProfiler::StartProfiling', + ProfileChunk = 'ProfileChunk', + UpdateCounters = 'UpdateCounters', + + /* Other */ + Animation = 'Animation', + ParseAuthorStyleSheet = 'ParseAuthorStyleSheet', + EmbedderCallback = 'EmbedderCallback', + SetLayerTreeId = 'SetLayerTreeId', + TracingStartedInPage = 'TracingStartedInPage', + TracingStartedInBrowser = 'TracingStartedInBrowser', + TracingSessionIdForWorker = 'TracingSessionIdForWorker', + LazyPixelRef = 'LazyPixelRef', + LayerTreeHostImplSnapshot = 'cc::LayerTreeHostImpl', + PictureSnapshot = 'cc::Picture', + DisplayItemListSnapshot = 'cc::DisplayItemList', + InputLatencyMouseMove = 'InputLatency::MouseMove', + InputLatencyMouseWheel = 'InputLatency::MouseWheel', + ImplSideFling = 'InputHandlerProxy::HandleGestureFling::started', +} + +export interface TraceEventSyntheticNetworkRequest extends TraceEventComplete { + args: TraceEventArgs&{ + data: TraceEventArgsData & { + syntheticData: TraceEventSyntheticArgsData, + // All fields below are from TraceEventsForNetworkRequest, + // Required fields + decodedBodyLength: number, + encodedDataLength: number, + frame: string, + fromServiceWorker: boolean, + host: string, + mimeType: string, + pathname: string, + search: string, + priority: Priority, + initialPriority: Priority, + protocol: string, + redirects: TraceEventSyntheticNetworkRedirect[], + renderBlocking: RenderBlocking, + requestId: string, + requestingFrameUrl: string, + statusCode: number, + url: string, + // Optional fields + requestMethod?: string, + timing?: TraceEventResourceReceiveResponseTimingData, + }, + }; + cat: 'loading'; + name: 'SyntheticNetworkRequest'; + ph: Phase.COMPLETE; + dur: MicroSeconds; + tdur: MicroSeconds; + ts: MicroSeconds; + tts: MicroSeconds; + pid: ProcessID; + tid: ThreadID; +}