diff --git a/.changeset/kind-pens-knock.md b/.changeset/kind-pens-knock.md new file mode 100644 index 0000000000..6335c3eaa0 --- /dev/null +++ b/.changeset/kind-pens-knock.md @@ -0,0 +1,6 @@ +--- +'@shopify/koa-performance': patch +'@shopify/performance': patch +--- + +Added support for Cumulative Layout Shift in the performance and koa-performance packages. diff --git a/packages/koa-performance/src/enums.ts b/packages/koa-performance/src/enums.ts index a97b490b12..7797b18d6e 100644 --- a/packages/koa-performance/src/enums.ts +++ b/packages/koa-performance/src/enums.ts @@ -5,6 +5,7 @@ export enum LifecycleMetric { TimeToFirstPaint = 'time_to_first_paint', DomContentLoaded = 'dom_content_loaded', FirstInputDelay = 'first_input_delay', + CumulativeLayoutShift = 'cumulative_layout_shift', Load = 'dom_load', } diff --git a/packages/koa-performance/src/middleware.ts b/packages/koa-performance/src/middleware.ts index e3472d18a0..0c026ef017 100644 --- a/packages/koa-performance/src/middleware.ts +++ b/packages/koa-performance/src/middleware.ts @@ -121,11 +121,15 @@ export function clientPerformanceMetrics({ tags, }); } - const value = + let value = event.type === EventType.FirstInputDelay ? event.duration : event.start; + // For CLS we multiply the value by 100 to avoid statsd issues with real numbers. + value = + event.type === EventType.CumulativeLayoutShift ? value * 100 : value; + metrics.push({ name: eventMetricName(event), value: Math.round(value), @@ -261,6 +265,7 @@ const EVENT_METRIC_MAPPING = { [EventType.TimeToFirstPaint]: LifecycleMetric.TimeToFirstPaint, [EventType.DomContentLoaded]: LifecycleMetric.DomContentLoaded, [EventType.FirstInputDelay]: LifecycleMetric.FirstInputDelay, + [EventType.CumulativeLayoutShift]: LifecycleMetric.CumulativeLayoutShift, [EventType.Load]: LifecycleMetric.Load, }; diff --git a/packages/koa-performance/src/tests/middleware.test.ts b/packages/koa-performance/src/tests/middleware.test.ts index 8cb45942c0..37b99a06d2 100644 --- a/packages/koa-performance/src/tests/middleware.test.ts +++ b/packages/koa-performance/src/tests/middleware.test.ts @@ -240,10 +240,15 @@ describe('client metrics middleware', () => { 456, ); + const clsEvent = createLifecycleEvent( + EventType.CumulativeLayoutShift, + 0.1, + ); + const context = createMockContext({ method: Method.Post, requestBody: createBody({ - events: [ttfbEvent, ttfcpEvent, ttlcpEvent], + events: [ttfbEvent, ttfcpEvent, ttlcpEvent, clsEvent], }), }); @@ -253,6 +258,8 @@ describe('client metrics middleware', () => { ); }); + expect(statsd.distribution).toHaveBeenCalledTimes(4); + expect(statsd.distribution).toHaveBeenCalledWith( 'time_to_first_byte', ttfbEvent.start, @@ -270,6 +277,12 @@ describe('client metrics middleware', () => { ttlcpEvent.start, expect.any(Object), ); + + expect(statsd.distribution).toHaveBeenCalledWith( + 'cumulative_layout_shift', + clsEvent.start * 100, + expect.any(Object), + ); }); it('sends distribution for redirect_duration when ttfb is supplied with relevant metadata', async () => { diff --git a/packages/performance/README.md b/packages/performance/README.md index 38987731e2..0bdc69ab63 100644 --- a/packages/performance/README.md +++ b/packages/performance/README.md @@ -135,6 +135,11 @@ Learn more about [DOM Content Loaded](https://developer.mozilla.org/en-US/docs/W The time from when a user first interacts with your site to the time when the browser is able to respond to that interaction. Learn more about [first Input Delay](https://developers.google.com/web/updates/2018/05/first-input-delay). +##### Cumulative Layout Shift (`EventType.CumulativeLayoutShift`) + +CLS is a measure of the largest burst of layout shift scores for every unexpected layout shift that occurs during the entire lifespan of a page. +Learn more about [Cumulative Layout Shift](https://web.dev/cls/) + ##### Load Event (`EventType.Load`) The time until the DOM and all its styles and synchronous scripts have loaded. diff --git a/packages/performance/src/performance.ts b/packages/performance/src/performance.ts index bb595970b0..edba707448 100644 --- a/packages/performance/src/performance.ts +++ b/packages/performance/src/performance.ts @@ -1,15 +1,15 @@ -import {onLCP, onFID, onFCP, onTTFB} from 'web-vitals'; +import {onCLS, onFCP, onFID, onLCP, onTTFB} from 'web-vitals'; import {InflightNavigation} from './inflight'; import type {Navigation} from './navigation'; import { + hasGlobal, now, + referenceTime, + supportsPerformanceObserver, withEntriesOfType, withNavigation, withTiming, - supportsPerformanceObserver, - referenceTime, - hasGlobal, getResourceTypeFromEntry, } from './utilities'; import type {Event, LifecycleEvent} from './types'; @@ -174,6 +174,14 @@ export class Performance { }); }); + onCLS((metric) => { + this.lifecycleEvent({ + type: EventType.CumulativeLayoutShift, + start: metric.value, + duration: 0, + }); + }); + onLCP((metric) => { this.lifecycleEvent({ type: EventType.TimeToLargestContentfulPaint, diff --git a/packages/performance/src/types.ts b/packages/performance/src/types.ts index ed3ee72c41..6c5bda7666 100644 --- a/packages/performance/src/types.ts +++ b/packages/performance/src/types.ts @@ -5,6 +5,7 @@ export enum EventType { TimeToLargestContentfulPaint = 'ttlcp', DomContentLoaded = 'dcl', FirstInputDelay = 'fid', + CumulativeLayoutShift = 'cls', Load = 'load', LongTask = 'longtask', Usable = 'usable', @@ -53,6 +54,11 @@ export interface FirstInputDelayEvent extends BasicEvent { metadata?: undefined; } +export interface CumulativeLayoutShift extends BasicEvent { + type: EventType.CumulativeLayoutShift; + metadata?: undefined; +} + export interface LoadEvent extends BasicEvent { type: EventType.Load; metadata?: undefined; @@ -94,6 +100,7 @@ export type LifecycleEvent = | TimeToLargestContentfulPaintEvent | DomContentLoadedEvent | FirstInputDelayEvent + | CumulativeLayoutShift | LoadEvent; export type Event = @@ -112,6 +119,7 @@ export interface EventMap { [EventType.TimeToLargestContentfulPaint]: TimeToLargestContentfulPaintEvent; [EventType.DomContentLoaded]: DomContentLoadedEvent; [EventType.FirstInputDelay]: FirstInputDelayEvent; + [EventType.CumulativeLayoutShift]: CumulativeLayoutShift; [EventType.Load]: LoadEvent; [EventType.ScriptDownload]: ScriptDownloadEvent; [EventType.StyleDownload]: StyleDownloadEvent;