diff --git a/packages/next/client/index.js b/packages/next/client/index.js
index 124b8ab35dc1efc..1e1721ae1abe3a9 100644
--- a/packages/next/client/index.js
+++ b/packages/next/client/index.js
@@ -11,6 +11,11 @@ import { HeadManagerContext } from '../next-server/lib/head-manager-context'
import { RouterContext } from '../next-server/lib/router-context'
import { parse as parseQs, stringify as stringifyQs } from 'querystring'
import { isDynamicRoute } from '../next-server/lib/router/utils/is-dynamic'
+import {
+ observeLayoutShift,
+ observeLargestContentfulPaint,
+ observePaint,
+} from './performance-relayer'
///
@@ -161,8 +166,14 @@ export default async ({ webpackHMR: passedWebpackHMR } = {}) => {
const { page: app, mod } = await pageLoader.loadPageScript('/_app')
App = app
if (mod && mod.unstable_onPerformanceData) {
- onPerfEntry = function({ name, startTime, value, duration }) {
- mod.unstable_onPerformanceData({ name, startTime, value, duration })
+ onPerfEntry = function({ name, startTime, value, duration, entryType }) {
+ mod.unstable_onPerformanceData({
+ name,
+ startTime,
+ value,
+ duration,
+ entryType,
+ })
}
}
@@ -313,14 +324,9 @@ function renderReactElement(reactEl, domEl) {
if (onPerfEntry && ST) {
try {
- const observer = new PerformanceObserver(list => {
- list.getEntries().forEach(onPerfEntry)
- })
- // Start observing paint entry types.
- observer.observe({
- type: 'paint',
- buffered: true,
- })
+ observeLayoutShift(onPerfEntry)
+ observeLargestContentfulPaint(onPerfEntry)
+ observePaint(onPerfEntry)
} catch (e) {
window.addEventListener('load', () => {
performance.getEntriesByType('paint').forEach(onPerfEntry)
diff --git a/packages/next/client/performance-relayer.js b/packages/next/client/performance-relayer.js
new file mode 100644
index 000000000000000..2d2c9dae326c3b9
--- /dev/null
+++ b/packages/next/client/performance-relayer.js
@@ -0,0 +1,78 @@
+function isTypeSupported(type) {
+ if (self.PerformanceObserver && PerformanceObserver.supportedEntryTypes) {
+ return PerformanceObserver.supportedEntryTypes.includes(type)
+ }
+ return false
+}
+
+export function observeLayoutShift(onPerfEntry) {
+ if (isTypeSupported('layout-shift')) {
+ let cumulativeScore = 0
+ const observer = new PerformanceObserver(list => {
+ for (const entry of list.getEntries()) {
+ // Only count layout shifts without recent user input.
+ if (!entry.hadRecentInput) {
+ cumulativeScore += entry.value
+ }
+ }
+ })
+ observer.observe({ type: 'layout-shift', buffered: true })
+
+ document.addEventListener(
+ 'visibilitychange',
+ function clsObserver() {
+ if (document.visibilityState === 'hidden') {
+ // Force any pending records to be dispatched.
+ observer.takeRecords()
+ observer.disconnect()
+ removeEventListener('visibilitychange', clsObserver, true)
+ onPerfEntry({
+ name: 'cumulative-layout-shift',
+ value: cumulativeScore,
+ })
+ }
+ },
+ true
+ )
+ }
+}
+
+export function observeLargestContentfulPaint(onPerfEntry) {
+ if (isTypeSupported('largest-contentful-paint')) {
+ // Create a variable to hold the latest LCP value (since it can change).
+ let lcp
+
+ // Create the PerformanceObserver instance.
+ const observer = new PerformanceObserver(entryList => {
+ const entries = entryList.getEntries()
+ const lastEntry = entries[entries.length - 1]
+ lcp = lastEntry.renderTime || lastEntry.loadTime
+ })
+
+ observer.observe({ type: 'largest-contentful-paint', buffered: true })
+
+ document.addEventListener(
+ 'visibilitychange',
+ function lcpObserver() {
+ if (lcp && document.visibilityState === 'hidden') {
+ removeEventListener('visibilitychange', lcpObserver, true)
+ onPerfEntry({
+ name: 'largest-contentful-paint',
+ value: lcp,
+ })
+ }
+ },
+ true
+ )
+ }
+}
+
+export function observePaint(onPerfEntry) {
+ const observer = new PerformanceObserver(list => {
+ list.getEntries().forEach(onPerfEntry)
+ })
+ observer.observe({
+ type: 'paint',
+ buffered: true,
+ })
+}
diff --git a/test/integration/relay-analytics/pages/_app.js b/test/integration/relay-analytics/pages/_app.js
index d8536b65b7a5bb7..c9a40c8672c2cde 100644
--- a/test/integration/relay-analytics/pages/_app.js
+++ b/test/integration/relay-analytics/pages/_app.js
@@ -8,5 +8,8 @@ export default class MyApp extends App {}
Method is experimental and will eventually be handled in a Next.js plugin
*/
export function unstable_onPerformanceData(data) {
- localStorage.setItem(data.name, data.value || data.startTime)
+ localStorage.setItem(
+ data.name || data.entryType,
+ data.value !== undefined ? data.value : data.startTime
+ )
}
diff --git a/test/integration/relay-analytics/pages/index.js b/test/integration/relay-analytics/pages/index.js
index 7a227b55ed81ce8..5730772dc4b9bb4 100644
--- a/test/integration/relay-analytics/pages/index.js
+++ b/test/integration/relay-analytics/pages/index.js
@@ -1,3 +1,8 @@
export default () => {
- return
Hello!
+ return (
+
+
Foo!
+ bar!
+
+ )
}
diff --git a/test/integration/relay-analytics/test/index.test.js b/test/integration/relay-analytics/test/index.test.js
index d5c02861ec2b9ce..91263c21d0768fa 100644
--- a/test/integration/relay-analytics/test/index.test.js
+++ b/test/integration/relay-analytics/test/index.test.js
@@ -30,13 +30,34 @@ describe('Analytics relayer', () => {
const firstContentfulPaint = parseFloat(
await browser.eval('localStorage.getItem("first-contentful-paint")')
)
- expect(h1Text).toMatch(/Hello!/)
+ let largestContentfulPaint = await browser.eval(
+ 'localStorage.getItem("largest-contentful-paint")'
+ )
+ let cls = await browser.eval(
+ 'localStorage.getItem("cumulative-layout-shift")'
+ )
+ expect(h1Text).toMatch(/Foo!/)
expect(data).not.toBeNaN()
expect(data).toBeGreaterThan(0)
expect(firstPaint).not.toBeNaN()
expect(firstPaint).toBeGreaterThan(0)
expect(firstContentfulPaint).not.toBeNaN()
expect(firstContentfulPaint).toBeGreaterThan(0)
+ expect(largestContentfulPaint).toBeNull()
+ expect(cls).toBeNull()
+ // Create an artificial layout shift
+ await browser.eval('document.querySelector("h1").style.display = "none"')
+ await browser.refresh()
+ await browser.waitForElementByCss('h1')
+ largestContentfulPaint = parseFloat(
+ await browser.eval('localStorage.getItem("largest-contentful-paint")')
+ )
+ cls = parseFloat(
+ await browser.eval('localStorage.getItem("cumulative-layout-shift")')
+ )
+ expect(cls).not.toBeNull()
+ expect(largestContentfulPaint).not.toBeNaN()
+ expect(largestContentfulPaint).toBeGreaterThan(0)
await browser.close()
})
})
diff --git a/test/integration/size-limit/test/index.test.js b/test/integration/size-limit/test/index.test.js
index 08a1262ded5a73e..0acf2859cfc5069 100644
--- a/test/integration/size-limit/test/index.test.js
+++ b/test/integration/size-limit/test/index.test.js
@@ -80,7 +80,7 @@ describe('Production response size', () => {
)
// These numbers are without gzip compression!
- const delta = responseSizesBytes - 230 * 1024
+ const delta = responseSizesBytes - 231 * 1024
expect(delta).toBeLessThanOrEqual(1024) // don't increase size more than 1kb
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
})
@@ -100,7 +100,7 @@ describe('Production response size', () => {
)
// These numbers are without gzip compression!
- const delta = responseSizesBytes - 163 * 1024
+ const delta = responseSizesBytes - 164 * 1024
expect(delta).toBeLessThanOrEqual(1024) // don't increase size more than 1kb
expect(delta).toBeGreaterThanOrEqual(-1024) // don't decrease size more than 1kb without updating target
})