Skip to content

Commit

Permalink
Report initial CLS value when reportAllChanges
Browse files Browse the repository at this point in the history
  • Loading branch information
philipwalton committed Nov 15, 2022
1 parent ce30cb6 commit ba2d1d3
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 77 deletions.
19 changes: 19 additions & 0 deletions src/lib/doubleRAF.ts
@@ -0,0 +1,19 @@
/*
* Copyright 2022 Google LLC
*
* 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
*
* https://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.
*/

export const doubleRAF = (cb: () => unknown) => {
requestAnimationFrame(() => requestAnimationFrame(() => cb()));
};
38 changes: 21 additions & 17 deletions src/onCLS.ts
Expand Up @@ -19,14 +19,12 @@ import {initMetric} from './lib/initMetric.js';
import {observe} from './lib/observe.js';
import {onHidden} from './lib/onHidden.js';
import {bindReporter} from './lib/bindReporter.js';
import {doubleRAF} from './lib/doubleRAF.js';
import {whenActivated} from './lib/whenActivated.js';
import {onFCP} from './onFCP.js';
import {CLSMetric, CLSReportCallback, ReportOpts} from './types.js';


let isMonitoringFCP = false;
let fcpValue = -1;

/**
* Calculates the [CLS](https://web.dev/cls/) value for the current page and
* calls the `callback` function once the value is ready to be reported, along
Expand Down Expand Up @@ -56,27 +54,19 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
// https://web.dev/cls/#what-is-a-good-cls-score
const thresholds = [0.1, 0.25];

// Start monitoring FCP so we can only report CLS if FCP is also reported.
// Note: this is done to match the current behavior of CrUX.
if (!isMonitoringFCP) {
onFCP((metric) => {
fcpValue = metric.value;
});
isMonitoringFCP = true;
}
let metric = initMetric('CLS');
let report: ReturnType<typeof bindReporter>;

let fcpValue = -1;
let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];

const onReportWrapped: CLSReportCallback = (arg) => {
if (fcpValue > -1) {
onReport(arg);
}
};

let metric = initMetric('CLS', 0);
let report: ReturnType<typeof bindReporter>;

let sessionValue = 0;
let sessionEntries: PerformanceEntry[] = [];

// const handleEntries = (entries: Metric['entries']) => {
const handleEntries = (entries: LayoutShift[]) => {
entries.forEach((entry) => {
Expand Down Expand Up @@ -115,6 +105,18 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
report = bindReporter(
onReportWrapped, metric, thresholds, opts!.reportAllChanges);

// Start monitoring FCP so we can only report CLS if FCP is also reported.
// Note: this is done to match the current behavior of CrUX.
// Also, if there have not been any layout shifts when FCP is dispatched,
// call "report" with a zero value
onFCP((fcpMetric) => {
fcpValue = fcpMetric.value;
if (metric.value < 0) {
metric.value = 0;
report();
}
});

onHidden(() => {
handleEntries(po.takeRecords() as CLSMetric['entries']);
report(true);
Expand All @@ -128,6 +130,8 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => {
metric = initMetric('CLS', 0);
report = bindReporter(
onReportWrapped, metric, thresholds, opts!.reportAllChanges);

doubleRAF(() => report());
});
}
});
Expand Down
9 changes: 4 additions & 5 deletions src/onFCP.ts
Expand Up @@ -16,6 +16,7 @@

import {onBFCacheRestore} from './lib/bfcache.js';
import {bindReporter} from './lib/bindReporter.js';
import {doubleRAF} from './lib/doubleRAF.js';
import {getActivationStart} from './lib/getActivationStart.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
Expand Down Expand Up @@ -73,11 +74,9 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => {
report = bindReporter(
onReport, metric, thresholds, opts!.reportAllChanges);

requestAnimationFrame(() => {
requestAnimationFrame(() => {
metric.value = performance.now() - event.timeStamp;
report(true);
});
doubleRAF(() => {
metric.value = performance.now() - event.timeStamp;
report(true);
});
});
}
Expand Down
11 changes: 5 additions & 6 deletions src/onLCP.ts
Expand Up @@ -16,6 +16,7 @@

import {onBFCacheRestore} from './lib/bfcache.js';
import {bindReporter} from './lib/bindReporter.js';
import {doubleRAF} from './lib/doubleRAF.js';
import {getActivationStart} from './lib/getActivationStart.js';
import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js';
import {initMetric} from './lib/initMetric.js';
Expand Down Expand Up @@ -101,12 +102,10 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => {
report = bindReporter(
onReport, metric, thresholds, opts!.reportAllChanges);

requestAnimationFrame(() => {
requestAnimationFrame(() => {
metric.value = performance.now() - event.timeStamp;
reportedMetricIDs[metric.id] = true;
report(true);
});
doubleRAF(() => {
metric.value = performance.now() - event.timeStamp;
reportedMetricIDs[metric.id] = true;
report(true);
});
});
}
Expand Down
140 changes: 92 additions & 48 deletions test/e2e/onCLS-test.js
Expand Up @@ -209,26 +209,34 @@ describe('onCLS()', async function() {
await browser.url('/test/cls?reportAllChanges=1');

// Beacons should be sent as soon as layout shifts occur, wait for them.
await beaconCountIs(2);
await beaconCountIs(3);

const [cls1, cls2] = await getBeacons();
const [cls1, cls2, cls3] = await getBeacons();

assert(cls1.value >= 0);
assert.strictEqual(cls1.value, 0);
assert(cls1.id.match(/^v3-\d+-\d+$/));
assert.strictEqual(cls1.name, 'CLS');
assert.strictEqual(cls1.value, cls1.delta);
assert.strictEqual(cls1.rating, 'good');
assert.strictEqual(cls1.entries.length, 1);
assert.strictEqual(cls1.entries.length, 0);
assert.match(cls1.navigationType, /navigate|reload/);

assert(cls2.value >= cls1.value);
assert(cls2.value >= 0);
assert.strictEqual(cls2.name, 'CLS');
assert.strictEqual(cls2.id, cls1.id);
assert.strictEqual(cls2.value, cls1.value + cls2.delta);
assert.strictEqual(cls2.value, cls1.delta + cls2.delta);
assert.strictEqual(cls2.rating, 'good');
assert.strictEqual(cls2.entries.length, 2);
assert.strictEqual(cls2.entries.length, 1);
assert.match(cls2.navigationType, /navigate|reload/);

assert(cls3.value >= cls2.value);
assert.strictEqual(cls3.name, 'CLS');
assert.strictEqual(cls3.id, cls2.id);
assert.strictEqual(cls3.value, cls2.value + cls3.delta);
assert.strictEqual(cls3.rating, 'good');
assert.strictEqual(cls3.entries.length, 2);
assert.match(cls3.navigationType, /navigate|reload/);

await clearBeacons();
await stubVisibilityChange('hidden');

Expand All @@ -245,25 +253,34 @@ describe('onCLS()', async function() {
await browser.url('/test/cls?reportAllChanges=1');

// Beacons should be sent as soon as layout shifts occur, wait for them.
await beaconCountIs(2);
await beaconCountIs(3);

const [cls1, cls2] = await getBeacons();
const [cls1, cls2, cls3] = await getBeacons();

assert(cls1.value >= 0);
assert.strictEqual(cls1.value, 0);
assert(cls1.id.match(/^v3-\d+-\d+$/));
assert.strictEqual(cls1.name, 'CLS');
assert.strictEqual(cls1.value, cls1.delta);
assert.strictEqual(cls1.rating, 'good');
assert.strictEqual(cls1.entries.length, 1);
assert.strictEqual(cls1.entries.length, 0);
assert.match(cls1.navigationType, /navigate|reload/);

assert(cls2.value >= cls1.value);
assert(cls2.value >= 0);
assert.strictEqual(cls2.name, 'CLS');
assert.strictEqual(cls2.id, cls1.id);
assert.strictEqual(cls2.value, cls1.value + cls2.delta);
assert.strictEqual(cls2.value, cls1.delta + cls2.delta);
assert.strictEqual(cls2.rating, 'good');
assert.strictEqual(cls2.entries.length, 2);
assert.strictEqual(cls2.entries.length, 1);
assert.match(cls2.navigationType, /navigate|reload/);

assert(cls3.value >= cls2.value);
assert.strictEqual(cls3.name, 'CLS');
assert.strictEqual(cls3.id, cls2.id);
assert.strictEqual(cls3.value, cls2.value + cls3.delta);
assert.strictEqual(cls3.rating, 'good');
assert.strictEqual(cls3.entries.length, 2);
assert.match(cls3.navigationType, /navigate|reload/);

// Unload the page after no new shifts have occurred.
await clearBeacons();
await browser.url('about:blank');
Expand Down Expand Up @@ -323,24 +340,34 @@ describe('onCLS()', async function() {
if (!browserSupportsCLS) this.skip();

await browser.url(`/test/cls?reportAllChanges=1`);
await beaconCountIs(2);
await beaconCountIs(3);

const [cls1, cls2] = await getBeacons();
const [cls1, cls2, cls3] = await getBeacons();

assert(cls1.value > 0);
assert.strictEqual(cls1.value, 0);
assert(cls1.id.match(/^v3-\d+-\d+$/));
assert.strictEqual(cls1.name, 'CLS');
assert.strictEqual(cls1.value, cls1.delta);
assert.strictEqual(cls1.entries.length, 1);
assert.strictEqual(cls1.rating, 'good');
assert.strictEqual(cls1.entries.length, 0);
assert.match(cls1.navigationType, /navigate|reload/);

assert(cls2.value > cls1.value);
assert(cls2.value >= 0);
assert.strictEqual(cls2.name, 'CLS');
assert.strictEqual(cls2.id, cls1.id);
assert.strictEqual(cls2.value, cls1.value + cls2.delta);
assert.strictEqual(cls2.value, cls1.delta + cls2.delta);
assert.strictEqual(cls2.rating, 'good');
assert.strictEqual(cls2.entries.length, 2);
assert.strictEqual(cls2.entries.length, 1);
assert.match(cls2.navigationType, /navigate|reload/);

assert(cls3.value >= cls2.value);
assert.strictEqual(cls3.name, 'CLS');
assert.strictEqual(cls3.id, cls2.id);
assert.strictEqual(cls3.value, cls2.value + cls3.delta);
assert.strictEqual(cls3.rating, 'good');
assert.strictEqual(cls3.entries.length, 2);
assert.match(cls3.navigationType, /navigate|reload/);

// Unload the page after no new shifts have occurred.
await clearBeacons();
await stubVisibilityChange('hidden');
Expand All @@ -352,15 +379,15 @@ describe('onCLS()', async function() {
await triggerLayoutShift();

await beaconCountIs(1);
const [cls3] = await getBeacons();

assert(cls3.value > cls2.value);
assert.strictEqual(cls3.name, 'CLS');
assert.strictEqual(cls3.id, cls2.id);
assert.strictEqual(cls3.value, cls2.value + cls3.delta);
assert.strictEqual(cls3.rating, 'good');
assert.strictEqual(cls3.entries.length, 3);
assert.match(cls3.navigationType, /navigate|reload/);
const [cls4] = await getBeacons();

assert(cls4.value > cls3.value);
assert.strictEqual(cls4.name, 'CLS');
assert.strictEqual(cls4.id, cls3.id);
assert.strictEqual(cls4.value, cls3.value + cls4.delta);
assert.strictEqual(cls4.rating, 'good');
assert.strictEqual(cls4.entries.length, 3);
assert.match(cls4.navigationType, /navigate|reload/);
});

it('continues reporting after bfcache restore (reportAllChanges === false)', async function() {
Expand Down Expand Up @@ -426,25 +453,34 @@ describe('onCLS()', async function() {
if (!browserSupportsCLS) this.skip();

await browser.url(`/test/cls?reportAllChanges=1`);
await beaconCountIs(2);
await beaconCountIs(3);

const [cls1, cls2] = await getBeacons();
const [cls1, cls2, cls3] = await getBeacons();

assert(cls1.value > 0);
assert.strictEqual(cls1.value, 0);
assert(cls1.id.match(/^v3-\d+-\d+$/));
assert.strictEqual(cls1.name, 'CLS');
assert.strictEqual(cls1.value, cls1.delta);
assert.strictEqual(cls1.entries.length, 1);
assert.strictEqual(cls1.rating, 'good');
assert.strictEqual(cls1.entries.length, 0);
assert.match(cls1.navigationType, /navigate|reload/);

assert(cls2.value > cls1.value);
assert(cls2.value >= 0);
assert.strictEqual(cls2.name, 'CLS');
assert.strictEqual(cls2.id, cls1.id);
assert.strictEqual(cls2.value, cls1.value + cls2.delta);
assert.strictEqual(cls2.value, cls1.delta + cls2.delta);
assert.strictEqual(cls2.rating, 'good');
assert.strictEqual(cls2.entries.length, 2);
assert.strictEqual(cls2.entries.length, 1);
assert.match(cls2.navigationType, /navigate|reload/);

assert(cls3.value >= cls2.value);
assert.strictEqual(cls3.name, 'CLS');
assert.strictEqual(cls3.id, cls2.id);
assert.strictEqual(cls3.value, cls2.value + cls3.delta);
assert.strictEqual(cls3.rating, 'good');
assert.strictEqual(cls3.entries.length, 2);
assert.match(cls3.navigationType, /navigate|reload/);

await clearBeacons();
await stubForwardBack();

Expand All @@ -453,17 +489,25 @@ describe('onCLS()', async function() {

await triggerLayoutShift();

await beaconCountIs(1);
const [cls3] = await getBeacons();

assert(cls3.value > 0);
assert(cls3.id.match(/^v3-\d+-\d+$/));
assert(cls3.id !== cls2.id);
assert.strictEqual(cls3.name, 'CLS');
assert.strictEqual(cls3.value, cls3.delta);
assert.strictEqual(cls3.rating, 'good');
assert.strictEqual(cls3.entries.length, 1);
assert.strictEqual(cls3.navigationType, 'back-forward-cache');
await beaconCountIs(2);
const [cls4, cls5] = await getBeacons();

assert.strictEqual(cls4.value, 0);
assert(cls4.id.match(/^v3-\d+-\d+$/));
assert(cls4.id !== cls3.id);
assert.strictEqual(cls4.name, 'CLS');
assert.strictEqual(cls4.value, cls4.delta);
assert.strictEqual(cls4.rating, 'good');
assert.strictEqual(cls4.entries.length, 0);
assert.strictEqual(cls4.navigationType, 'back-forward-cache');

assert(cls5.value > 0);
assert.strictEqual(cls5.id, cls4.id);
assert.strictEqual(cls5.name, 'CLS');
assert.strictEqual(cls5.value, cls4.delta + cls5.delta);
assert.strictEqual(cls5.rating, 'good');
assert.strictEqual(cls5.entries.length, 1);
assert.strictEqual(cls5.navigationType, 'back-forward-cache');
});

it('reports zero if no layout shifts occurred on first visibility hidden (reportAllChanges === false)', async function() {
Expand Down
2 changes: 1 addition & 1 deletion test/views/cls.njk
Expand Up @@ -50,7 +50,7 @@
onCLS((cls) => {
// Log for easier manual testing.
console.log(window.cls = cls);
console.log(cls);
// Sources is verbose to serialize, so remove first.
cls = {
Expand Down

0 comments on commit ba2d1d3

Please sign in to comment.