Skip to content

Commit

Permalink
✨ Introduce new tick event cls (Cumulative Layout Shift). (ampproje…
Browse files Browse the repository at this point in the history
…ct#23200)

* Introduce new tick event `cls` (Cumulative Layout Shift).

Fixes ampproject#23154

* Add CLS to tickevent docs

* Lint fixes

* Add PerformanceObserver.supportedEntryTypes type def.

This should be removed when the definition is released in
closure-compiler.

* Fix stubbing of supportedEntryTypes property

* PerformanceObserver is not defined in headless browser test environment

* empty
  • Loading branch information
ericandrewlewis authored and RINDO committed Jul 24, 2019
1 parent e1c6437 commit bf4f36c
Show file tree
Hide file tree
Showing 5 changed files with 339 additions and 3 deletions.
1 change: 1 addition & 0 deletions build-system/compile/compile.js
Expand Up @@ -130,6 +130,7 @@ function compile(entryModuleFilenames, outputDir, outputFilename, options) {
'build-system/dompurify.extern.js',
'build-system/event-timing.extern.js',
'build-system/layout-jank.extern.js',
'build-system/performance-observer.extern.js',
'third_party/closure-compiler/externs/web_animations.js',
'third_party/moment/moment.extern.js',
'third_party/react-externs/externs.js',
Expand Down
29 changes: 29 additions & 0 deletions build-system/performance-observer.extern.js
@@ -0,0 +1,29 @@
/**
* Copyright 2019 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

/**
* @fileoverview Definitions for the PerformanceObserver API.
*
* Created from
* @see https://www.w3.org/TR/performance-timeline-2/#the-performanceobserver-interface
*
* @todo This should be removed when the definitions are released
* in closure-compiler.
*
* @externs
*/

/** @type {!Array} */ PerformanceObserver.supportedEntryTypes;
4 changes: 3 additions & 1 deletion extensions/amp-viewer-integration/TICKEVENTS.md
Expand Up @@ -37,4 +37,6 @@ As an example if we executed `perf.tick('label')` we assume we have a counterpar
| First input delay | `fid` | Millisecond delay in handling the first user input on the page. See https://github.com/WICG/event-timing |
| First input delay, polyfill value | `fid-polyfill` | Millisecond delay in handling the first user input on the page, reported by [a polyfill](https://github.com/GoogleChromeLabs/first-input-delay) |
| Layout Jank, first exit | `lj` | The aggregate jank score when the user leaves the page (navigation, tab switching, dismissing application) for the first time. See https://gist.github.com/skobes/2f296da1b0a88cc785a4bf10a42bca07 |
| Layout Jank, second exit | `lj-2` | The aggregate jank score when the user leaves the page (navigation, tab switching, dismissing application) for the second time. |
| Layout Jank, second exit | `lj-2` | The aggregate jank score when the user leaves the page (navigation, tab switching, dismissing application) for the second time. |
| 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. |
110 changes: 108 additions & 2 deletions src/service/performance-impl.js
Expand Up @@ -116,6 +116,13 @@ export class Performance {
*/
this.jankScoresTicked_ = 0;

/**
* How many times a layout shift metric has been ticked.
*
* @private {number}
*/
this.shiftScoresTicked_ = 0;

/**
* The sum of all layout jank fractions triggered on the page from the
* Layout Jank API.
Expand All @@ -124,8 +131,27 @@ export class Performance {
*/
this.aggregateJankScore_ = 0;

/**
* The sum of all layout shift fractions triggered on the page from the
* Layout Instability API.
*
* @private {number}
*/
this.aggregateShiftScore_ = 0;

/**
* Whether the user agent supports the Layout Instability API.
*
* @private {boolean}
*/
this.supportsLayoutInstabilityAPI_ =
this.win.PerformanceObserver &&
this.win.PerformanceObserver.supportedEntryTypes &&
this.win.PerformanceObserver.supportedEntryTypes.includes('layoutShift');

this.boundOnVisibilityChange_ = this.onVisibilityChange_.bind(this);
this.boundTickLayoutJankScore_ = this.tickLayoutJankScore_.bind(this);
this.boundTickLayoutShiftScore_ = this.tickLayoutShiftScore_.bind(this);
this.onViewerVisibilityChange_ = this.onViewerVisibilityChange_.bind(this);

// Add RTV version as experiment ID, so we can slice the data by version.
Expand Down Expand Up @@ -191,6 +217,29 @@ export class Performance {
this.viewer_.onVisibilityChanged(this.onViewerVisibilityChange_);
}

if (this.supportsLayoutInstabilityAPI_) {
// Register a handler to record the layout shift metric when the page
// enters the hidden lifecycle state.
this.win.addEventListener(
VISIBILITY_CHANGE_EVENT,
this.boundOnVisibilityChange_,
{capture: true}
);

// Safari does not reliably fire the `pagehide` or `visibilitychange`
// events when closing a tab, so we have to use `beforeunload`.
// See https://bugs.webkit.org/show_bug.cgi?id=151234
const platform = Services.platformFor(this.win);
if (platform.isSafari()) {
this.win.addEventListener(
BEFORE_UNLOAD_EVENT,
this.boundTickLayoutShiftScore_
);
}

this.viewer_.onVisibilityChanged(this.onViewerVisibilityChange_);
}

// We don't check `isPerformanceTrackingOn` here since there are some
// events that we call on the viewer even though performance tracking
// is off we only need to know if the AMP page has a messaging
Expand Down Expand Up @@ -252,6 +301,8 @@ export class Performance {
recordedFirstInputDelay = true;
} else if (entry.entryType === 'layoutJank') {
this.aggregateJankScore_ += entry.fraction;
} else if (entry.entryType === 'layoutShift') {
this.aggregateShiftScore_ += entry.value;
}
};

Expand Down Expand Up @@ -280,6 +331,16 @@ export class Performance {
entryTypesToObserve.push('layoutJank');
}

if (this.supportsLayoutInstabilityAPI_) {
// Programmatically read once as currently PerformanceObserver does not
// report past entries as of Chrome 61.
// https://bugs.chromium.org/p/chromium/issues/detail?id=725567
this.win.performance
.getEntriesByType('layoutShift')
.forEach(processEntry);
entryTypesToObserve.push('layoutShift');
}

if (entryTypesToObserve.length === 0) {
return;
}
Expand Down Expand Up @@ -320,7 +381,12 @@ export class Performance {
*/
onVisibilityChange_() {
if (this.win.document.visibilityState === 'hidden') {
this.tickLayoutJankScore_();
if (this.win.PerformanceLayoutJank) {
this.tickLayoutJankScore_();
}
if (this.supportsLayoutInstabilityAPI_) {
this.tickLayoutShiftScore_();
}
}
}

Expand All @@ -330,7 +396,12 @@ export class Performance {
*/
onViewerVisibilityChange_() {
if (this.viewer_.getVisibilityState() === VisibilityState.INACTIVE) {
this.tickLayoutJankScore_();
if (this.win.PerformanceLayoutJank) {
this.tickLayoutJankScore_();
}
if (this.supportsLayoutInstabilityAPI_) {
this.tickLayoutShiftScore_();
}
}
}

Expand Down Expand Up @@ -369,6 +440,41 @@ export class Performance {
}
}

/**
* Tick the layout shift score metric.
*
* A value of the metric is recorded in under two names, `cls` and `cls-2`,
* for the first two times the page transitions into a hidden lifecycle state
* (when the page is navigated a way from, the tab is backgrounded for
* another tab, or the user backgrounds the browser application).
*
* Since we can't reliably detect when a page session finally ends,
* recording the value for these first two events should provide a fair
* amount of visibility into this metric.
*/
tickLayoutShiftScore_() {
if (this.shiftScoresTicked_ === 0) {
this.tickDelta('cls', this.aggregateShiftScore_);
this.flush();
this.shiftScoresTicked_ = 1;
} else if (this.shiftScoresTicked_ === 1) {
this.tickDelta('cls-2', this.aggregateShiftScore_);
this.flush();
this.shiftScoresTicked_ = 2;

// No more work to do, so clean up event listeners.
this.win.removeEventListener(
VISIBILITY_CHANGE_EVENT,
this.boundOnVisibilityChange_,
{capture: true}
);
this.win.removeEventListener(
BEFORE_UNLOAD_EVENT,
this.boundTickLayoutShiftScore_
);
}
}

/**
* Tick fp time based on Chrome's legacy paint timing API when
* appropriate.
Expand Down

0 comments on commit bf4f36c

Please sign in to comment.