add method to measure Interaction to Next Paint (INP) (#36490)

This commit lets users measure their Interaction to Next Paint [INP](https://web.dev/inp/) web vital.
Note that the `web-vitals` package is beta to denote that INP is an experimental metric, the code is stable and v3 is backwards compatible.

    `web-vitals` CHANGELOG for v3:
    
    - [BREAKING] Report TTFB after a bfcache restore
    - [BREAKING] Only include last LCP entry in metric entries
    - Add support for the new INP metric
    - Rename getXXX() functions to onXXX()
    - Add a navigationType property to the Metric object
    
    See https://github.com/GoogleChrome/web-vitals/blob/next/CHANGELOG.md

Upgraded `playwright-chromium` from `1.14.1` to `1.17.2` because the Events Timing API used to measure INP is only available in Chromium >= v98.



## Bug

- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Errors have helpful link attached, see `contributing.md`

## Feature

- [ ] Implements an existing feature request or RFC. Make sure the feature request has been accepted for implementation before opening a PR.
- [ ] Related issues linked using `fixes #number`
- [ ] Integration tests added
- [ ] Documentation added
- [ ] Telemetry added. In case of a feature if it's used or not.
- [ ] Errors have helpful link attached, see `contributing.md`

## Documentation / Examples

- [ ] Make sure the linting passes by running `yarn lint`
This commit is contained in:
Keen Yee Liau 2022-06-07 11:28:58 -07:00 committed by GitHub
parent 42f838e156
commit dc36199b22
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 60 additions and 24 deletions

View file

@ -1,17 +1,18 @@
/* global location */ /* global location */
import { import {
getCLS, onCLS,
getFCP, onFCP,
getFID, onFID,
getLCP, onINP,
getTTFB, onLCP,
onTTFB,
Metric, Metric,
ReportHandler, ReportCallback,
} from 'next/dist/compiled/web-vitals' } from 'next/dist/compiled/web-vitals'
const initialHref = location.href const initialHref = location.href
let isRegistered = false let isRegistered = false
let userReportHandler: ReportHandler | undefined let userReportHandler: ReportCallback | undefined
function onReport(metric: Metric): void { function onReport(metric: Metric): void {
if (userReportHandler) { if (userReportHandler) {
@ -71,7 +72,7 @@ function onReport(metric: Metric): void {
} }
} }
export default (onPerfEntry?: ReportHandler): void => { export default (onPerfEntry?: ReportCallback): void => {
// Update function if it changes: // Update function if it changes:
userReportHandler = onPerfEntry userReportHandler = onPerfEntry
@ -81,9 +82,10 @@ export default (onPerfEntry?: ReportHandler): void => {
} }
isRegistered = true isRegistered = true
getCLS(onReport) onCLS(onReport)
getFID(onReport) onFID(onReport)
getFCP(onReport) onFCP(onReport)
getLCP(onReport) onLCP(onReport)
getTTFB(onReport) onTTFB(onReport)
onINP(onReport)
} }

File diff suppressed because one or more lines are too long

View file

@ -270,7 +270,7 @@
"uuid": "8.3.2", "uuid": "8.3.2",
"vm-browserify": "1.1.2", "vm-browserify": "1.1.2",
"watchpack": "2.4.0", "watchpack": "2.4.0",
"web-vitals": "2.1.0", "web-vitals": "3.0.0-beta.2",
"webpack-sources1": "npm:webpack-sources@1.4.3", "webpack-sources1": "npm:webpack-sources@1.4.3",
"webpack-sources3": "npm:webpack-sources@3.2.3", "webpack-sources3": "npm:webpack-sources@3.2.3",
"webpack4": "npm:webpack@4.44.1", "webpack4": "npm:webpack@4.44.1",

View file

@ -47,7 +47,7 @@ export type NextWebVitalsMetric = {
} & ( } & (
| { | {
label: 'web-vital' label: 'web-vital'
name: 'FCP' | 'LCP' | 'CLS' | 'FID' | 'TTFB' name: 'FCP' | 'LCP' | 'CLS' | 'FID' | 'TTFB' | 'INP'
} }
| { | {
label: 'custom' label: 'custom'

View file

@ -565,7 +565,7 @@ importers:
uuid: 8.3.2 uuid: 8.3.2
vm-browserify: 1.1.2 vm-browserify: 1.1.2
watchpack: 2.4.0 watchpack: 2.4.0
web-vitals: 2.1.0 web-vitals: 3.0.0-beta.2
webpack-sources1: npm:webpack-sources@1.4.3 webpack-sources1: npm:webpack-sources@1.4.3
webpack-sources3: npm:webpack-sources@3.2.3 webpack-sources3: npm:webpack-sources@3.2.3
webpack4: npm:webpack@4.44.1 webpack4: npm:webpack@4.44.1
@ -753,7 +753,7 @@ importers:
uuid: 8.3.2 uuid: 8.3.2
vm-browserify: 1.1.2 vm-browserify: 1.1.2
watchpack: 2.4.0 watchpack: 2.4.0
web-vitals: 2.1.0 web-vitals: 3.0.0-beta.2
webpack-sources1: /webpack-sources/1.4.3 webpack-sources1: /webpack-sources/1.4.3
webpack-sources3: /webpack-sources/3.2.3 webpack-sources3: /webpack-sources/3.2.3
webpack4: /webpack/4.44.1 webpack4: /webpack/4.44.1
@ -1058,7 +1058,7 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0 '@babel/core': ^7.0.0
dependencies: dependencies:
'@babel/compat-data': 7.17.0 '@babel/compat-data': 7.17.10
'@babel/core': 7.17.5 '@babel/core': 7.17.5
'@babel/helper-validator-option': 7.16.7 '@babel/helper-validator-option': 7.16.7
browserslist: 4.20.2 browserslist: 4.20.2
@ -1071,7 +1071,7 @@ packages:
peerDependencies: peerDependencies:
'@babel/core': ^7.0.0 '@babel/core': ^7.0.0
dependencies: dependencies:
'@babel/compat-data': 7.17.0 '@babel/compat-data': 7.17.10
'@babel/core': 7.18.0 '@babel/core': 7.18.0
'@babel/helper-validator-option': 7.16.7 '@babel/helper-validator-option': 7.16.7
browserslist: 4.20.2 browserslist: 4.20.2
@ -8823,7 +8823,7 @@ packages:
mississippi: 3.0.0 mississippi: 3.0.0
mkdirp: 0.5.5 mkdirp: 0.5.5
move-concurrently: 1.0.1 move-concurrently: 1.0.1
promise-inflight: 1.0.1 promise-inflight: 1.0.1_bluebird@3.7.2
rimraf: 2.7.1 rimraf: 2.7.1
ssri: 6.0.1 ssri: 6.0.1
unique-filename: 1.1.1 unique-filename: 1.1.1
@ -15129,7 +15129,7 @@ packages:
dev: true dev: true
/json-schema/0.2.3: /json-schema/0.2.3:
resolution: {integrity: sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=} resolution: {integrity: sha512-a3xHnILGMtk+hDOqNwHzF6e2fNbiMrXZvxKQiEv2MlgQP+pjIOzqAmKYD2mDpXYE/44M7g+n9p2bKkYWDUcXCQ==}
dev: true dev: true
/json-stable-stringify-without-jsonify/1.0.1: /json-stable-stringify-without-jsonify/1.0.1:
@ -19125,6 +19125,17 @@ packages:
optional: true optional: true
dev: true dev: true
/promise-inflight/1.0.1_bluebird@3.7.2:
resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==}
peerDependencies:
bluebird: '*'
peerDependenciesMeta:
bluebird:
optional: true
dependencies:
bluebird: 3.7.2
dev: true
/promise-polyfill/6.1.0: /promise-polyfill/6.1.0:
resolution: {integrity: sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=} resolution: {integrity: sha1-36lpQ+qcEh/KTem1hoyznTRy4Fc=}
dev: true dev: true
@ -23107,8 +23118,8 @@ packages:
resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==} resolution: {integrity: sha512-wYxSGajtmoP4WxfejAPIr4l0fVh+jeMXZb08wNc0tMg6xsfZXj3cECqIK0G7ZAqUq0PP8WlMDtaOGVBTAWztNw==}
dev: true dev: true
/web-vitals/2.1.0: /web-vitals/3.0.0-beta.2:
resolution: {integrity: sha512-npEyJP8jHf3J71t1tRTEtz9FeKp8H2udWJUUq5ykfPhhstr//TUxiYhIEzLNwk4zv2ybAilMn7v7N6Mxmuitmg==} resolution: {integrity: sha512-W9OALsWK4RkA5GWvLhsfszy+Q29WJBB27Dnucc3eYP6/0kz1XsfMgm+4au9X/KjXMIo92ZRU1fWBaSdNsaVjJg==}
dev: true dev: true
/webidl-conversions/3.0.1: /webidl-conversions/3.0.1:

View file

@ -16,12 +16,21 @@ if (typeof navigator !== 'undefined') {
} }
} }
function toggleText(e) {
const startTime = performance.now()
while (performance.now() < startTime + 100) {
// busy waiting
}
e.target.textContent = e.target.textContent === 'Click' ? 'Press' : 'Click'
}
export default () => { export default () => {
// Below comment will be used for replacing exported report method with hook based one. // Below comment will be used for replacing exported report method with hook based one.
return ( return (
<div> <div>
<h1>Foo!</h1> <h1>Foo!</h1>
<h2>bar!</h2> <h2>bar!</h2>
<button onClick={toggleText}>Click</button>
</div> </div>
) )
} }

View file

@ -108,4 +108,18 @@ function runTest() {
expect(stdout).toMatch('Next.js Analytics') expect(stdout).toMatch('Next.js Analytics')
await browser.close() await browser.close()
}) })
it('reports INP metric', async () => {
const browser = await webdriver(appPort, '/')
await browser.elementByCss('button').click()
await browser.waitForCondition(
'document.querySelector("button").textContent === "Press"'
)
// INP metric is only reported on pagehide or visibilitychange event, so refresh the page
await browser.refresh()
const INP = parseInt(await browser.eval('localStorage.getItem("INP")'), 10)
// We introduced a delay of 100ms, so INP duration should be >= 100
expect(INP).toBeGreaterThanOrEqual(100)
await browser.close()
})
} }