Skip to content

Commit

Permalink
Instrument back/forward cache navigations (#70)
Browse files Browse the repository at this point in the history
* Allow tests to opt out of no-cache headers

* Support more than one route HTML file per test

* New e2e test case: back/forward cache behaviour

* Move pageshow listener into the ttvc library

* Capture navigationType for the measurement in question

* Extend browser's builtin navigation type

* Clarify relationship between navigationType and NavigationTimingType in documentation

---------

Co-authored-by: Andrew Hyndman <[email protected]>
  • Loading branch information
ajhyndman and Andrew Hyndman authored Apr 12, 2023
1 parent 5f0c5e0 commit 97b5942
Show file tree
Hide file tree
Showing 7 changed files with 110 additions and 7 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,23 @@ export type Metric = {
// (this can be either a mutation or a load event target, whichever
// occurred last)
lastVisibleChange?: HTMLElement | TimestampedMutationRecord;

// describes how the navigation being measured was initiated
// NOTE: this extends the navigation type values defined in the W3 spec;
// "script" is usually reported as "navigation" by the browser, but we
// report that distinctly
// @see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming/type
navigationType: // Navigation started by clicking a link, by entering the
// URL in the browser's address bar, or by form submission.
| 'navigate'
// Navigation is through the browser's reload operation.
| 'reload'
// Navigation is through the browser's history traversal operation.
| 'back_forward'
// Navigation is initiated by a prerender hint.
| 'prerender'
// Navigation is triggered with a script operation, e.g. in a single page application.
| 'script';
};
};
```
Expand Down
11 changes: 10 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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));

// 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);
});
};

/**
Expand Down
19 changes: 18 additions & 1 deletion src/visuallyCompleteCalculator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@ import {requestAllIdleCallback} from './requestAllIdleCallback';
import {InViewportImageObserver} from './inViewportImageObserver';
import {Logger} from './util/logger';

export type NavigationType =
| 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)
Expand All @@ -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;
};
};

Expand Down Expand Up @@ -88,7 +95,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()', '::', 'index =', navigationIndex);
Expand Down Expand Up @@ -121,12 +128,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;

// report result to subscribers
this.next({
start,
end,
duration: end - start,
detail: {
navigationType,
didNetworkTimeOut,
lastVisibleChange:
this.lastImageLoadTimestamp > (this.lastMutation?.timestamp ?? 0)
Expand Down
8 changes: 8 additions & 0 deletions test/e2e/bfcache/about.html
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>
8 changes: 8 additions & 0 deletions test/e2e/bfcache/index.html
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>
41 changes: 41 additions & 0 deletions test/e2e/bfcache/index.spec.ts
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');
});
});
13 changes: 8 additions & 5 deletions test/server/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ const PORT = process.env.PORT ?? 3000;
const app = express();

// disable browser cache
app.use((req, res, next) => {
res.header('Cache-Control', 'no-cache');
res.header('Vary', '*'); // macOS safari doesn't respect Cache-Control
app.use(({query}, res, next) => {
if (!query?.cache) {
res.header('Cache-Control', 'no-cache');
res.header('Vary', '*'); // macOS safari doesn't respect Cache-Control
}
next();
});

Expand Down Expand Up @@ -47,9 +49,10 @@ app.post('/api', (req, res) => {
res.json(req.body);
});

app.get('/test/:view', ({params}, res) => {
app.get('/test/:view/:route?', ({params}, res) => {
const view = params.view;
res.sendFile(`test/e2e/${view}/index.html`, {root: '.'});
const route = params.route ?? 'index';
res.sendFile(`test/e2e/${view}/${route}.html`, {root: '.'});
});

app.listen(PORT, () => {
Expand Down

0 comments on commit 97b5942

Please sign in to comment.