Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Experimental: hook version of reportWebVitals #28769

Merged
merged 4 commits into from
Sep 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
69 changes: 39 additions & 30 deletions packages/next/client/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,21 @@ import {
assign,
} from '../shared/lib/router/utils/querystring'
import { setConfig } from '../shared/lib/runtime-config'
import { getURL, loadGetInitialProps, NEXT_DATA, ST } from '../shared/lib/utils'
import {
getURL,
loadGetInitialProps,
NextWebVitalsMetric,
NEXT_DATA,
ST,
} from '../shared/lib/utils'
import { Portal } from './portal'
import initHeadManager from './head-manager'
import PageLoader, { StyleSheetTuple } from './page-loader'
import measureWebVitals from './performance-relayer'
import { RouteAnnouncer } from './route-announcer'
import { createRouter, makePublicRouterInstance } from './router'
import isError from '../lib/is-error'
import { trackWebVitalMetric } from '../vitals/vitals'

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

Expand Down Expand Up @@ -273,37 +280,38 @@ export async function initNext(opts: { webpackHMR?: any } = {}) {

const { component: app, exports: mod } = appEntrypoint
CachedApp = app as AppComponent
if (mod && mod.reportWebVitals) {
onPerfEntry = ({
id,
name,
startTime,
value,
duration,
entryType,
entries,
}): void => {
// Combines timestamp with random number for unique ID
const uniqueID: string = `${Date.now()}-${
Math.floor(Math.random() * (9e12 - 1)) + 1e12
}`
let perfStartEntry: string | undefined

if (entries && entries.length) {
perfStartEntry = entries[0].startTime
}
const exportedReportWebVitals = mod && mod.reportWebVitals
onPerfEntry = ({
id,
name,
startTime,
value,
duration,
entryType,
entries,
}: any): void => {
// Combines timestamp with random number for unique ID
const uniqueID: string = `${Date.now()}-${
Math.floor(Math.random() * (9e12 - 1)) + 1e12
}`
let perfStartEntry: string | undefined

if (entries && entries.length) {
perfStartEntry = entries[0].startTime
}

mod.reportWebVitals({
id: id || uniqueID,
name,
startTime: startTime || perfStartEntry,
value: value == null ? duration : value,
label:
entryType === 'mark' || entryType === 'measure'
? 'custom'
: 'web-vital',
})
const webVitals: NextWebVitalsMetric = {
id: id || uniqueID,
name,
startTime: startTime || perfStartEntry,
value: value == null ? duration : value,
label:
entryType === 'mark' || entryType === 'measure'
? 'custom'
: 'web-vital',
}
exportedReportWebVitals?.(webVitals)
trackWebVitalMetric(webVitals)
}

const pageEntrypoint =
Expand Down Expand Up @@ -576,6 +584,7 @@ function markRenderComplete(): void {
.getEntriesByName('Next.js-route-change-to-render')
.forEach(onPerfEntry)
}

clearMarks()
;['Next.js-route-change-to-render', 'Next.js-render'].forEach((measure) =>
performance.clearMeasures(measure)
Expand Down
1 change: 1 addition & 0 deletions packages/next/client/performance-relayer.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* global location */
import {
getCLS,
getFCP,
Expand Down
10 changes: 10 additions & 0 deletions packages/next/taskfile.js
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,7 @@ export async function compile(task, opts) {
'pages',
'lib',
'client',
'vitals',
'telemetry',
'trace',
'shared',
Expand Down Expand Up @@ -992,6 +993,14 @@ export async function client(task, opts) {
notify('Compiled client files')
}

export async function vitals(task, opts) {
await task
.source(opts.src || 'vitals/**/*.+(js|ts|tsx)')
.swc('vitals', { dev: opts.dev })
.target('dist/vitals')
notify('Compiled vitals files')
}

// export is a reserved keyword for functions
export async function nextbuildstatic(task, opts) {
await task
Expand Down Expand Up @@ -1056,6 +1065,7 @@ export default async function (task) {
await task.watch('build/**/*.+(js|ts|tsx)', 'nextbuild', opts)
await task.watch('export/**/*.+(js|ts|tsx)', 'nextbuildstatic', opts)
await task.watch('client/**/*.+(js|ts|tsx)', 'client', opts)
await task.watch('vitals/**/*.+(js|ts|tsx)', 'vitals', opts)
await task.watch('lib/**/*.+(js|ts|tsx)', 'lib', opts)
await task.watch('cli/**/*.+(js|ts|tsx)', 'cli', opts)
await task.watch('telemetry/**/*.+(js|ts|tsx)', 'telemetry', opts)
Expand Down
1 change: 1 addition & 0 deletions packages/next/vitals.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './dist/vitals/index'
1 change: 1 addition & 0 deletions packages/next/vitals.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = require('./dist/vitals/index')
1 change: 1 addition & 0 deletions packages/next/vitals/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useExperimentalWebVitalsReport } from './vitals'
33 changes: 33 additions & 0 deletions packages/next/vitals/vitals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { useEffect, useRef } from 'react'
import { NextWebVitalsMetric } from '../pages/_app'

type ReportWebVitalsCallback = (webVitals: NextWebVitalsMetric) => any
export const webVitalsCallbacks = new Set<ReportWebVitalsCallback>()
const metrics: NextWebVitalsMetric[] = []

export function trackWebVitalMetric(metric: NextWebVitalsMetric) {
metrics.push(metric)
webVitalsCallbacks.forEach((callback) => callback(metric))
}

export function useExperimentalWebVitalsReport(
callback: ReportWebVitalsCallback
) {
const metricIndexRef = useRef(0)

useEffect(() => {
// Flush calculated metrics
const reportMetric = (metric: NextWebVitalsMetric) => {
callback(metric)
metricIndexRef.current = metrics.length
}
for (let i = metricIndexRef.current; i < metrics.length; i++) {
reportMetric(metrics[i])
}

webVitalsCallbacks.add(reportMetric)
return () => {
webVitalsCallbacks.delete(reportMetric)
}
}, [callback])
}
9 changes: 8 additions & 1 deletion test/integration/relay-analytics/pages/_app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,16 @@ export default class MyApp extends App {}
/*
Method is experimental and will eventually be handled in a Next.js plugin
*/

// Below comment will be used for replacing exported report method with hook based one.
///* reportWebVitals
export function reportWebVitals(data) {
const name = data.name || data.entryType
localStorage.setItem(
data.name || data.entryType,
name,
data.value !== undefined ? data.value : data.startTime
)
const countMap = window.__BEACONS_COUNT
countMap.set(name, (countMap.get(name) || 0) + 1)
}
// reportWebVitals */
17 changes: 17 additions & 0 deletions test/integration/relay-analytics/pages/index.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
/* global localStorage */
import { useExperimentalWebVitalsReport } from 'next/vitals'

if (typeof navigator !== 'undefined') {
window.__BEACONS = window.__BEACONS || []
window.__BEACONS_COUNT = new Map()

navigator.sendBeacon = async function () {
const args = await Promise.all(
Expand All @@ -16,6 +20,19 @@ if (typeof navigator !== 'undefined') {
}

export default () => {
// Below comment will be used for replacing exported report method with hook based one.
///* useExperimentalWebVitalsReport
useExperimentalWebVitalsReport((data) => {
const name = data.name || data.entryType
localStorage.setItem(
name,
data.value !== undefined ? data.value : data.startTime
)
const countMap = window.__BEACONS_COUNT
countMap.set(name, (countMap.get(name) || 0) + 1)
})
// useExperimentalWebVitalsReport */

return (
<div>
<h1>Foo!</h1>
Expand Down
76 changes: 65 additions & 11 deletions test/integration/relay-analytics/test/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,70 @@

import { join } from 'path'
import webdriver from 'next-webdriver'
import { killApp, findPort, nextBuild, nextStart, check } from 'next-test-utils'
import {
File,
killApp,
findPort,
nextBuild,
nextStart,
check,
} from 'next-test-utils'

const appDir = join(__dirname, '../')
const customApp = new File(join(appDir, 'pages/_app.js'))
const indexPage = new File(join(appDir, 'pages/index.js'))

let appPort
let server
let stdout
jest.setTimeout(1000 * 60 * 2)

async function buildApp() {
appPort = await findPort()
;({ stdout } = await nextBuild(appDir, [], {
env: { VERCEL_ANALYTICS_ID: 'test' },
stdout: true,
}))
server = await nextStart(appDir, appPort)
}
async function killServer() {
await killApp(server)
}

describe('Analytics relayer with exported method', () => {
beforeAll(async () => {
// Keep app exported reporting and comment the hook one
indexPage.replace('///* useExperimentalWebVitalsReport', '/*')
indexPage.replace('// useExperimentalWebVitalsReport */', '*/')
await buildApp()
})
afterAll(async () => {
indexPage.restore()
await killServer()
})
runTest()
})

describe('Analytics relayer', () => {
let stdout
describe('Analytics relayer with hook', () => {
beforeAll(async () => {
appPort = await findPort()
;({ stdout } = await nextBuild(appDir, [], {
env: { VERCEL_ANALYTICS_ID: 'test' },
stdout: true,
}))
server = await nextStart(appDir, appPort)
// Keep hook reporting and comment the app exported one
customApp.replace('///* reportWebVitals', '/*')
customApp.replace('// reportWebVitals */', '*/')
await buildApp()
})
afterAll(() => killApp(server))

afterAll(async () => {
customApp.restore()
await killServer()
})
runTest()
})

function runTest() {
it('Relays the data to user code', async () => {
const browser = await webdriver(appPort, '/')
await browser.waitForElementByCss('h1')

const h1Text = await browser.elementByCss('h1').text()
const data = parseFloat(
await browser.eval('localStorage.getItem("Next.js-hydration")')
Expand All @@ -46,6 +89,12 @@ describe('Analytics relayer', () => {
expect(firstContentfulPaint).toBeGreaterThan(0)
expect(largestContentfulPaint).toBeNull()
expect(cls).toBeNull()

const beaconsCountBeforeCLS = await browser.eval('window.__BEACONS_COUNT')
expect(
Object.values(beaconsCountBeforeCLS).every((value) => value === 1)
).toBe(true)

// Create an artificial layout shift
await browser.eval('document.querySelector("h1").style.display = "none"')
await browser.refresh()
Expand All @@ -69,6 +118,11 @@ describe('Analytics relayer', () => {
Object.fromEntries(new URLSearchParams(value))
)

const beaconsCountAfterCLS = await browser.eval('window.__BEACONS_COUNT')
expect(
Object.values(beaconsCountAfterCLS).every((value) => value === 2)
).toBe(true)

expect(beacons.length).toBe(2)

for (const beacon of beacons) {
Expand All @@ -86,4 +140,4 @@ describe('Analytics relayer', () => {
expect(stdout).toMatch('Next.js Analytics')
await browser.close()
})
})
}