Skip to content

Commit

Permalink
performance-impl: start tracking lcp element (#35123)
Browse files Browse the repository at this point in the history
* performance-impl: start tracking lcp element

* tagName --> nodeName

* include video

* Update src/service/performance-impl.js

Co-authored-by: Justin Ridgewell <justin@ridgewell.name>

* Revert "Update src/service/performance-impl.js"

This reverts commit 8cb0f4a.

* Update src/service/performance-impl.js

Co-authored-by: Justin Ridgewell <justin@ridgewell.name>

Co-authored-by: Justin Ridgewell <justin@ridgewell.name>
  • Loading branch information
samouri and jridgewell committed Sep 18, 2021
1 parent ff08f03 commit 71f17a6
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 4 deletions.
1 change: 1 addition & 0 deletions extensions/amp-viewer-integration/TICKEVENTS.md
Expand Up @@ -27,6 +27,7 @@ As an example if we executed `perf.tick('label')` we assume we have a counterpar
| Cumulative Layout Shift, first exit | `cls` | The aggregate layout shift score when the user leaves the page (navigation, tab switching, dismissing application) for the first time. See https://web.dev/layout-instability-api |
| Cumulative Layout Shift, second exit | `cls-2` | The aggregate layout shift score when the user leaves the page (navigation, tab switching, dismissing application) for the second time. |
| Largest Contentful Paint | `lcp` | The time in milliseconds for the largest contentful element to display. |
| Largest Contentful Paint Type | `lcpt` | The type LCP Element type (see `LCP_ELEMENT_TYPE`) |
| Largest Contentful Paint (since visible) | `lcpv` | The time in ms for largest contentful element to display, offset by first visible time. Based on render time, falls back to load time. |
| DOM Complete | `domComplete` | Time immediately before the browser sets the current document readiness of the current document to complete |
| DOM Content Loaded Event End | `domContentLoadedEventEnd` | Time immediately after the current document's DOMContentLoaded event completes |
Expand Down
1 change: 1 addition & 0 deletions src/core/constants/enums.js
Expand Up @@ -34,6 +34,7 @@ export const TickLabel = {
GOOD_FRAME_PROBABILITY: 'gfp',
INSTALL_STYLES: 'is',
LARGEST_CONTENTFUL_PAINT: 'lcp',
LARGEST_CONTENTFUL_PAINT_TYPE: 'lcpt',
LARGEST_CONTENTFUL_PAINT_VISIBLE: 'lcpv',
LONG_TASKS_CHILD: 'ltc',
LONG_TASKS_SELF: 'lts',
Expand Down
57 changes: 57 additions & 0 deletions src/service/performance-impl.js
Expand Up @@ -36,6 +36,18 @@ const TAG = 'Performance';
*/
let TickEventDef;

/**
* @enum {number}
*/
export const LCP_ELEMENT_TYPE = {
other: 0,
image: 1,
video: 2,
ad: 3,
carousel: 4,
bcarousel: 5,
};

/**
* Performance holds the mechanism to call `tick` to stamp out important
* events in the lifecycle of the AMP runtime. It can hold a small amount
Expand Down Expand Up @@ -173,6 +185,12 @@ export class Performance {
*/
this.largestContentfulPaint_ = null;

/**
* Which type of element was chosen as the LCP.
* @private {LCP_ELEMENT_TYPE}
*/
this.largestContentfulPaintType_ = null;

this.onAmpDocVisibilityChange_ = this.onAmpDocVisibilityChange_.bind(this);

// Add RTV version as experiment ID, so we can slice the data by version.
Expand Down Expand Up @@ -341,6 +359,24 @@ export class Performance {
}
} else if (entry.entryType === 'largest-contentful-paint') {
this.largestContentfulPaint_ = entry.startTime;
if (entry.element) {
const {tagName} = getOutermostAmpElement(entry.element);
if (tagName === 'IMG' || tagName === 'AMP-IMG') {
this.largestContentfulPaintType_ = LCP_ELEMENT_TYPE.image;
} else if (tagName === 'VIDEO' || tagName === 'AMP-VIDEO') {
this.largestContentfulPaintType_ = LCP_ELEMENT_TYPE.video;
} else if (tagName === 'AMP-CAROUSEL') {
this.largestContentfulPaintType_ = LCP_ELEMENT_TYPE.carousel;
} else if (tagName === 'AMP-BASE-CAROUSEL') {
this.largestContentfulPaintType_ = LCP_ELEMENT_TYPE.bcarousel;
} else if (tagName === 'AMP-AD') {
this.largestContentfulPaintType_ = LCP_ELEMENT_TYPE.ad;
} else {
this.largestContentfulPaintType_ = LCP_ELEMENT_TYPE.other;
}
} else {
this.largestContentfulPaintType_ = LCP_ELEMENT_TYPE.other;
}
} else if (entry.entryType == 'navigation' && !recordedNavigation) {
[
'domComplete',
Expand Down Expand Up @@ -559,6 +595,10 @@ export class Performance {
return;
}

this.tickDelta(
TickLabel.LARGEST_CONTENTFUL_PAINT_TYPE,
this.largestContentfulPaintType_
);
this.tickDelta(
TickLabel.LARGEST_CONTENTFUL_PAINT,
this.largestContentfulPaint_
Expand Down Expand Up @@ -839,6 +879,23 @@ export class Performance {
}
}

/**
* Traverse node ancestors and return the highest level amp element.
* Returns the given node if none are found.
*
* @param {!HTMLElement} node
* @return {!HTMLElement}
*/
function getOutermostAmpElement(node) {
let max = node;
while ((node = node.parentNode) != null) {
if (node.nodeName.startsWith('AMP-')) {
max = node;
}
}
return max;
}

/**
* @param {!Window} window
*/
Expand Down
67 changes: 63 additions & 4 deletions test/unit/test-performance.js
Expand Up @@ -5,6 +5,7 @@ import {VisibilityState} from '#core/constants/visibility-state';
import {Services} from '#service';
import {installRuntimeServices} from '#service/core-services';
import {
LCP_ELEMENT_TYPE,
Performance,
installPerformanceService,
} from '#service/performance-impl';
Expand Down Expand Up @@ -1044,15 +1045,73 @@ describes.realWin('PeformanceObserver metrics', {amp: true}, (env) => {
// The document has become hidden, e.g. via the user switching tabs.
toggleVisibility(perf, false);

const lcpEvents = perf.events_.filter(({label}) =>
label.startsWith('lcp')
);
expect(lcpEvents.length).to.equal(2);
const lcpEvents = perf.events_.filter(({label}) => label === 'lcp');
expect(lcpEvents.length).to.equal(1);
expect(lcpEvents).deep.include({
label: 'lcp',
delta: 23,
});
});

it('should include lcp type', async () => {
// Fake the Performance API.
env.win.PerformanceObserver.supportedEntryTypes = [
'largest-contentful-paint',
];

installPerformanceService(env.win);
const perf = Services.performanceFor(env.win);
perf.coreServicesAvailable();
expect(perf.events_.length).to.equal(0);

// Fake an img being the LCP Element
performanceObserver.triggerCallback({
getEntries() {
return [
{
entryType: 'largest-contentful-paint',
startTime: 12,
element: document.createElement('img'),
},
];
},
});
// Flush LCP
toggleVisibility(perf, false);
toggleVisibility(perf, true);

// Fake an amp-img nested within an amp-carousel.
const parent = document.createElement('amp-carousel');
const child = document.createElement('amp-img');
parent.appendChild(child);
performanceObserver.triggerCallback({
getEntries() {
return [
{
entryType: 'largest-contentful-paint',
loadTime: 23,
renderTime: undefined,
startTime: 23,
element: child,
},
];
},
});
// Flush LCP again.
toggleVisibility(perf, false);

const lcptEvents = perf.events_.filter(({label}) =>
label.startsWith('lcpt')
);
expect(lcptEvents).deep.include({
label: 'lcpt',
delta: LCP_ELEMENT_TYPE.image,
});
expect(lcptEvents).deep.include({
label: 'lcpt',
delta: LCP_ELEMENT_TYPE.carousel,
});
});
});

describe('should forward first input metrics for performance entries', () => {
Expand Down

0 comments on commit 71f17a6

Please sign in to comment.