-
Notifications
You must be signed in to change notification settings - Fork 7
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
Instrument back/forward cache navigations #70
Changes from all commits
4304142
f1f0b89
363f9f2
ba2bc2d
a47bdd1
fe35e26
9519aa0
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -23,7 +23,16 @@ export const init = (options?: TtvcOptions) => { | |
|
||
calculator = getVisuallyCompleteCalculator(); | ||
void calculator.start(); | ||
window.addEventListener('locationchange', () => void calculator.start(performance.now())); | ||
|
||
// restart measurement for SPA navigation | ||
window.addEventListener('locationchange', (event) => void calculator.start(event.timeStamp)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is there more documentation on the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nvm - just realized we have it documented in README already. |
||
|
||
// restart measurement on back/forward cache page restoration | ||
window.addEventListener('pageshow', (event) => { | ||
// abort if this is the initial pageload | ||
if (!event.persisted) return; | ||
void calculator.start(event.timeStamp, true); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think that's likely. This event should only be triggered either on initial pageload, in which case we ignore it, or on restore from the bfcache, in which case, nothing else on the page knows that something different from normal operation has occurred. The design of |
||
}); | ||
}; | ||
|
||
/** | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -4,6 +4,11 @@ import {requestAllIdleCallback} from './requestAllIdleCallback'; | |
import {InViewportImageObserver} from './inViewportImageObserver'; | ||
import {Logger} from './util/logger'; | ||
|
||
export type NavigationType = | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can this extend |
||
| NavigationTimingType | ||
// Navigation was triggered with a script operation, e.g. in a single page application. | ||
| 'script'; | ||
|
||
export type Metric = { | ||
// time since timeOrigin that the navigation was triggered | ||
// (this will be 0 for the initial pageload) | ||
|
@@ -22,6 +27,8 @@ export type Metric = { | |
|
||
// the most recent visual update; this can be either a mutation or a load event target | ||
lastVisibleChange?: HTMLElement | TimestampedMutationRecord; | ||
|
||
navigationType: NavigationType; | ||
}; | ||
}; | ||
|
||
|
@@ -82,7 +89,7 @@ class VisuallyCompleteCalculator { | |
} | ||
|
||
/** begin measuring a new navigation */ | ||
async start(start = 0) { | ||
async start(start = 0, isBfCacheRestore = false) { | ||
const navigationIndex = (this.navigationCount += 1); | ||
this.activeMeasurementIndex = navigationIndex; | ||
Logger.info('VisuallyCompleteCalculator.start()'); | ||
|
@@ -115,12 +122,22 @@ class VisuallyCompleteCalculator { | |
// identify timestamp of last visible change | ||
const end = Math.max(start, this.lastImageLoadTimestamp, this.lastMutation?.timestamp ?? 0); | ||
|
||
const navigationEntries = performance.getEntriesByType( | ||
'navigation' | ||
) as PerformanceNavigationTiming[]; | ||
const navigationType = isBfCacheRestore | ||
? 'back_forward' | ||
: start !== 0 | ||
? 'script' | ||
: navigationEntries[navigationEntries.length - 1].type; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think it matters much either way since there is only 1 navigation entry, but maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think I wasn't confident that there can only ever be one navigation entry. It seems likely there are some edge cases where the navigation entry can be reported again, after a cache restore, for instance. Do you know if the spec guarantees that there will only ever be one navigation entry? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This says
and web-vitals only takes the first navigation entry: https://github.com/GoogleChrome/web-vitals/blob/main/src/lib/getNavigationEntry.ts |
||
|
||
// report result to subscribers | ||
this.next({ | ||
start, | ||
end, | ||
duration: end - start, | ||
detail: { | ||
navigationType, | ||
didNetworkTimeOut, | ||
lastVisibleChange: | ||
this.lastImageLoadTimestamp > (this.lastMutation?.timestamp ?? 0) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<head> | ||
<script src="/dist/index.min.js"></script> | ||
<script src="/analytics.js"></script> | ||
</head> | ||
|
||
<body> | ||
<h1 id="h1">About</h1> | ||
</body> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
<head> | ||
<script src="/dist/index.min.js"></script> | ||
<script src="/analytics.js"></script> | ||
</head> | ||
|
||
<body> | ||
<h1 id="h1">Hello world!</h1> | ||
</body> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,41 @@ | ||
import {test, expect} from '@playwright/test'; | ||
|
||
import {FUDGE} from '../../util/constants'; | ||
import {getEntries} from '../../util/entries'; | ||
|
||
const PAGELOAD_DELAY = 1000; | ||
|
||
test.describe('TTVC', () => { | ||
test('a static HTML document', async ({page}) => { | ||
await page.goto(`/test/bfcache?delay=${PAGELOAD_DELAY}&cache=true`, { | ||
waitUntil: 'networkidle', | ||
}); | ||
|
||
let entries = await getEntries(page); | ||
|
||
expect(entries.length).toBe(1); | ||
expect(entries[0].duration).toBeGreaterThanOrEqual(PAGELOAD_DELAY); | ||
expect(entries[0].duration).toBeLessThanOrEqual(PAGELOAD_DELAY + FUDGE); | ||
expect(entries[0].detail.navigationType).toBe('navigate'); | ||
|
||
await page.goto(`/test/bfcache/about?delay=${PAGELOAD_DELAY}&cache=true`, { | ||
waitUntil: 'networkidle', | ||
}); | ||
|
||
entries = await getEntries(page); | ||
|
||
expect(entries.length).toBe(1); | ||
expect(entries[0].duration).toBeGreaterThanOrEqual(PAGELOAD_DELAY); | ||
expect(entries[0].duration).toBeLessThanOrEqual(PAGELOAD_DELAY + FUDGE); | ||
expect(entries[0].detail.navigationType).toBe('navigate'); | ||
|
||
await page.goBack({waitUntil: 'networkidle'}); | ||
|
||
entries = await getEntries(page); | ||
|
||
// note: webkit clears previous values from this list on page restore | ||
expect(entries[entries.length - 1].duration).toBeGreaterThanOrEqual(0); | ||
expect(entries[entries.length - 1].duration).toBeLessThanOrEqual(FUDGE); | ||
expect(entries[entries.length - 1].detail.navigationType).toBe('back_forward'); | ||
}); | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we document that this is an extension of
NavigationTimingType
andscript
is the only additional value?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, good call, updated.