From 43042e187e015ef450203f5edeba1836ada4e1f9 Mon Sep 17 00:00:00 2001 From: Adam Raine <6752989+adamraine@users.noreply.github.com> Date: Mon, 27 Jul 2020 16:56:36 -0400 Subject: [PATCH] core: generalize TraceElements gatherer and add animated elements (#11138) * Add animation events to trace elements * Add trace elements to smoke test * Update smoke test to trace-elements * nits * lint * rename metric name * Fix smoke test * remove node id * lint * Add node test * nit * newline * fix tests * update sample * nits * old code cleanup * height too * set height manually * fix sample * Fix flaky score * switch to layout-shift * Correct element * switch to div * nit --- .../test/fixtures/perf/delayed-element.js | 3 +- .../test/fixtures/perf/trace-elements.html | 15 ++ .../test-definitions/perf/expectations.js | 62 ++++++ .../largest-contentful-paint-element.js | 4 +- .../audits/layout-shift-elements.js | 4 +- .../gather/gatherers/trace-elements.js | 88 +++++--- .../largest-contentful-paint-element-test.js | 2 +- .../test/audits/layout-shift-elements-test.js | 4 +- .../gather/gatherers/trace-elements-test.js | 195 ++++++++++++++++-- .../test/results/artifacts/artifacts.json | 10 +- types/artifacts.d.ts | 3 +- 11 files changed, 322 insertions(+), 68 deletions(-) diff --git a/lighthouse-cli/test/fixtures/perf/delayed-element.js b/lighthouse-cli/test/fixtures/perf/delayed-element.js index f267b9c57aec..4f472d5985e0 100644 --- a/lighthouse-cli/test/fixtures/perf/delayed-element.js +++ b/lighthouse-cli/test/fixtures/perf/delayed-element.js @@ -28,8 +28,9 @@ async function rerender(iterations) { setTimeout(() => { const imgEl = document.createElement('img'); imgEl.src = '../dobetterweb/lighthouse-480x318.jpg'; - const textEl = document.createElement('span'); + const textEl = document.createElement('div'); textEl.textContent = 'Sorry!'; + textEl.style.height = '18px' // this height can be flaky so we set it manually const top = document.getElementById('late-content'); top.appendChild(imgEl); top.appendChild(textEl); diff --git a/lighthouse-cli/test/fixtures/perf/trace-elements.html b/lighthouse-cli/test/fixtures/perf/trace-elements.html index 58ad27c20dd0..1624133ab13c 100644 --- a/lighthouse-cli/test/fixtures/perf/trace-elements.html +++ b/lighthouse-cli/test/fixtures/perf/trace-elements.html @@ -1,6 +1,21 @@ + +

Please don't move me

diff --git a/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js b/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js index a7c5a2a5c93f..c3cb33e7db0f 100644 --- a/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js +++ b/lighthouse-cli/test/smokehouse/test-definitions/perf/expectations.js @@ -156,6 +156,68 @@ module.exports = [ }, }, { + artifacts: { + TraceElements: [ + { + traceEventType: 'largest-contentful-paint', + selector: 'body > div#late-content > img', + nodeLabel: 'img', + snippet: '', + boundingRect: { + top: 108, + bottom: 426, + left: 8, + right: 488, + width: 480, + height: 318, + }, + }, + { + traceEventType: 'layout-shift', + selector: 'body > h1', + nodeLabel: 'Please don\'t move me', + snippet: '

', + boundingRect: { + top: 465, + bottom: 502, + left: 8, + right: 352, + width: 344, + height: 37, + }, + score: '0.058 +/- 0.01', + }, + { + traceEventType: 'layout-shift', + selector: 'body > div#late-content > div', + nodeLabel: 'Sorry!', + snippet: '
', + boundingRect: { + top: 426, + bottom: 444, + left: 8, + right: 352, + width: 344, + height: 18, + }, + score: '0.026 +/- 0.01', + }, + { + traceEventType: 'animation', + selector: 'body > div#animate-me', + nodeLabel: 'div', + snippet: '
', + boundingRect: { + top: 8, + bottom: 108, + left: 8, + right: 108, + width: 100, + height: 100, + }, + }, + ], + }, lhr: { requestedUrl: 'http://localhost:10200/perf/trace-elements.html', finalUrl: 'http://localhost:10200/perf/trace-elements.html', diff --git a/lighthouse-core/audits/largest-contentful-paint-element.js b/lighthouse-core/audits/largest-contentful-paint-element.js index 04317c8b74c6..a17d540f479b 100644 --- a/lighthouse-core/audits/largest-contentful-paint-element.js +++ b/lighthouse-core/audits/largest-contentful-paint-element.js @@ -42,8 +42,8 @@ class LargestContentfulPaintElement extends Audit { * @return {LH.Audit.Product} */ static audit(artifacts) { - const lcpElement = - artifacts.TraceElements.find(element => element.metricName === 'largest-contentful-paint'); + const lcpElement = artifacts.TraceElements + .find(element => element.traceEventType === 'largest-contentful-paint'); const lcpElementDetails = []; if (lcpElement) { lcpElementDetails.push({ diff --git a/lighthouse-core/audits/layout-shift-elements.js b/lighthouse-core/audits/layout-shift-elements.js index 4d01c194f8e1..7c948c7da67f 100644 --- a/lighthouse-core/audits/layout-shift-elements.js +++ b/lighthouse-core/audits/layout-shift-elements.js @@ -43,8 +43,8 @@ class LayoutShiftElements extends Audit { * @return {LH.Audit.Product} */ static audit(artifacts) { - const clsElements = - artifacts.TraceElements.filter(element => element.metricName === 'cumulative-layout-shift'); + const clsElements = artifacts.TraceElements + .filter(element => element.traceEventType === 'layout-shift'); const clsElementData = clsElements.map(element => { return { diff --git a/lighthouse-core/gather/gatherers/trace-elements.js b/lighthouse-core/gather/gatherers/trace-elements.js index 1a724f02cae0..a5714e759728 100644 --- a/lighthouse-core/gather/gatherers/trace-elements.js +++ b/lighthouse-core/gather/gatherers/trace-elements.js @@ -20,16 +20,13 @@ const RectHelpers = require('../../lib/rect-helpers.js'); /** * @this {HTMLElement} - * @param {string} metricName - * @return {LH.Artifacts.TraceElement | undefined} */ /* istanbul ignore next */ -function setAttributeMarker(metricName) { +function getNodeDetailsData() { const elem = this.nodeType === document.ELEMENT_NODE ? this : this.parentElement; // eslint-disable-line no-undef let traceElement; if (elem) { traceElement = { - metricName, // @ts-ignore - put into scope via stringification devtoolsNodePath: getNodePath(elem), // eslint-disable-line no-undef // @ts-ignore - put into scope via stringification @@ -137,6 +134,24 @@ class TraceElements extends Gatherer { return topFive; } + /** + * Find the node ids of elements which are animated using the Animation trace events. + * @param {Array} mainThreadEvents + * @return {Array} + */ + static getAnimatedElements(mainThreadEvents) { + const animatedElementIds = new Set(mainThreadEvents + .filter(e => e.name === 'Animation' && e.ph === 'b') + .map(e => this.getNodeIDFromTraceEvent(e))); + + /** @type Array */ + const animatedElementData = []; + for (const nodeId of animatedElementIds) { + nodeId && animatedElementData.push({nodeId}); + } + return animatedElementData; + } + /** * @param {LH.Gatherer.PassContext} passContext * @param {LH.Gatherer.LoadData} loadData @@ -150,40 +165,47 @@ class TraceElements extends Gatherer { const {largestContentfulPaintEvt, mainThreadEvents} = TraceProcessor.computeTraceOfTab(loadData.trace); - /** @type {Array} */ - const backendNodeData = []; const lcpNodeId = TraceElements.getNodeIDFromTraceEvent(largestContentfulPaintEvt); const clsNodeData = TraceElements.getTopLayoutShiftElements(mainThreadEvents); - if (lcpNodeId) { - backendNodeData.push({nodeId: lcpNodeId}); - } - backendNodeData.push(...clsNodeData); + const animatedElementData = TraceElements.getAnimatedElements(mainThreadEvents); - const traceElements = []; - for (let i = 0; i < backendNodeData.length; i++) { - const backendNodeId = backendNodeData[i].nodeId; - const metricName = - lcpNodeId === backendNodeId ? 'largest-contentful-paint' : 'cumulative-layout-shift'; - const objectId = await driver.resolveNodeIdToObjectId(backendNodeId); - if (!objectId) continue; - const response = await driver.sendCommand('Runtime.callFunctionOn', { - objectId, - functionDeclaration: `function () { - ${setAttributeMarker.toString()}; - ${pageFunctions.getNodePathString}; - ${pageFunctions.getNodeSelectorString}; - ${pageFunctions.getNodeLabelString}; - ${pageFunctions.getOuterHTMLSnippetString}; - ${pageFunctions.getBoundingClientRectString}; - return setAttributeMarker.call(this, '${metricName}'); - }`, - returnByValue: true, - awaitPromise: true, - }); + /** @type Map */ + const backendNodeDataMap = new Map([ + ['largest-contentful-paint', lcpNodeId ? [{nodeId: lcpNodeId}] : []], + ['layout-shift', clsNodeData], + ['animation', animatedElementData], + ]); - if (response && response.result && response.result.value) { - traceElements.push({...response.result.value, score: backendNodeData[i].score}); + const traceElements = []; + for (const [traceEventType, backendNodeData] of backendNodeDataMap) { + for (let i = 0; i < backendNodeData.length; i++) { + const backendNodeId = backendNodeData[i].nodeId; + const objectId = await driver.resolveNodeIdToObjectId(backendNodeId); + if (!objectId) continue; + const response = await driver.sendCommand('Runtime.callFunctionOn', { + objectId, + functionDeclaration: `function () { + ${getNodeDetailsData.toString()}; + ${pageFunctions.getNodePathString}; + ${pageFunctions.getNodeSelectorString}; + ${pageFunctions.getNodeLabelString}; + ${pageFunctions.getOuterHTMLSnippetString}; + ${pageFunctions.getBoundingClientRectString}; + return getNodeDetailsData.call(this); + }`, + returnByValue: true, + awaitPromise: true, + }); + + if (response && response.result && response.result.value) { + traceElements.push({ + traceEventType, + ...response.result.value, + score: backendNodeData[i].score, + nodeId: backendNodeId, + }); + } } } diff --git a/lighthouse-core/test/audits/largest-contentful-paint-element-test.js b/lighthouse-core/test/audits/largest-contentful-paint-element-test.js index 60b3114cebfd..56998869581c 100644 --- a/lighthouse-core/test/audits/largest-contentful-paint-element-test.js +++ b/lighthouse-core/test/audits/largest-contentful-paint-element-test.js @@ -13,7 +13,7 @@ describe('Performance: largest-contentful-paint-element audit', () => { it('correctly surfaces the LCP element', async () => { const artifacts = { TraceElements: [{ - metricName: 'largest-contentful-paint', + traceEventType: 'largest-contentful-paint', devtoolsNodePath: '1,HTML,3,BODY,5,DIV,0,HEADER', selector: 'div.l-header > div.chorus-emc__content', nodeLabel: 'My Test Label', diff --git a/lighthouse-core/test/audits/layout-shift-elements-test.js b/lighthouse-core/test/audits/layout-shift-elements-test.js index 2294faf314dd..f5a6cb65be8b 100644 --- a/lighthouse-core/test/audits/layout-shift-elements-test.js +++ b/lighthouse-core/test/audits/layout-shift-elements-test.js @@ -14,7 +14,7 @@ describe('Performance: layout-shift-elements audit', () => { it('correctly surfaces a single CLS element', async () => { const artifacts = { TraceElements: [{ - metricName: 'cumulative-layout-shift', + traceEventType: 'layout-shift', devtoolsNodePath: '1,HTML,3,BODY,5,DIV,0,HEADER', selector: 'div.l-header > div.chorus-emc__content', nodeLabel: 'My Test Label', @@ -34,7 +34,7 @@ describe('Performance: layout-shift-elements audit', () => { it('correctly surfaces multiple CLS elements', async () => { const clsElement = { - metricName: 'cumulative-layout-shift', + traceEventType: 'layout-shift', devtoolsNodePath: '1,HTML,3,BODY,5,DIV,0,HEADER', selector: 'div.l-header > div.chorus-emc__content', nodeLabel: 'My Test Label', diff --git a/lighthouse-core/test/gather/gatherers/trace-elements-test.js b/lighthouse-core/test/gather/gatherers/trace-elements-test.js index 2260f7552c3a..254a3cc5fcaf 100644 --- a/lighthouse-core/test/gather/gatherers/trace-elements-test.js +++ b/lighthouse-core/test/gather/gatherers/trace-elements-test.js @@ -8,16 +8,20 @@ /* eslint-env jest */ const TraceElementsGatherer = require('../../../gather/gatherers/trace-elements.js'); +const Driver = require('../../../gather/driver.js'); +const Connection = require('../../../gather/connections/connection.js'); +const createTestTrace = require('../../create-test-trace.js'); +const {createMockSendCommandFn} = require('../mock-commands.js'); describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { - function makeTraceEvent(score, impactedNodes, had_recent_input = false) { + function makeLayoutShiftTraceEvent(score, impactedNodes, had_recent_input = false) { return { name: 'LayoutShift', cat: 'loading', ph: 'I', - pid: 4998, - tid: 775, - ts: 308559814315, + pid: 1111, + tid: 222, + ts: 1200, args: { data: { had_recent_input, @@ -29,6 +33,52 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { }; } + function makeAnimationTraceEvent(id, nodeId) { + return { + args: { + data: { + id, + name: '', + nodeId, + nodeName: 'DIV', + state: 'running', + }, + }, + cat: 'blink.animations,devtools.timeline,benchmark,rail', + id2: { + local: '0x363db876c8', + }, + name: 'Animation', + ph: 'b', + pid: 1111, + scope: 'blink.animations,devtools.timeline,benchmark,rail', + tid: 222, + ts: 1300, + }; + } + + function makeLCPTraceEvent(nodeId) { + return { + args: { + data: { + candidateIndex: 1, + isMainFrame: true, + navigationId: 'AB3DB6ED51813821034CE7325C0BAC6B', + nodeId, + size: 1212, + type: 'text', + }, + frame: '3EFC2700D7BC3F4734CAF2F726EFB78C', + }, + cat: 'loading,rail,devtools.timeline', + name: 'largestContentfulPaint::Candidate', + ph: 'R', + pid: 1111, + tid: 222, + ts: 1400, + }; + } + /** * @param {Array<{nodeId: number, score: number}>} shiftScores */ @@ -45,7 +95,7 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { it('returns layout shift data sorted by impact area', () => { const traceEvents = [ - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 60, @@ -70,14 +120,14 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { it('does not ignore initial trace events with input', () => { const traceEvents = [ - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 1, old_rect: [0, 0, 200, 100], }, ], true), - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 2, @@ -95,14 +145,14 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { it('does ignore later trace events with input', () => { const traceEvents = [ - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 1, old_rect: [0, 0, 200, 100], }, ]), - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 2, @@ -119,49 +169,49 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { it('correctly ignores trace events with input (complex)', () => { const traceEvents = [ - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 1, old_rect: [0, 0, 200, 100], }, ], true), - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 2, old_rect: [0, 0, 200, 100], }, ], true), - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 3, old_rect: [0, 0, 200, 100], }, ]), - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 4, old_rect: [0, 0, 200, 100], }, ]), - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 5, old_rect: [0, 0, 200, 100], }, ], true), - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 6, old_rect: [0, 0, 200, 100], }, ], true), - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 7, @@ -182,7 +232,7 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { it('combines scores for the same nodeId accross multiple shift events', () => { const traceEvents = [ - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 0, 200, 200], node_id: 60, @@ -194,7 +244,7 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { old_rect: [0, 100, 200, 100], }, ]), - makeTraceEvent(0.3, [ + makeLayoutShiftTraceEvent(0.3, [ { new_rect: [0, 100, 200, 200], node_id: 60, @@ -214,7 +264,7 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { it('returns only the top five values', () => { const traceEvents = [ - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 100, 100, 100], node_id: 1, @@ -226,14 +276,14 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { old_rect: [0, 100, 100, 100], }, ]), - makeTraceEvent(1, [ + makeLayoutShiftTraceEvent(1, [ { new_rect: [0, 100, 200, 200], node_id: 3, old_rect: [0, 100, 200, 200], }, ]), - makeTraceEvent(0.75, [ + makeLayoutShiftTraceEvent(0.75, [ { new_rect: [0, 0, 100, 50], node_id: 4, @@ -268,4 +318,107 @@ describe('Trace Elements gatherer - GetTopLayoutShiftElements', () => { const total = sumScores(result); expectEqualFloat(total, 2.5); }); + + it('gets animated node ids without duplicates', () => { + const traceEvents = [ + makeAnimationTraceEvent('1', 5), + makeAnimationTraceEvent('2', 5), + makeAnimationTraceEvent('3', 6), + ]; + + const result = TraceElementsGatherer.getAnimatedElements(traceEvents); + expect(result).toEqual([ + {nodeId: 5}, + {nodeId: 6}, + ]); + }); + + it('properly resolves all node id types', async () => { + const layoutShiftNodeData = { + traceEventType: 'layout-shift', + devtoolsNodePath: '1,HTML,1,BODY,1,DIV', + selector: 'body > div#shift', + nodeLabel: 'div', + snippet: '
', + boundingRect: { + top: 50, + bottom: 200, + left: 50, + right: 100, + width: 50, + height: 150, + }, + }; + const animationNodeData = { + traceEventType: 'animation', + devtoolsNodePath: '1,HTML,1,BODY,1,DIV', + selector: 'body > div#animated', + nodeLabel: 'div', + snippet: '
', + boundingRect: { + top: 60, + bottom: 200, + left: 60, + right: 100, + width: 40, + height: 140, + }, + }; + const LCPNodeData = { + traceEventType: 'largest-contentful-paint', + devtoolsNodePath: '1,HTML,1,BODY,1,DIV', + selector: 'body > div#lcp', + nodeLabel: 'div', + snippet: '
', + boundingRect: { + top: 70, + bottom: 200, + left: 70, + right: 100, + width: 30, + height: 130, + }, + }; + const connectionStub = new Connection(); + connectionStub.sendCommand = createMockSendCommandFn() + .mockResponse('DOM.resolveNode', {object: {objectId: 1}}) + .mockResponse('Runtime.callFunctionOn', {result: {value: LCPNodeData}}) + .mockResponse('DOM.resolveNode', {object: {objectId: 2}}) + .mockResponse('Runtime.callFunctionOn', {result: {value: layoutShiftNodeData}}) + .mockResponse('DOM.resolveNode', {object: {objectId: 3}}) + .mockResponse('Runtime.callFunctionOn', {result: {value: animationNodeData}}); + const driver = new Driver(connectionStub); + + const trace = createTestTrace({timeOrigin: 0, traceEnd: 2000}); + trace.traceEvents.push( + makeLayoutShiftTraceEvent(1, [ + { + new_rect: [0, 100, 200, 200], + node_id: 4, + old_rect: [0, 100, 200, 200], + }, + ]) + ); + trace.traceEvents.push(makeAnimationTraceEvent('1', 5)); + trace.traceEvents.push(makeLCPTraceEvent(6)); + + const gatherer = new TraceElementsGatherer(); + const result = await gatherer.afterPass({driver}, {trace}); + + expect(result).toEqual([ + { + ...LCPNodeData, + nodeId: 6, + }, + { + ...layoutShiftNodeData, + score: 1, + nodeId: 4, + }, + { + ...animationNodeData, + nodeId: 5, + }, + ]); + }); }); diff --git a/lighthouse-core/test/results/artifacts/artifacts.json b/lighthouse-core/test/results/artifacts/artifacts.json index 676152cdb49d..614f282c06bf 100644 --- a/lighthouse-core/test/results/artifacts/artifacts.json +++ b/lighthouse-core/test/results/artifacts/artifacts.json @@ -2397,35 +2397,35 @@ ], "TraceElements": [ { - "metricName": "largest-contentful-paint", + "traceEventType": "largest-contentful-paint", "devtoolsNodePath": "1,HTML,1,BODY,0,DIV,1,P", "selector": "body > div > p", "nodeLabel": "This domain is for use in illustrative examples in documents. You may use this …", "snippet": "

" }, { - "metricName": "cumulative-layout-shift", + "traceEventType": "layout-shift", "devtoolsNodePath": "1,HTML,1,BODY,2,DIV,0,DIV,0,DIV,0,ARTICLE,1,DIV,3,H5", "selector": "div.blog-index > article > div.entry-content > h5", "nodeLabel": "Debugging Node.js with Chrome DevTools", "snippet": "

" }, { - "metricName": "cumulative-layout-shift", + "traceEventType": "layout-shift", "devtoolsNodePath": "1,HTML,1,BODY,2,DIV,0,DIV,0,DIV,0,ARTICLE,1,DIV,4,P", "selector": "div.blog-index > article > div.entry-content > p", "nodeLabel": "The canonical guide to using the Chrome DevTools UI for debugging Node.js. It d…", "snippet": "

" }, { - "metricName": "cumulative-layout-shift", + "traceEventType": "layout-shift", "devtoolsNodePath": "1,HTML,1,BODY,2,DIV,0,DIV,0,DIV,0,ARTICLE,1,DIV,5,HR", "selector": "div.blog-index > article > div.entry-content > hr", "nodeLabel": "hr", "snippet": "


" }, { - "metricName": "cumulative-layout-shift", + "traceEventType": "layout-shift", "devtoolsNodePath": "1,HTML,1,BODY,2,DIV,0,DIV,0,DIV,0,ARTICLE,1,DIV,6,P", "selector": "div.blog-index > article > div.entry-content > p", "nodeLabel": "Aside from that, I’ve been busy working on Lighthouse, performance metrics, too…", diff --git a/types/artifacts.d.ts b/types/artifacts.d.ts index 6cf7c9cbc763..ba7ef38daa8d 100644 --- a/types/artifacts.d.ts +++ b/types/artifacts.d.ts @@ -492,13 +492,14 @@ declare global { } export interface TraceElement { - metricName: string; + traceEventType: 'largest-contentful-paint'|'layout-shift'|'animation'; selector: string; nodeLabel?: string; devtoolsNodePath: string; snippet?: string; score?: number; boundingRect: Rect; + nodeId?: number; } export interface ViewportDimensions {