From 7a15a646fa5ce05cc5f12c709615c466f1e395e9 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Sat, 22 Oct 2022 11:20:15 -0700 Subject: [PATCH 1/2] Defer all observers until after activation --- package-lock.json | 277 +++++++++----------------------- package.json | 2 +- src/lib/getVisibilityWatcher.ts | 49 ++++-- src/lib/whenActivated.ts | 24 +++ src/onCLS.ts | 134 +++++++-------- src/onFCP.ts | 73 +++++---- src/onFID.ts | 89 +++++----- src/onINP.ts | 159 +++++++++--------- src/onLCP.ts | 106 ++++++------ src/onTTFB.ts | 3 +- test/views/layout.njk | 2 +- 11 files changed, 427 insertions(+), 491 deletions(-) create mode 100644 src/lib/whenActivated.ts diff --git a/package-lock.json b/package-lock.json index dd870346..0c5a290a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,7 @@ "@wdio/selenium-standalone-service": "^7.25.1", "@wdio/spec-reporter": "^7.25.1", "body-parser": "^1.20.0", - "chromedriver": "^106.0.1", + "chromedriver": "^107.0.3", "eslint": "^8.24.0", "eslint-config-google": "^0.14.0", "express": "^4.18.1", @@ -35,6 +35,41 @@ "wdio-chromedriver-service": "^8.0.0" } }, + "../../GoogleChromeLabs/pending-beacon-polyfill": { + "version": "0.1.0", + "extraneous": true, + "license": "Apache-2.0", + "devDependencies": { + "@babel/core": "^7.19.6", + "@babel/preset-env": "^7.19.4", + "@rollup/plugin-babel": "^6.0.2", + "@rollup/plugin-replace": "^5.0.1", + "@rollup/plugin-terser": "^0.1.0", + "@wdio/cli": "^7.25.4", + "@wdio/local-runner": "^7.25.4", + "@wdio/mocha-framework": "^7.25.4", + "@wdio/selenium-standalone-service": "^7.25.4", + "@wdio/spec-reporter": "^7.25.4", + "body-parser": "^1.20.1", + "brotli-size-cli": "^1.0.0", + "chai": "^4.3.6", + "chromedriver": "^107.0.1", + "eslint": "^8.26.0", + "eslint-config-google": "^0.14.0", + "express": "^4.18.2", + "fs-extra": "^10.1.0", + "husky": "^8.0.1", + "lint-staged": "^13.0.3", + "mocha": "^10.1.0", + "npm-run-all": "^4.1.5", + "nunjucks": "^3.2.3", + "prettier": "^2.7.1", + "rollup": "^3.2.3", + "sinon": "^14.0.1", + "typescript": "^4.8.4", + "wdio-chromedriver-service": "^8.0.0" + } + }, "node_modules/@ampproject/remapping": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", @@ -2972,19 +3007,6 @@ "node": ">= 6.0.0" } }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -3200,13 +3222,14 @@ "dev": true }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", "dev": true, "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/babel-plugin-dynamic-import-node": { @@ -3732,16 +3755,15 @@ } }, "node_modules/chromedriver": { - "version": "106.0.1", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-106.0.1.tgz", - "integrity": "sha512-thaBvbDEPgGocSp4/SBIajQz3G7UQfUqCOHZBp9TVhRJv7c91eZrUGcjeJUaNF4p9CfSjCYNYzs4EVVryqmddA==", + "version": "107.0.3", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-107.0.3.tgz", + "integrity": "sha512-jmzpZgctCRnhYAn0l/NIjP4vYN3L8GFVbterTrRr2Ly3W5rFMb9H8EKGuM5JCViPKSit8FbE718kZTEt3Yvffg==", "dev": true, "hasInstallScript": true, "dependencies": { "@testim/chrome-version": "^1.1.3", - "axios": "^0.27.2", + "axios": "^1.1.3", "compare-versions": "^5.0.1", - "del": "^6.1.1", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^1.1.0", @@ -3751,7 +3773,7 @@ "chromedriver": "bin/chromedriver" }, "engines": { - "node": ">=10" + "node": ">=14" } }, "node_modules/ci-info": { @@ -3760,15 +3782,6 @@ "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", "dev": true }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true, - "engines": { - "node": ">=6" - } - }, "node_modules/cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -4159,28 +4172,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dev": true, - "dependencies": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -5120,9 +5111,9 @@ "dev": true }, "node_modules/follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "dev": true, "funding": [ { @@ -5717,15 +5708,6 @@ "node": ">=0.8.19" } }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -6036,24 +6018,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true, - "engines": { - "node": ">=8" - } - }, "node_modules/is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -7811,21 +7775,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -8485,27 +8434,15 @@ } }, "node_modules/recursive-readdir": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", - "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", "dev": true, "dependencies": { - "minimatch": "3.0.4" + "minimatch": "^3.0.5" }, "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/recursive-readdir/node_modules/minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": ">=6.0.0" } }, "node_modules/regenerate": { @@ -12419,16 +12356,6 @@ "debug": "4" } }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - } - }, "ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -12603,13 +12530,14 @@ "dev": true }, "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.1.3.tgz", + "integrity": "sha512-00tXVRwKx/FZr/IDVFt4C+f9FYairX517WoGCL6dpOntqLkZofjhu43F/Xl44UOpqa+9sLFDrG/XAnFsUYgkDA==", "dev": true, "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "babel-plugin-dynamic-import-node": { @@ -12989,15 +12917,14 @@ } }, "chromedriver": { - "version": "106.0.1", - "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-106.0.1.tgz", - "integrity": "sha512-thaBvbDEPgGocSp4/SBIajQz3G7UQfUqCOHZBp9TVhRJv7c91eZrUGcjeJUaNF4p9CfSjCYNYzs4EVVryqmddA==", + "version": "107.0.3", + "resolved": "https://registry.npmjs.org/chromedriver/-/chromedriver-107.0.3.tgz", + "integrity": "sha512-jmzpZgctCRnhYAn0l/NIjP4vYN3L8GFVbterTrRr2Ly3W5rFMb9H8EKGuM5JCViPKSit8FbE718kZTEt3Yvffg==", "dev": true, "requires": { "@testim/chrome-version": "^1.1.3", - "axios": "^0.27.2", + "axios": "^1.1.3", "compare-versions": "^5.0.1", - "del": "^6.1.1", "extract-zip": "^2.0.1", "https-proxy-agent": "^5.0.1", "proxy-from-env": "^1.1.0", @@ -13010,12 +12937,6 @@ "integrity": "sha512-xmDt/QIAdeZ9+nfdPsaBCpMvHNLFiLdjj59qjqn+6iPe6YmHGQ35sBnQ8uslRBXFmXkiZQOJRjvQeoGppoTjjg==", "dev": true }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, "cli-cursor": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", @@ -13303,22 +13224,6 @@ "object-keys": "^1.1.1" } }, - "del": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-6.1.1.tgz", - "integrity": "sha512-ua8BhapfP0JUJKC/zV9yHHDW/rDoDxP4Zhn3AkA6/xT6gY7jYXJiaeyBZznYVujhZZET+UgcbZiQ7sN3WqcImg==", - "dev": true, - "requires": { - "globby": "^11.0.1", - "graceful-fs": "^4.2.4", - "is-glob": "^4.0.1", - "is-path-cwd": "^2.2.0", - "is-path-inside": "^3.0.2", - "p-map": "^4.0.0", - "rimraf": "^3.0.2", - "slash": "^3.0.0" - } - }, "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", @@ -14050,9 +13955,9 @@ "dev": true }, "follow-redirects": { - "version": "1.15.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", "dev": true }, "form-data": { @@ -14468,12 +14373,6 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -14693,18 +14592,6 @@ "has-tostringtag": "^1.0.0" } }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", - "dev": true - }, - "is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "dev": true - }, "is-plain-obj": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", @@ -16037,15 +15924,6 @@ "p-limit": "^3.0.2" } }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, "p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -16547,23 +16425,12 @@ } }, "recursive-readdir": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.2.tgz", - "integrity": "sha512-nRCcW9Sj7NuZwa2XvH9co8NPeXUBhZP7CRKJtU+cS6PW9FpCIFoI5ib0NT1ZrbNuPoRy0ylyCaUL8Gih4LSyFg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", + "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", "dev": true, "requires": { - "minimatch": "3.0.4" - }, - "dependencies": { - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, - "requires": { - "brace-expansion": "^1.1.7" - } - } + "minimatch": "^3.0.5" } }, "regenerate": { diff --git a/package.json b/package.json index 42b9b8c1..75bcce17 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,7 @@ "@wdio/selenium-standalone-service": "^7.25.1", "@wdio/spec-reporter": "^7.25.1", "body-parser": "^1.20.0", - "chromedriver": "^106.0.1", + "chromedriver": "^107.0.3", "eslint": "^8.24.0", "eslint-config-google": "^0.14.0", "express": "^4.18.1", diff --git a/src/lib/getVisibilityWatcher.ts b/src/lib/getVisibilityWatcher.ts index 84772956..e121aea9 100644 --- a/src/lib/getVisibilityWatcher.ts +++ b/src/lib/getVisibilityWatcher.ts @@ -15,24 +15,51 @@ */ import {onBFCacheRestore} from './bfcache.js'; -import {onHidden} from './onHidden.js'; + let firstHiddenTime = -1; const initHiddenTime = () => { - // If the document is hidden and not prerendering, assume it was always - // hidden and the page was loaded in the background. + // If the document is hidden when this code runs, assume it was always + // hidden and the page was loaded in the background, with the one exception + // that visibility state is always 'hidden' during prerendering, so we have + // to ignore that case until prerendering finishes (see: `prerenderingchange` + // event logic below). return document.visibilityState === 'hidden' && !document.prerendering ? 0 : Infinity; } -const trackChanges = () => { - // Update the time if/when the document becomes hidden. - onHidden(({timeStamp}) => { - firstHiddenTime = timeStamp - }, true); +const onVisibilityUpdate = (event: Event) => { + // If the document is 'hidden' and no previous hidden timestamp has been + // set, update it based on the current event data. + if (document.visibilityState === 'hidden' && firstHiddenTime > -1) { + // If the event is a 'visibilitychange' event, it means the page was + // visible prior to this change, so the event timestamp is the first + // hidden time. However, if the event is a 'prerenderingchange' event and + // the document is 'hidden', assume the tab was activated in a background + // state and has always been hidden. + firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0; + + // Remove all listeners now that a `firstHiddenTime` value has been set. + removeChangeListeners(); + } +} + +const addChangeListeners = () => { + addEventListener('visibilitychange', onVisibilityUpdate, true); + // IMPORTANT: when a page is prerendering, its `visibilityState` is + // 'hidden', so in order to account for cases where this module checks for + // visibility during prerendering, an additional check after prerendering + // completes is also required. + addEventListener('prerenderingchange', onVisibilityUpdate, true); +}; + +const removeChangeListeners = () => { + removeEventListener('visibilitychange', onVisibilityUpdate, true); + removeEventListener('prerenderingchange', onVisibilityUpdate, true); }; + export const getVisibilityWatcher = () => { if (firstHiddenTime < 0) { // If the document is hidden when this code runs, assume it was hidden @@ -42,11 +69,11 @@ export const getVisibilityWatcher = () => { if (window.__WEB_VITALS_POLYFILL__) { firstHiddenTime = window.webVitals.firstHiddenTime; if (firstHiddenTime === Infinity) { - trackChanges(); + addChangeListeners(); } } else { firstHiddenTime = initHiddenTime(); - trackChanges(); + addChangeListeners(); } // Reset the time on bfcache restores. @@ -56,7 +83,7 @@ export const getVisibilityWatcher = () => { // https://bugs.chromium.org/p/chromium/issues/detail?id=1133363 setTimeout(() => { firstHiddenTime = initHiddenTime(); - trackChanges(); + addChangeListeners(); }, 0); }); } diff --git a/src/lib/whenActivated.ts b/src/lib/whenActivated.ts new file mode 100644 index 00000000..d9769743 --- /dev/null +++ b/src/lib/whenActivated.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2022 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + + +export const whenActivated = (callback: () => void) => { + if (document.prerendering) { + addEventListener('prerenderingchange', () => callback(), true); + } else { + callback(); + } +} diff --git a/src/onCLS.ts b/src/onCLS.ts index 25fecf81..56257030 100644 --- a/src/onCLS.ts +++ b/src/onCLS.ts @@ -19,6 +19,7 @@ import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; import {bindReporter} from './lib/bindReporter.js'; +import {whenActivated} from './lib/whenActivated.js'; import {onFCP} from './onFCP.js'; import {CLSMetric, CLSReportCallback, ReportOpts} from './types.js'; @@ -51,80 +52,83 @@ export const onCLS = (onReport: CLSReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; - // https://web.dev/cls/#what-is-a-good-cls-score - const thresholds = [0.1, 0.25]; + whenActivated(() => { + // https://web.dev/cls/#what-is-a-good-cls-score + const thresholds = [0.1, 0.25]; - // Start monitoring FCP so we can only report CLS if FCP is also reported. - // Note: this is done to match the current behavior of CrUX. - if (!isMonitoringFCP) { - onFCP((metric) => { - fcpValue = metric.value; - }); - isMonitoringFCP = true; - } - - const onReportWrapped: CLSReportCallback = (arg) => { - if (fcpValue > -1) { - onReport(arg); + // Start monitoring FCP so we can only report CLS if FCP is also reported. + // Note: this is done to match the current behavior of CrUX. + if (!isMonitoringFCP) { + onFCP((metric) => { + fcpValue = metric.value; + }); + isMonitoringFCP = true; } - }; - let metric = initMetric('CLS', 0); - let report: ReturnType; + const onReportWrapped: CLSReportCallback = (arg) => { + if (fcpValue > -1) { + onReport(arg); + } + }; - let sessionValue = 0; - let sessionEntries: PerformanceEntry[] = []; + let metric = initMetric('CLS', 0); + let report: ReturnType; - // const handleEntries = (entries: Metric['entries']) => { - const handleEntries = (entries: LayoutShift[]) => { - entries.forEach((entry) => { - // Only count layout shifts without recent user input. - if (!entry.hadRecentInput) { - const firstSessionEntry = sessionEntries[0]; - const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; + let sessionValue = 0; + let sessionEntries: PerformanceEntry[] = []; - // If the entry occurred less than 1 second after the previous entry and - // less than 5 seconds after the first entry in the session, include the - // entry in the current session. Otherwise, start a new session. - if (sessionValue && - entry.startTime - lastSessionEntry.startTime < 1000 && - entry.startTime - firstSessionEntry.startTime < 5000) { - sessionValue += entry.value; - sessionEntries.push(entry); - } else { - sessionValue = entry.value; - sessionEntries = [entry]; - } - - // If the current session value is larger than the current CLS value, - // update CLS and the entries contributing to it. - if (sessionValue > metric.value) { - metric.value = sessionValue; - metric.entries = sessionEntries; - report(); - } - } - }); - }; + // const handleEntries = (entries: Metric['entries']) => { + const handleEntries = (entries: LayoutShift[]) => { + entries.forEach((entry) => { + // Only count layout shifts without recent user input. + if (!entry.hadRecentInput) { + const firstSessionEntry = sessionEntries[0]; + const lastSessionEntry = sessionEntries[sessionEntries.length - 1]; - const po = observe('layout-shift', handleEntries); - if (po) { - report = bindReporter( - onReportWrapped, metric, thresholds, opts.reportAllChanges); + // If the entry occurred less than 1 second after the previous entry + // and less than 5 seconds after the first entry in the session, + // include the entry in the current session. Otherwise, start a new + // session. + if (sessionValue && + entry.startTime - lastSessionEntry.startTime < 1000 && + entry.startTime - firstSessionEntry.startTime < 5000) { + sessionValue += entry.value; + sessionEntries.push(entry); + } else { + sessionValue = entry.value; + sessionEntries = [entry]; + } - onHidden(() => { - handleEntries(po.takeRecords() as CLSMetric['entries']); - report(true); - }); + // If the current session value is larger than the current CLS value, + // update CLS and the entries contributing to it. + if (sessionValue > metric.value) { + metric.value = sessionValue; + metric.entries = sessionEntries; + report(); + } + } + }); + }; - // Only report after a bfcache restore if the `PerformanceObserver` - // successfully registered. - onBFCacheRestore(() => { - sessionValue = 0; - fcpValue = -1; - metric = initMetric('CLS', 0); + const po = observe('layout-shift', handleEntries); + if (po) { report = bindReporter( onReportWrapped, metric, thresholds, opts!.reportAllChanges); - }); - } + + onHidden(() => { + handleEntries(po.takeRecords() as CLSMetric['entries']); + report(true); + }); + + // Only report after a bfcache restore if the `PerformanceObserver` + // successfully registered. + onBFCacheRestore(() => { + sessionValue = 0; + fcpValue = -1; + metric = initMetric('CLS', 0); + report = bindReporter( + onReportWrapped, metric, thresholds, opts!.reportAllChanges); + }); + } + }); }; diff --git a/src/onFCP.ts b/src/onFCP.ts index 5f3c18b2..62c97437 100644 --- a/src/onFCP.ts +++ b/src/onFCP.ts @@ -20,6 +20,7 @@ import {getActivationStart} from './lib/getActivationStart.js'; import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; +import {whenActivated} from './lib/whenActivated.js'; import {FCPMetric, FCPReportCallback, ReportOpts} from './types.js'; /** @@ -32,51 +33,53 @@ export const onFCP = (onReport: FCPReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; - // https://web.dev/fcp/#what-is-a-good-fcp-score - const thresholds = [1800, 3000]; + whenActivated(() => { + // https://web.dev/fcp/#what-is-a-good-fcp-score + const thresholds = [1800, 3000]; - const visibilityWatcher = getVisibilityWatcher(); - let metric = initMetric('FCP'); - let report: ReturnType; + const visibilityWatcher = getVisibilityWatcher(); + let metric = initMetric('FCP'); + let report: ReturnType; - const handleEntries = (entries: FCPMetric['entries']) => { - (entries as PerformancePaintTiming[]).forEach((entry) => { - if (entry.name === 'first-contentful-paint') { - po!.disconnect(); + const handleEntries = (entries: FCPMetric['entries']) => { + (entries as PerformancePaintTiming[]).forEach((entry) => { + if (entry.name === 'first-contentful-paint') { + po!.disconnect(); - // Only report if the page wasn't hidden prior to the first paint. - if (entry.startTime < visibilityWatcher.firstHiddenTime) { - // The activationStart reference is used because FCP should be - // relative to page activation rather than navigation start if the - // page was prerendered. But in cases where `activationStart` occurs - // after the FCP, this time should be clamped at 0. - metric.value = Math.max(entry.startTime - getActivationStart(), 0); - metric.entries.push(entry); - report(true); + // Only report if the page wasn't hidden prior to the first paint. + if (entry.startTime < visibilityWatcher.firstHiddenTime) { + // The activationStart reference is used because FCP should be + // relative to page activation rather than navigation start if the + // page was prerendered. But in cases where `activationStart` occurs + // after the FCP, this time should be clamped at 0. + metric.value = Math.max(entry.startTime - getActivationStart(), 0); + metric.entries.push(entry); + report(true); + } } - } - }); - }; - - const po = observe('paint', handleEntries); + }); + }; - if (po) { - report = bindReporter( - onReport, metric, thresholds, opts!.reportAllChanges); + const po = observe('paint', handleEntries); - // Only report after a bfcache restore if the `PerformanceObserver` - // successfully registered or the `paint` entry exists. - onBFCacheRestore((event) => { - metric = initMetric('FCP'); + if (po) { report = bindReporter( onReport, metric, thresholds, opts!.reportAllChanges); - requestAnimationFrame(() => { + // Only report after a bfcache restore if the `PerformanceObserver` + // successfully registered or the `paint` entry exists. + onBFCacheRestore((event) => { + metric = initMetric('FCP'); + report = bindReporter( + onReport, metric, thresholds, opts!.reportAllChanges); + requestAnimationFrame(() => { - metric.value = performance.now() - event.timeStamp; - report(true); + requestAnimationFrame(() => { + metric.value = performance.now() - event.timeStamp; + report(true); + }); }); }); - }); - } + } + }); }; diff --git a/src/onFID.ts b/src/onFID.ts index ed6cb3a0..835f464d 100644 --- a/src/onFID.ts +++ b/src/onFID.ts @@ -21,6 +21,7 @@ import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; import {firstInputPolyfill, resetFirstInputPolyfill} from './lib/polyfills/firstInputPolyfill.js'; +import {whenActivated} from './lib/whenActivated.js'; import {FIDMetric, FirstInputPolyfillCallback, ReportCallback, ReportOpts} from './types.js'; /** @@ -36,62 +37,64 @@ export const onFID = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; - // https://web.dev/fid/#what-is-a-good-fid-score - const thresholds = [100, 300]; + whenActivated(() => { + // https://web.dev/fid/#what-is-a-good-fid-score + const thresholds = [100, 300]; - const visibilityWatcher = getVisibilityWatcher(); - let metric = initMetric('FID'); - let report: ReturnType; + const visibilityWatcher = getVisibilityWatcher(); + let metric = initMetric('FID'); + let report: ReturnType; - const handleEntry = (entry: PerformanceEventTiming) => { - // Only report if the page wasn't hidden prior to the first input. - if (entry.startTime < visibilityWatcher.firstHiddenTime) { - metric.value = entry.processingStart - entry.startTime; - metric.entries.push(entry); - report(true); + const handleEntry = (entry: PerformanceEventTiming) => { + // Only report if the page wasn't hidden prior to the first input. + if (entry.startTime < visibilityWatcher.firstHiddenTime) { + metric.value = entry.processingStart - entry.startTime; + metric.entries.push(entry); + report(true); + } } - } - const handleEntries = (entries: FIDMetric['entries']) => { - (entries as PerformanceEventTiming[]).forEach(handleEntry); - } - - const po = observe('first-input', handleEntries); - report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges); - - if (po) { - onHidden(() => { - handleEntries(po.takeRecords() as FIDMetric['entries']); - po.disconnect(); - }, true); - } + const handleEntries = (entries: FIDMetric['entries']) => { + (entries as PerformanceEventTiming[]).forEach(handleEntry); + } - if (window.__WEB_VITALS_POLYFILL__) { - console.warn('The web-vitals "base+polyfill" build is deprecated. See: https://bit.ly/3aqzsGm'); + const po = observe('first-input', handleEntries); + report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges); - // Prefer the native implementation if available, - if (!po) { - window.webVitals.firstInputPolyfill(handleEntry as FirstInputPolyfillCallback) + if (po) { + onHidden(() => { + handleEntries(po.takeRecords() as FIDMetric['entries']); + po.disconnect(); + }, true); } - onBFCacheRestore(() => { - metric = initMetric('FID'); - report = bindReporter( - onReport, metric, thresholds, opts!.reportAllChanges); - window.webVitals.resetFirstInputPolyfill(); - window.webVitals.firstInputPolyfill(handleEntry as FirstInputPolyfillCallback); - }); - } else { - // Only monitor bfcache restores if the browser supports FID natively. - if (po) { + if (window.__WEB_VITALS_POLYFILL__) { + console.warn('The web-vitals "base+polyfill" build is deprecated. See: https://bit.ly/3aqzsGm'); + + // Prefer the native implementation if available, + if (!po) { + window.webVitals.firstInputPolyfill(handleEntry as FirstInputPolyfillCallback) + } onBFCacheRestore(() => { metric = initMetric('FID'); report = bindReporter( onReport, metric, thresholds, opts!.reportAllChanges); - resetFirstInputPolyfill(); - firstInputPolyfill(handleEntry as FirstInputPolyfillCallback); + window.webVitals.resetFirstInputPolyfill(); + window.webVitals.firstInputPolyfill(handleEntry as FirstInputPolyfillCallback); }); + } else { + // Only monitor bfcache restores if the browser supports FID natively. + if (po) { + onBFCacheRestore(() => { + metric = initMetric('FID'); + report = bindReporter( + onReport, metric, thresholds, opts!.reportAllChanges); + + resetFirstInputPolyfill(); + firstInputPolyfill(handleEntry as FirstInputPolyfillCallback); + }); + } } - } + }); }; diff --git a/src/onINP.ts b/src/onINP.ts index b3e3dd8e..d6d777ab 100644 --- a/src/onINP.ts +++ b/src/onINP.ts @@ -20,6 +20,7 @@ import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; import {getInteractionCount, initInteractionCountPolyfill} from './lib/polyfills/interactionCountPolyfill.js'; +import {whenActivated} from './lib/whenActivated.js'; import {INPMetric, ReportCallback, ReportOpts} from './types.js'; interface Interaction { @@ -135,92 +136,94 @@ export const onINP = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; - // https://web.dev/inp/#what's-a-%22good%22-inp-value - const thresholds = [200, 500]; + whenActivated(() => { + // https://web.dev/inp/#what's-a-%22good%22-inp-value + const thresholds = [200, 500]; - // TODO(philipwalton): remove once the polyfill is no longer needed. - initInteractionCountPolyfill(); + // TODO(philipwalton): remove once the polyfill is no longer needed. + initInteractionCountPolyfill(); - let metric = initMetric('INP'); - let report: ReturnType; + let metric = initMetric('INP'); + let report: ReturnType; - const handleEntries = (entries: INPMetric['entries']) => { - entries.forEach((entry) => { - if (entry.interactionId) { - processEntry(entry); - } + const handleEntries = (entries: INPMetric['entries']) => { + entries.forEach((entry) => { + if (entry.interactionId) { + processEntry(entry); + } - // Entries of type `first-input` don't currently have an `interactionId`, - // so to consider them in INP we have to first check that an existing - // entry doesn't match the `duration` and `startTime`. - // Note that this logic assumes that `event` entries are dispatched - // before `first-input` entries. This is true in Chrome but it is not - // true in Firefox; however, Firefox doesn't support interactionId, so - // it's not an issue at the moment. - // TODO(philipwalton): remove once crbug.com/1325826 is fixed. - if (entry.entryType === 'first-input') { - const noMatchingEntry = !longestInteractionList.some((interaction) => { - return interaction.entries.some((prevEntry) => { - return entry.duration === prevEntry.duration && - entry.startTime === prevEntry.startTime; + // Entries of type `first-input` don't currently have an `interactionId`, + // so to consider them in INP we have to first check that an existing + // entry doesn't match the `duration` and `startTime`. + // Note that this logic assumes that `event` entries are dispatched + // before `first-input` entries. This is true in Chrome but it is not + // true in Firefox; however, Firefox doesn't support interactionId, so + // it's not an issue at the moment. + // TODO(philipwalton): remove once crbug.com/1325826 is fixed. + if (entry.entryType === 'first-input') { + const noMatchingEntry = !longestInteractionList.some((interaction) => { + return interaction.entries.some((prevEntry) => { + return entry.duration === prevEntry.duration && + entry.startTime === prevEntry.startTime; + }); }); - }); - if (noMatchingEntry) { - processEntry(entry); + if (noMatchingEntry) { + processEntry(entry); + } } - } - }); + }); - const inp = estimateP98LongestInteraction(); + const inp = estimateP98LongestInteraction(); - if (inp && inp.latency !== metric.value) { - metric.value = inp.latency; - metric.entries = inp.entries; - report(); - } - }; - - const po = observe('event', handleEntries, { - // Event Timing entries have their durations rounded to the nearest 8ms, - // so a duration of 40ms would be any event that spans 2.5 or more frames - // at 60Hz. This threshold is chosen to strike a balance between usefulness - // and performance. Running this callback for any interaction that spans - // just one or two frames is likely not worth the insight that could be - // gained. - durationThreshold: opts.durationThreshold || 40, - } as PerformanceObserverInit); - - report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges); - - if (po) { - // Also observe entries of type `first-input`. This is useful in cases - // where the first interaction is less than the `durationThreshold`. - po.observe({type: 'first-input', buffered: true}); - - onHidden(() => { - handleEntries(po.takeRecords() as INPMetric['entries']); - - // If the interaction count shows that there were interactions but - // none were captured by the PerformanceObserver, report a latency of 0. - if (metric.value < 0 && getInteractionCountForNavigation() > 0) { - metric.value = 0; - metric.entries = []; + if (inp && inp.latency !== metric.value) { + metric.value = inp.latency; + metric.entries = inp.entries; + report(); } + }; + + const po = observe('event', handleEntries, { + // Event Timing entries have their durations rounded to the nearest 8ms, + // so a duration of 40ms would be any event that spans 2.5 or more frames + // at 60Hz. This threshold is chosen to strike a balance between usefulness + // and performance. Running this callback for any interaction that spans + // just one or two frames is likely not worth the insight that could be + // gained. + durationThreshold: opts!.durationThreshold || 40, + } as PerformanceObserverInit); + + report = bindReporter(onReport, metric, thresholds, opts!.reportAllChanges); + + if (po) { + // Also observe entries of type `first-input`. This is useful in cases + // where the first interaction is less than the `durationThreshold`. + po.observe({type: 'first-input', buffered: true}); + + onHidden(() => { + handleEntries(po.takeRecords() as INPMetric['entries']); + + // If the interaction count shows that there were interactions but + // none were captured by the PerformanceObserver, report a latency of 0. + if (metric.value < 0 && getInteractionCountForNavigation() > 0) { + metric.value = 0; + metric.entries = []; + } - report(true); - }); - - // Only report after a bfcache restore if the `PerformanceObserver` - // successfully registered. - onBFCacheRestore(() => { - longestInteractionList = []; - // Important, we want the count for the full page here, - // not just for the current navigation. - prevInteractionCount = getInteractionCount(); - - metric = initMetric('INP'); - report = bindReporter( - onReport, metric, thresholds, opts!.reportAllChanges); - }); - } + report(true); + }); + + // Only report after a bfcache restore if the `PerformanceObserver` + // successfully registered. + onBFCacheRestore(() => { + longestInteractionList = []; + // Important, we want the count for the full page here, + // not just for the current navigation. + prevInteractionCount = getInteractionCount(); + + metric = initMetric('INP'); + report = bindReporter( + onReport, metric, thresholds, opts!.reportAllChanges); + }); + } + }); }; diff --git a/src/onLCP.ts b/src/onLCP.ts index 9ecc4d62..4796d3e1 100644 --- a/src/onLCP.ts +++ b/src/onLCP.ts @@ -21,6 +21,7 @@ import {getVisibilityWatcher} from './lib/getVisibilityWatcher.js'; import {initMetric} from './lib/initMetric.js'; import {observe} from './lib/observe.js'; import {onHidden} from './lib/onHidden.js'; +import {whenActivated} from './lib/whenActivated.js'; import {LCPMetric, ReportCallback, ReportOpts} from './types.js'; @@ -41,70 +42,73 @@ export const onLCP = (onReport: ReportCallback, opts?: ReportOpts) => { // Set defaults opts = opts || {}; - // https://web.dev/lcp/#what-is-a-good-lcp-score - const thresholds = [2500, 4000]; + whenActivated(() => { + // https://web.dev/lcp/#what-is-a-good-lcp-score + const thresholds = [2500, 4000]; - const visibilityWatcher = getVisibilityWatcher(); - let metric = initMetric('LCP'); - let report: ReturnType; + const visibilityWatcher = getVisibilityWatcher(); + let metric = initMetric('LCP'); + let report: ReturnType; - const handleEntries = (entries: LCPMetric['entries']) => { - const lastEntry = (entries[entries.length - 1] as LargestContentfulPaint); - if (lastEntry) { - // The startTime attribute returns the value of the renderTime if it is - // not 0, and the value of the loadTime otherwise. The activationStart - // reference is used because LCP should be relative to page activation - // rather than navigation start if the page was prerendered. But in cases - // where `activationStart` occurs after the LCP, this time should be - // clamped at 0. - const value = Math.max(lastEntry.startTime - getActivationStart(), 0); + const handleEntries = (entries: LCPMetric['entries']) => { + const lastEntry = (entries[entries.length - 1] as LargestContentfulPaint); + if (lastEntry) { + // The startTime attribute returns the value of the renderTime if it is + // not 0, and the value of the loadTime otherwise. The activationStart + // reference is used because LCP should be relative to page activation + // rather than navigation start if the page was prerendered. But in cases + // where `activationStart` occurs after the LCP, this time should be + // clamped at 0. + const value = Math.max(lastEntry.startTime - getActivationStart(), 0); - // Only report if the page wasn't hidden prior to LCP. - if (value < visibilityWatcher.firstHiddenTime) { - metric.value = value; - metric.entries = [lastEntry]; - report(); + // Only report if the page wasn't hidden prior to LCP. + if (value < visibilityWatcher.firstHiddenTime) { + metric.value = value; + metric.entries = [lastEntry]; + report(); + } } - } - }; + }; - const po = observe('largest-contentful-paint', handleEntries); + const po = observe('largest-contentful-paint', handleEntries); - if (po) { - report = bindReporter(onReport, metric, thresholds, opts.reportAllChanges); + if (po) { + report = bindReporter( + onReport, metric, thresholds, opts!.reportAllChanges); - const stopListening = () => { - if (!reportedMetricIDs[metric.id]) { - handleEntries(po.takeRecords() as LCPMetric['entries']); - po.disconnect(); - reportedMetricIDs[metric.id] = true; - report(true); + const stopListening = () => { + if (!reportedMetricIDs[metric.id]) { + handleEntries(po!.takeRecords() as LCPMetric['entries']); + po!.disconnect(); + reportedMetricIDs[metric.id] = true; + report(true); + } } - } - // Stop listening after input. Note: while scrolling is an input that - // stop LCP observation, it's unreliable since it can be programmatically - // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 - ['keydown', 'click'].forEach((type) => { - addEventListener(type, stopListening, {once: true, capture: true}); - }); + // Stop listening after input. Note: while scrolling is an input that + // stops LCP observation, it's unreliable since it can be programmatically + // generated. See: https://github.com/GoogleChrome/web-vitals/issues/75 + ['keydown', 'click'].forEach((type) => { + addEventListener(type, stopListening, {once: true, capture: true}); + }); - onHidden(stopListening, true); + onHidden(stopListening, true); - // Only report after a bfcache restore if the `PerformanceObserver` - // successfully registered. - onBFCacheRestore((event) => { - metric = initMetric('LCP'); - report = bindReporter( - onReport, metric, thresholds, opts!.reportAllChanges); + // Only report after a bfcache restore if the `PerformanceObserver` + // successfully registered. + onBFCacheRestore((event) => { + metric = initMetric('LCP'); + report = bindReporter( + onReport, metric, thresholds, opts!.reportAllChanges); - requestAnimationFrame(() => { requestAnimationFrame(() => { - metric.value = performance.now() - event.timeStamp; - reportedMetricIDs[metric.id] = true; - report(true); + requestAnimationFrame(() => { + metric.value = performance.now() - event.timeStamp; + reportedMetricIDs[metric.id] = true; + report(true); + }); }); }); - }); - } + } + }); }; diff --git a/src/onTTFB.ts b/src/onTTFB.ts index 301274cf..c9fa86e6 100644 --- a/src/onTTFB.ts +++ b/src/onTTFB.ts @@ -20,6 +20,7 @@ import {onBFCacheRestore} from './lib/bfcache.js'; import {getNavigationEntry} from './lib/getNavigationEntry.js'; import {ReportCallback, ReportOpts} from './types.js'; import {getActivationStart} from './lib/getActivationStart.js'; +import {whenActivated} from './lib/whenActivated.js'; /** @@ -28,7 +29,7 @@ import {getActivationStart} from './lib/getActivationStart.js'; */ const whenReady = (callback: () => void) => { if (document.prerendering) { - addEventListener('prerenderingchange', () => whenReady(callback), true); + whenActivated(() => whenReady(callback)); } else if (document.readyState !== 'complete') { addEventListener('load', () => whenReady(callback), true); } else { diff --git a/test/views/layout.njk b/test/views/layout.njk index 432d6667..2128b92a 100644 --- a/test/views/layout.njk +++ b/test/views/layout.njk @@ -84,7 +84,6 @@ configurable: true, }); setTimeout(() => { - self.__stubVisibilityChange('visible'); const time = self.performance ? performance.now() : 0; const fcpEntry = performance.getEntriesByName('first-contentful-paint')[0]; Object.defineProperty(navEntry, 'activationStart', { @@ -92,6 +91,7 @@ enumerable: true, value: Math.min(time, fcpEntry && fcpEntry.startTime || time), }); + self.__stubVisibilityChange('visible'); delete document.prerendering; document.dispatchEvent(new Event('prerenderingchange')); resolve(); From db64fc94869283b91cfdcc8ea50b74542d0f8385 Mon Sep 17 00:00:00 2001 From: Philip Walton Date: Tue, 15 Nov 2022 11:57:28 -0800 Subject: [PATCH 2/2] Update getVisibilityWatcher logic comment Co-authored-by: Barry Pollard --- src/lib/getVisibilityWatcher.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/lib/getVisibilityWatcher.ts b/src/lib/getVisibilityWatcher.ts index e121aea9..bae72d34 100644 --- a/src/lib/getVisibilityWatcher.ts +++ b/src/lib/getVisibilityWatcher.ts @@ -35,9 +35,11 @@ const onVisibilityUpdate = (event: Event) => { if (document.visibilityState === 'hidden' && firstHiddenTime > -1) { // If the event is a 'visibilitychange' event, it means the page was // visible prior to this change, so the event timestamp is the first - // hidden time. However, if the event is a 'prerenderingchange' event and - // the document is 'hidden', assume the tab was activated in a background - // state and has always been hidden. + // hidden time. + // However, if the event is not a 'visibilitychange' event, then it must + // be a 'prerenderingchange' event, and the fact that the document is + // still 'hidden' from the above check means the tab was activated + // in a background state and so has always been hidden. firstHiddenTime = event.type === 'visibilitychange' ? event.timeStamp : 0; // Remove all listeners now that a `firstHiddenTime` value has been set.