Skip to content

Commit 5a76650

Browse files
authored
Introduce performance timeline API integration
This replaces our previous broken idea of using currentchange for duration tracking. Closes #59. Closes #33. Closes #14 by removing currentchange entirely for now (we can always add it back later if we see a use case).
1 parent 73a0406 commit 5a76650

File tree

2 files changed

+43
-56
lines changed

2 files changed

+43
-56
lines changed

README.md

Lines changed: 32 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,12 @@ backButtonEl.addEventListener("click", () => {
4848
});
4949
```
5050
51-
The new `currentchange` event fires whenever the current history entry changes, and includes the time it took for a single-page application nav to settle:
51+
A new extension to the [performance timeline API](https://developer.mozilla.org/en-US/docs/Web/API/Performance_Timeline) allows you to calculate the duration of single-page navigations, including any asynchronous work performed by the `navigate` handler:
5252
5353
```js
54-
appHistory.addEventListener("currentchange", e => {
55-
if (e.startTime) {
56-
analyticsPackage.sendEvent("single-page-app-nav", { loadTime: e.timeStamp - e.startTime });
57-
}
58-
});
54+
for (const entry of performance.getEntriesByType("same-document-navigation")) {
55+
console.log(`It took ${entry.duration} ms to navigate to the URL ${entry.name}`);
56+
}
5957
```
6058
6159
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
@@ -86,6 +84,7 @@ appHistory.addEventListener("currentchange", e => {
8684
- [Example: next/previous buttons](#example-nextprevious-buttons)
8785
- [Per-entry events](#per-entry-events)
8886
- [Current entry change monitoring](#current-entry-change-monitoring)
87+
- [Performance timeline API integration](#performance-timeline-api-integration)
8988
- [Complete event sequence](#complete-event-sequence)
9089
- [Guide for migrating from the existing history API](#guide-for-migrating-from-the-existing-history-api)
9190
- [Performing navigations](#performing-navigations)
@@ -225,15 +224,6 @@ The current entry can be replaced with a new entry, with a new `AppHistoryEntry`
225224
226225
- When using the `navigate` event to [convert a cross-document replace navigation into a same-document navigation](#navigation-monitoring-and-interception).
227226
228-
In both cases, for same-document navigations a `currentchange` event will fire on `appHistory`:
229-
230-
```js
231-
appHistory.addEventListener("currentchange", () => {
232-
// appHistory.current has changed: either to a completely new entry (with new key),
233-
// or it has been replaced (keeping the same key).
234-
});
235-
```
236-
237227
### Inspection of the app history list
238228
239229
In addition to the current entry, the entire list of app history entries can be inspected, using `appHistory.entries()`, which returns an array of `AppHistoryEntry` instances. (Recall that all app history entries are same-origin contiguous entries for the current frame, so this is not a security issue.)
@@ -500,7 +490,7 @@ This does not yet solve all accessibility problems with single-page navigations.
500490
501491
Continuing with the theme of `respondWith()` giving ecosystem benefits beyond just web developer convenience, telling the browser about the start time, duration, end time, and success/failure if a single-page app navigation has benefits for metrics gathering.
502492
503-
In particular, analytics frameworks would be able to consume this information from the browser in a way that works across all applications using the app history API. See the example in the [Current entry change monitoring](#current-entry-change-monitoring) section for one way this could look; other possibilities include integrating into the existing [performance APIs](https://w3c.github.io/performance-timeline/).
493+
In particular, analytics frameworks would be able to consume this information from the browser in a way that works across all applications using the app history API. See the discussion on [performance timeline API integration](#performance-timeline-api-integration) for what we are proposing there.
504494
505495
This standardized notion of single-page navigations also gives a hook for other useful metrics to build off of. For example, you could imagine variants of the `"first-paint"` and `"first-contentful-paint"` APIs which are collected after the `navigate` event is fired. Or, you could imagine vendor-specific or application-specific measurements like [Cumulative Layout Shift](https://web.dev/cls/) or React hydration time being reset after such navigations begin.
506496
@@ -875,34 +865,34 @@ await appHistory.navigate("/1-b");
875865

876866
This can be useful for cleaning up any information in secondary stores, such as `sessionStorage` or caches, when we're guaranteed to never reach those particular history entries again.
877867
878-
### Current entry change monitoring
868+
### Performance timeline API integration
879869
880-
**Although the basic idea of an event for when `appHistory.current` changes will probably survive, much of this section needs revamping. See the several discussions linked below.**
870+
The [performance timeline API](https://w3c.github.io/performance-timeline/) provides a generic framework for the browser to signal about interesting events, their durations, and their associated data via `PerformanceEntry` objects. For example, cross-document navigations are done with the [navigation timing API](https://w3c.github.io/navigation-timing/), which uses a subclass of `PerformanceEntry` called `PerformanceNavigationTiming`.
881871
882-
The `window.appHistory` object has an event, `currentchange`, which allows the application to react to any updates to the `appHistory.current` property. This includes both navigations that change its value, and calls to `appHistory.navigate()` that change its state or URL. This cannot be intercepted or canceled, as it occurs after the navigation has already happened; it's just an after-the-fact notification.
872+
Until now, it has not been possible to measure such data for same-document navigations. This is somewhat understandable, as such navigations have always been "zero duration": they occur instantaneously when the application calls `history.pushState()` or `history.replaceState()`. So measuring them isn't that interesting. But with the app history API, [browsers know about the start time, end time, and duration of the navigation](#measuring-standardized-single-page-navigations), so we can give useful performance entries.
883873

884-
This event has one special property, `event.startTime`, which for [same-document](#appendix-types-of-navigations) navigations gives the value of `performance.now()` when the navigation was initiated. This includes for navigations that were originally [cross-document](#appendix-types-of-navigations), like the user clicking on `<a href="https://example.com/another-page">`, but were transformed into same-document navigations by [navigation interception](#navigation-monitoring-and-interception). For completely cross-document navigations, `startTime` will be `null`.
874+
The `PerformanceEntry` instances for such same-document navigations are instances of a new subclass, `SameDocumentNavigationEntry`, with the following properties:
885875

886-
"Initiated" means either when the corresponding API was called (like `location.href` or `appHistory.navigate()`), or when the user activated the corresponding `<a>` element, or submitted the corresponding `<form>`. This allows it to be used for determining the overall time from navigation initiation to navigation completion, including the time it took for a promise passed to `e.respondWith()` to settle:
876+
- `name`: the URL being navigated to. (The use of `name` instead of `url` is strange, but matches all the other `PerformanceEntry`s on the platform.)
887877

888-
```js
889-
appHistory.addEventListener("currentchange", e => {
890-
if (e.startTime) {
891-
const loadTime = e.timeStamp - e.startTime;
878+
- `entryType`: always `"same-document-navigation"`.
892879

893-
document.querySelector("#status-bar").textContent = `Loaded in ${loadTime} ms!`;
894-
analyticsPackage.sendEvent("single-page-app-nav", { loadTime });
895-
} else {
896-
document.querySelector("#status-bar").textContent = `Welcome to this document!`;
897-
}
898-
});
899-
```
880+
- `startTime`: the time at which the navigation was initiated, i.e. when the corresponding API was called (like `location.href` or `appHistory.navigate()`), or when the user activated the corresponding `<a>` element, or submitted the corresponding `<form>`.
881+
882+
- `duration`: the duration of the navigation, which is either `0` for `history.pushState()`/`history.replaceState()`, or is the duration it takes the promise passed to `event.respondWith()` to settle, for navigations intercepted by a `navigate` event handler.
900883

901-
_TODO: reconsider cross-document navigations. There will only be one (the initial load of the page); should we even fire this event in that case? (That's [#31](https://github.com/WICG/app-history/issues/31).) Could we give `startTime` a useful value there, if we do?_
884+
- `success`: `false` if the promise passed to `event.respondWith()` rejected; `true` otherwise (including for `history.pushState()`/`history.replaceState()`).
902885

903-
_TODO: this property-on-the-event design is not good and does not work, per [#59](https://github.com/WICG/app-history/issues/59). We should probably integrate with the performance timeline APIs instead? Discuss in [#33](https://github.com/WICG/app-history/issues/33)._
886+
To record single-page navigations using [`PerformanceObserver`](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceObserver), web developers could then use code such as the following:
904887

905-
_TODO: Add a non-analytics examples, similar to how people use `popstate` today. [#14](https://github.com/WICG/app-history/issues/14)_
888+
```js
889+
const observer = new PerformanceObserver(list => {
890+
for (const entry of list.getEntries()) {
891+
analyticsPackage.send("same-document-navigation", entry.toJSON());
892+
}
893+
});
894+
observer.observe({ type: "same-document-navigation" });
895+
```
906896

907897
### Complete event sequence
908898

@@ -915,7 +905,6 @@ Between the per-`AppHistoryEntry` events and the `window.appHistory` events, as
915905
1. `appHistory.current` fires `navigatefrom`.
916906
1. `location.href` updates.
917907
1. `appHistory.current` updates. `appHistory.transition` is created.
918-
1. `currentchange` fires on `window.appHistory`.
919908
1. `appHistory.current` fires `navigateto`.
920909
1. Any now-unreachable `AppHistoryEntry` instances fire `dispose`.
921910
1. The URL bar updates.
@@ -927,20 +916,23 @@ Between the per-`AppHistoryEntry` events and the `window.appHistory` events, as
927916
1. If the process was initiated by a call to an `appHistory` API that returns a promise, then that promise gets fulfilled.
928917
1. `appHistory.transition.finished` fulfills with undefined.
929918
1. `appHistory.transition` becomes null.
919+
1. Queue a new `SameDocumentNavigationEntry` indicating success.
930920
1. Alternately, if the promise passed to `event.respondWith()` rejects:
931921
1. `appHistory.current` fires `finish`.
932922
1. `navigateerror` fires on `window.appHistory` with the rejection reason as its `error` property.
933923
1. Any loading spinner UI stops.
934924
1. If the process was initiated by a call to an `appHistory` API that returns a promise, then that promise gets rejected with the same rejection reason.
935925
1. `appHistory.transition.finished` rejects with the same rejection reason.
936926
1. `appHistory.transition` becomes null.
927+
1. Queue a new `SameDocumentNavigationEntry` indicating failure.
937928
1. Alternately, if the navigation gets [aborted](#aborted-navigations) before either of those two things occur:
938929
1. (`appHistory.current` never fires the `finish` event.)
939930
1. `navigateerror` fires on `window.appHistory` with an `"AbortError"` `DOMException` as its `error` property.
940931
1. Any loading spinner UI stops. (But potentially restarts, or maybe doesn't stop at all, if the navigation was aborted due to a second navigation starting.)
941932
1. If the process was initiated by a call to an `appHistory` API that returns a promise, then that promise gets rejected with the same `"AbortError"` `DOMException`.
942933
1. `appHistory.transition.finished` rejects with the same `"AbortError"` `DOMException`.
943934
1. `appHistory.transition` becomes null.
935+
1. Queue a new `SameDocumentNavigationEntry` indicating failure.
944936

945937
For more detailed analysis, including specific code examples, see [this dedicated document](./interception-details.md).
946938

@@ -1094,7 +1086,7 @@ The app history API provides several replacements that subsume these events:
10941086
10951087
- To react to and potentially intercept navigations before they complete, use the `navigate` event on `appHistory`. See the [Navigation monitoring and interception](#navigation-monitoring-and-interception) section for more details, including how the event object provides useful information that can be used to distinguish different types of navigations.
10961088
1097-
- To react to navigations that have completed, use the `currentchange` event on `appHistory`. See the [Current entry change monitoring](#current-entry-change-monitoring) section for more details, including an example of how to use it to determine how long a same-document navigation took.
1089+
- To react to navigations that have completed, use the `navigatesuccess` or `navigateerror` events on `appHistory`. Note that these will only be fired asynchronously, after any handlers passed to the `navigate` event's `event.respondWith()` method have completed.
10981090
10991091
- To watch a particular entry to see when it's navigated to, navigated from, or becomes unreachable, use that `AppHistoryEntry`'s `navigateto`, `navigatefrom`, and `dispose` events. See the [Per-entry events](#per-entry-events) section for more details.
11001092
@@ -1350,7 +1342,6 @@ interface AppHistory : EventTarget {
13501342
attribute EventHandler onnavigate;
13511343
attribute EventHandler onnavigatesuccess;
13521344
attribute EventHandler onnavigateerror;
1353-
attribute EventHandler oncurrentchange;
13541345
};
13551346
13561347
[Exposed=Window]
@@ -1434,13 +1425,8 @@ interface AppHistoryDestination {
14341425
};
14351426
14361427
[Exposed=Window]
1437-
interface AppHistoryCurrentChangeEvent : Event {
1438-
constructor(DOMString type, optional AppHistorycurrentChangeEventInit eventInit = {});
1439-
1440-
readonly attribute DOMHighResTimeStamp? startTime;
1441-
};
1442-
1443-
dictionary AppHistoryCurrentChangeEventInit : EventInit {
1444-
DOMHighResTimeStamp? startTime = null;
1428+
interface SameDocumentNavigationEntry : PerformanceEntry {
1429+
readonly attribute boolean success;
1430+
[Default] object toJSON();
14451431
};
14461432
```

0 commit comments

Comments
 (0)