Skip to content

Commit 56a24d7

Browse files
Fix missing FCP and LCP for prerendered pages (#621)
* Fix missing FCP and LCP for prerendered pages * Switch to real prerender * Fix tests * Make more generic * Fix tests * Fix tests * Fix TTFB test * Fix TTFB test * Fix INP tests * Fix INP test * Stop mocking initial hidden state * Reset CLS test runs * Fix FCP test * Clean up * Revert the new tab changes as doesn't open in initial hidden state anyway * Cleanup * Fix firefox test * Apply suggestions from code review Co-authored-by: Philip Walton <philipwalton@users.noreply.github.com> * Update test/views/layout.njk --------- Co-authored-by: Philip Walton <philipwalton@users.noreply.github.com>
1 parent 12665c3 commit 56a24d7

File tree

13 files changed

+306
-61
lines changed

13 files changed

+306
-61
lines changed

src/lib/getVisibilityWatcher.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616

1717
import {onBFCacheRestore} from './bfcache.js';
18+
import {getActivationStart} from './getActivationStart.js';
1819

1920
let firstHiddenTime = -1;
2021

@@ -64,11 +65,14 @@ const removeChangeListeners = () => {
6465
export const getVisibilityWatcher = () => {
6566
if (firstHiddenTime < 0) {
6667
// Check if we have a previous hidden `visibility-state` performance entry.
68+
const activationStart = getActivationStart();
6769
/* eslint-disable indent */
6870
const firstVisibilityStateHiddenTime = !document.prerendering
6971
? globalThis.performance
7072
.getEntriesByType('visibility-state')
71-
.filter((e) => e.name === 'hidden')[0]?.startTime
73+
.filter(
74+
(e) => e.name === 'hidden' && e.startTime > activationStart,
75+
)[0]?.startTime
7276
: undefined;
7377
/* eslint-enable indent */
7478

test/e2e/onCLS-test.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,12 @@ describe('onCLS()', async function () {
2929
this.retries(2);
3030

3131
let browserSupportsCLS;
32+
let browserSupportsPrerender;
3233
before(async function () {
3334
browserSupportsCLS = await browserSupportsEntry('layout-shift');
35+
browserSupportsPrerender = await browser.execute(() => {
36+
return 'onprerenderingchange' in document;
37+
});
3438

3539
// Set a standard screen size so thresholds are the same
3640
browser.setWindowSize(1280, 1024);
@@ -701,6 +705,9 @@ describe('onCLS()', async function () {
701705

702706
await stubForwardBack();
703707

708+
// Give it time for beacons to come in
709+
await browser.pause(2000);
710+
704711
// clear any beacons from page load.
705712
await clearBeacons();
706713

@@ -720,24 +727,35 @@ describe('onCLS()', async function () {
720727
assert.strictEqual(cls.navigationType, 'back-forward-cache');
721728
});
722729

723-
it('reports prerender as nav type for prerender', async function () {
730+
it('reports prerender as nav type and excludes shifts that happen in prerender state', async function () {
724731
if (!browserSupportsCLS) this.skip();
732+
if (!browserSupportsPrerender) this.skip();
725733

726734
await navigateTo('/test/cls?prerender=1');
727735

736+
// Wait a bit to allow the prerender to happen
737+
// and all loading shifts to complete
738+
await browser.pause(2000);
739+
740+
const prerenderLink = await $('#prerender-link');
741+
await prerenderLink.click();
742+
743+
await beaconCountIs(1);
744+
await clearBeacons();
745+
728746
// Wait until all images are loaded and rendered, then change to hidden.
729747
await imagesPainted();
730748
await stubVisibilityChange('hidden');
731749

732750
await beaconCountIs(1);
733751
const [cls] = await getBeacons();
734752

735-
assert(cls.value > 0);
753+
assert.strictEqual(cls.value, 0);
736754
assert(cls.id.match(/^v5-\d+-\d+$/));
737755
assert.strictEqual(cls.name, 'CLS');
738756
assert.strictEqual(cls.value, cls.delta);
739757
assert.strictEqual(cls.rating, 'good');
740-
assert.strictEqual(cls.entries.length, 2);
758+
assert.strictEqual(cls.entries.length, 0);
741759
assert.strictEqual(cls.navigationType, 'prerender');
742760
});
743761

test/e2e/onFCP-test.js

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,19 @@ import {browserSupportsEntry} from '../utils/browserSupportsEntry.js';
2020
import {navigateTo} from '../utils/navigateTo.js';
2121
import {stubForwardBack} from '../utils/stubForwardBack.js';
2222
import {stubVisibilityChange} from '../utils/stubVisibilityChange.js';
23+
import {webVitalsLoaded} from '../utils/webVitalsLoaded.js';
2324

2425
describe('onFCP()', async function () {
2526
// Retry all tests in this suite up to 2 times.
2627
this.retries(2);
2728

2829
let browserSupportsFCP;
30+
let browserSupportsPrerender;
2931
before(async function () {
3032
browserSupportsFCP = await browserSupportsEntry('paint');
33+
browserSupportsPrerender = await browser.execute(() => {
34+
return 'onprerenderingchange' in document;
35+
});
3136
});
3237

3338
beforeEach(async function () {
@@ -71,11 +76,20 @@ describe('onFCP()', async function () {
7176

7277
it('accounts for time prerendering the page', async function () {
7378
if (!browserSupportsFCP) this.skip();
79+
if (!browserSupportsPrerender) this.skip();
7480

7581
await navigateTo('/test/fcp?prerender=1');
7682

7783
await beaconCountIs(1);
84+
await clearBeacons();
85+
86+
// Wait a bit to allow the prerender to happen
87+
await browser.pause(1000);
88+
89+
const prerenderLink = await $('#prerender-link');
90+
await prerenderLink.click();
7891

92+
await beaconCountIs(1);
7993
const [fcp] = await getBeacons();
8094

8195
const activationStart = await browser.execute(() => {
@@ -133,6 +147,7 @@ describe('onFCP()', async function () {
133147
await navigateTo('/test/fcp?invisible=1', {readyState: 'interactive'});
134148

135149
await stubVisibilityChange('hidden');
150+
await webVitalsLoaded();
136151
await stubVisibilityChange('visible');
137152

138153
// Wait a bit to ensure no beacons were sent.
@@ -333,7 +348,11 @@ describe('onFCP()', async function () {
333348
/^(loading|dom-(interactive|content-loaded)|complete)$/,
334349
);
335350

336-
assert.deepEqual(fcp.attribution.fcpEntry, fcpEntry);
351+
// TODO Firefox has a bug causing this to fail. Remove once fixed
352+
// https://bugzilla.mozilla.org/show_bug.cgi?id=1965441
353+
if (browser.capabilities.browserName !== 'firefox') {
354+
assert.deepEqual(fcp.attribution.fcpEntry, fcpEntry);
355+
}
337356

338357
// When FCP is reported, not all values on the NavigationTiming entry
339358
// are finalized, so just check some keys that should be set before FCP.
@@ -346,11 +365,21 @@ describe('onFCP()', async function () {
346365

347366
it('accounts for time prerendering the page', async function () {
348367
if (!browserSupportsFCP) this.skip();
368+
if (!browserSupportsPrerender) this.skip();
349369

350370
await navigateTo(`/test/fcp?attribution=1&prerender=1`, {
351371
readyState: 'complete',
352372
});
353373

374+
await beaconCountIs(1);
375+
await clearBeacons();
376+
377+
// Wait a bit to allow the prerender to happen
378+
await browser.pause(1000);
379+
380+
const prerenderLink = await $('#prerender-link');
381+
await prerenderLink.click();
382+
354383
await beaconCountIs(1);
355384

356385
const navEntry = await browser.execute(() => {

test/e2e/onINP-test.js

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,13 @@ describe('onINP()', async function () {
3434

3535
let browserSupportsINP;
3636
let browserSupportsLoAF;
37+
let browserSupportsPrerender;
3738
before(async function () {
3839
browserSupportsINP = await browserSupportsEntry('event');
3940
browserSupportsLoAF = await browserSupportsEntry('long-animation-frame');
41+
browserSupportsPrerender = await browser.execute(() => {
42+
return 'onprerenderingchange' in document;
43+
});
4044
});
4145

4246
beforeEach(async function () {
@@ -431,12 +435,27 @@ describe('onINP()', async function () {
431435

432436
it('reports prerender as nav type for prerender', async function () {
433437
if (!browserSupportsINP) this.skip();
438+
if (!browserSupportsPrerender) this.skip();
434439

435-
await navigateTo('/test/inp?click=150&prerender=1', {
436-
readyState: 'interactive',
437-
});
440+
await navigateTo('/test/inp?click=150&prerender=1');
438441

439-
const h1 = await $('h1');
442+
await webVitalsLoaded();
443+
await firstContentfulPaint();
444+
445+
let h1 = await $('h1');
446+
await simulateUserLikeClick(h1);
447+
448+
// Wait a bit to allow the prerender to happen
449+
await browser.pause(500);
450+
451+
const prerenderLink = await $('#prerender-link');
452+
await prerenderLink.click();
453+
454+
await beaconCountIs(1);
455+
await clearBeacons();
456+
await webVitalsLoaded();
457+
458+
h1 = await $('h1');
440459
await simulateUserLikeClick(h1);
441460

442461
// Ensure the interaction completes.

0 commit comments

Comments
 (0)