Skip to content

Commit

Permalink
Adding new types of performance monitoring (#10421)
Browse files Browse the repository at this point in the history
* add new types of performance monitoring

* adding LCP

* adding cls to perf relayer

* add test for cls and lcp

* nit fixes

* re-organizing code

* fixing tests for lcp

* updating size limits in test
  • Loading branch information
prateekbh committed Feb 28, 2020
1 parent 709aac1 commit 5a82812
Show file tree
Hide file tree
Showing 6 changed files with 128 additions and 15 deletions.
26 changes: 16 additions & 10 deletions packages/next/client/index.js
Expand Up @@ -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'

/// <reference types="react-dom/experimental" />

Expand Down Expand Up @@ -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,
})
}
}

Expand Down Expand Up @@ -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)
Expand Down
78 changes: 78 additions & 0 deletions 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,
})
}
5 changes: 4 additions & 1 deletion test/integration/relay-analytics/pages/_app.js
Expand Up @@ -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
)
}
7 changes: 6 additions & 1 deletion test/integration/relay-analytics/pages/index.js
@@ -1,3 +1,8 @@
export default () => {
return <h1>Hello!</h1>
return (
<div>
<h1>Foo!</h1>
<h2>bar!</h2>
</div>
)
}
23 changes: 22 additions & 1 deletion test/integration/relay-analytics/test/index.test.js
Expand Up @@ -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()
})
})
4 changes: 2 additions & 2 deletions test/integration/size-limit/test/index.test.js
Expand Up @@ -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
})
Expand All @@ -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
})
Expand Down

0 comments on commit 5a82812

Please sign in to comment.