From 186768913e476c088caae1974f8710fd602e6847 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Tue, 11 May 2021 13:57:32 -0700 Subject: [PATCH] Update CLS to max session window 5s cap 1s gap --- src/getCLS.ts | 27 ++++++++++-- test/e2e/getCLS-test.js | 97 +++++++++++++++++++++++++++++++++++++++++ test/views/layout.njk | 15 ++++--- 3 files changed, 130 insertions(+), 9 deletions(-) diff --git a/src/getCLS.ts b/src/getCLS.ts index 797b616b..077dfdb8 100644 --- a/src/getCLS.ts +++ b/src/getCLS.ts @@ -32,12 +32,32 @@ export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean) => { let metric = initMetric('CLS', 0); let report: ReturnType; + let sessionValue = 0; + let sessionEntries: PerformanceEntry[] = []; + const entryHandler = (entry: LayoutShift) => { // Only count layout shifts without recent user input. if (!entry.hadRecentInput) { - (metric.value as number) += entry.value; - metric.entries.push(entry); - report(); + const firstSessionEntry = sessionEntries[0]; + const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; + + // If the entry is part of the current session, add it. + // Otherwise, start a new session. + if (sessionValue && + entry.startTime - lastSessionEntry.startTime < 1000 && + entry.startTime - firstSessionEntry.startTime < 5000) { + sessionValue += entry.value; + sessionEntries.push(entry); + } else { + sessionValue = entry.value; + sessionEntries = [entry]; + } + + if (sessionValue > metric.value) { + metric.value = sessionValue; + metric.entries = sessionEntries; + report(); + } } }; @@ -51,6 +71,7 @@ export const getCLS = (onReport: ReportHandler, reportAllChanges?: boolean) => { }); onBFCacheRestore(() => { + sessionValue = 0; metric = initMetric('CLS', 0); report = bindReporter(onReport, metric, reportAllChanges); }); diff --git a/test/e2e/getCLS-test.js b/test/e2e/getCLS-test.js index 85ea2967..ecd1a545 100644 --- a/test/e2e/getCLS-test.js +++ b/test/e2e/getCLS-test.js @@ -72,6 +72,103 @@ describe('getCLS()', async function() { assert.strictEqual(cls.entries.length, 2); }); + it('resets the session after timeout or gap elapses', async function() { + if (!browserSupportsCLS) this.skip(); + + await browser.url('/test/cls'); + + // Wait until all images are loaded and rendered. + await imagesPainted(); + await browser.pause(1000); + + await stubVisibilityChange('hidden'); + await beaconCountIs(1); + + const [cls1] = await getBeacons(); + + assert(cls1.value >= 0); + assert(cls1.id.match(/^v1-\d+-\d+$/)); + assert.strictEqual(cls1.name, 'CLS'); + assert.strictEqual(cls1.value, cls1.delta); + assert.strictEqual(cls1.entries.length, 2); + + await browser.pause(1000); + await stubVisibilityChange('visible'); + await clearBeacons(); + + // Force 2 layout shifts, totaling 0.5. + await browser.executeAsync((done) => { + document.body.style.overflow = 'hidden'; // Prevent scroll bars. + document.querySelector('main').style.left = '25vmax'; + setTimeout(() => { + document.querySelector('main').style.left = '0px'; + done(); + }, 50); + }); + + await stubVisibilityChange('hidden'); + await beaconCountIs(1); + + const [cls2] = await getBeacons(); + + // The value should be exactly 0.5, but round just in case. + assert.strictEqual(Math.round(cls2.value * 100) / 100, 0.5); + assert.strictEqual(cls2.name, 'CLS'); + assert.strictEqual(cls2.value, cls1.value + cls2.delta); + assert.strictEqual(cls2.entries.length, 2); + assert(cls2.id.match(/^v1-\d+-\d+$/)); + + await browser.pause(1000); + await stubVisibilityChange('visible'); + await clearBeacons(); + + // Force 4 separate layout shifts, totaling 1.5. + await browser.executeAsync((done) => { + document.querySelector('main').style.left = '25vmax'; + setTimeout(() => { + document.querySelector('main').style.left = '0px'; + setTimeout(() => { + document.querySelector('main').style.left = '50vmax'; + setTimeout(() => { + document.querySelector('main').style.left = '0px'; + done(); + }, 50); + }, 50); + }, 50); + }); + + await stubVisibilityChange('hidden'); + await beaconCountIs(1); + + const [cls3] = await getBeacons(); + + // The value should be exactly 1.5, but round just in case. + assert.strictEqual(Math.round(cls3.value * 100) / 100, 1.5); + assert.strictEqual(cls3.name, 'CLS'); + assert.strictEqual(cls3.value, cls2.value + cls3.delta); + assert.strictEqual(cls3.entries.length, 4); + assert(cls3.id.match(/^v1-\d+-\d+$/)); + + await browser.pause(1000); + await stubVisibilityChange('visible'); + await clearBeacons(); + + // Force 2 layout shifts, totalling 1.0 (less than the previous max). + await browser.executeAsync((done) => { + document.querySelector('main').style.left = '50vmax'; + setTimeout(() => { + document.querySelector('main').style.left = '0px'; + done(); + }, 50); + }); + + // Wait a bit to ensure no beacons were sent. + await browser.pause(1000); + + const beacons = await getBeacons(); + assert.strictEqual(beacons.length, 0); + }); + it('does not report if the browser does not support CLS', async function() { if (browserSupportsCLS) this.skip(); diff --git a/test/views/layout.njk b/test/views/layout.njk index 2b11146f..45b8f261 100644 --- a/test/views/layout.njk +++ b/test/views/layout.njk @@ -44,19 +44,22 @@ {% endif %} {% block head %}{% endblock %}