From ba2d1d3aa2ecbb707a2c69eed7f5f28f3f9520b3 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sun, 13 Nov 2022 21:11:04 -0800 Subject: [PATCH] Report initial CLS value when reportAllChanges --- src/lib/doubleRAF.ts | 19 ++++++ src/onCLS.ts | 38 ++++++----- src/onFCP.ts | 9 ++- src/onLCP.ts | 11 ++-- test/e2e/onCLS-test.js | 140 +++++++++++++++++++++++++++-------------- test/views/cls.njk | 2 +- 6 files changed, 142 insertions(+), 77 deletions(-) create mode 100644 src/lib/doubleRAF.ts diff --git a/src/lib/doubleRAF.ts b/src/lib/doubleRAF.ts new file mode 100644 index 00000000..c76d1421 --- /dev/null +++ b/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())); +}; diff --git a/src/onCLS.ts b/src/onCLS.ts index 56257030..7d4a6f91 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -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 @@ -56,14 +54,12 @@ 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; + + let fcpValue = -1; + let sessionValue = 0; + let sessionEntries: PerformanceEntry[] = []; const onReportWrapped: CLSReportCallback = (arg) => { if (fcpValue > -1) { @@ -71,12 +67,6 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { } }; - let metric = initMetric('CLS', 0); - let report: ReturnType; - - let sessionValue = 0; - let sessionEntries: PerformanceEntry[] = []; - // const handleEntries = (entries: Metric['entries']) => { const handleEntries = (entries: LayoutShift[]) => { entries.forEach((entry) => { @@ -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); @@ -128,6 +130,8 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { metric = initMetric('CLS', 0); report = bindReporter( onReportWrapped, metric, thresholds, opts!.reportAllChanges); + + doubleRAF(() => report()); }); } }); diff --git a/src/onFCP.ts b/src/onFCP.ts index 62c97437..feaadbdd 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -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'; @@ -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); }); }); } diff --git a/src/onLCP.ts b/src/onLCP.ts index 4796d3e1..551d8d5f 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -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'; @@ -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); }); }); } diff --git a/test/e2e/onCLS-test.js b/test/e2e/onCLS-test.js index 071755d6..3a116e26 100644 --- a/test/e2e/onCLS-test.js +++ b/test/e2e/onCLS-test.js @@ -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'); @@ -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'); @@ -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'); @@ -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() { @@ -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(); @@ -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() { diff --git a/test/views/cls.njk b/test/views/cls.njk index 36fd7212..237c57b9 100644 --- a/test/views/cls.njk +++ b/test/views/cls.njk @@ -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 = {