Soft Navigations

Draft Community Group Report,

This version:
https://wicg.github.io/soft-navigations/
Test Suite:
https://github.com/web-platform-tests/wpt/tree/master/soft-navigation-heuristics
Issue Tracking:
GitHub
Inline In Spec
Editor:
(Google)

Abstract

This document defines a heuristic that would enable browsers to report metrics related to Single Page Apps soft navigations.

Status of this document

This specification was published by the Web Platform Incubator Community Group. It is not a W3C Standard nor is it on the W3C Standards Track. Please note that under the W3C Community Contributor License Agreement (CLA) there is a limited opt-out and other conditions apply. Learn more about W3C Community and Business Groups.

1. Introduction

This section is non-normative.

A Single Page Application or an SPA is a web application that dynamically rewrites the DOM contents when the user navigates from one piece of content to another, instead of loading a new HTML page. Development of SPAs has become a common pattern on the web today. At the same time, browsers haven’t been able to measure performance metrics for such sites. Specifically, JS-driven same-document navigations in SPAs have not been something that browsers detect, and hence went unmeasured.

This specification outlines a heuristic to enable browsers to detect such navigations as Soft Navigations, and report them to the performance timeline and performance observers.

1.1. Task Attribution

The above heuristic relies on the ability to track task ancestory and to know that certain tasks were triggered by other tasks. This specification also outlines a mechanism that would enable user agents to deduce that information, and use it in web exposed features, such as soft navigations.

The user agent’s event loop is continuously running tasks, as well as microtasks. Being able to keep track of which task initiated which can be valuable in multiple cases:

2. The SoftNavigationEntry interface

[Exposed=Window]
interface SoftNavigationEntry : PerformanceEntry {
};

3. Algorithms

A Soft Navigation is a same document navigation which satisfies the following conditions:

To check soft navigation, with a Document document, run the following steps:

  1. Let interaction data be the result of calling get current interaction data with document.

  2. If interaction data’s same document commit is false or interaction data’s contentful paint is false, return.

  3. Let global be document’s relevant global object.

  4. Let url be interaction data’s url.

  5. Let start time be interaction data’s start time.

  6. Let entry be the result of calling create a soft navigation entry with global, url, and start time.

  7. Call emit soft navigation entry with global, entry, url and start time.

  8. Set interaction data’s emitted to true.

3.1. Soft navigation entry

To create a soft navigation entry, with a global object global, a string url, a DOMHighResTimeStamp start time, run the following steps:
  1. Let entry be a new SoftNavigationEntry object in global’s realm.

  2. Set entry’s name to be url.

  3. Set entry’s entryType to be "soft-navigation".

  4. Set entry’s startTime to be start time.

  5. Let now be the current high resolution time given global;

  6. Let duration be the duration between now and start time.

  7. Set entry’s duration to be duration.

  8. Return entry.

Note: id and navigationId are set further down, in queue a PerformanceEntry.

To emit soft navigation entry, with a global object global, and a SoftNavigationEntry entry, run the following steps:

  1. Queue entry.

  2. Add entry to global’s performance entry buffer.

  3. Set global’s has dispatched scroll event and has dispatched input event to false.

  4. Let doc be global’s associated Document.

  5. Set doc’s previously reported paints to the empty set.

  6. Set doc’s interaction task id to interaction data to an empty map.

  7. Set doc’s task id to interaction task id to an empty map.

  8. Set doc’s last interaction task id to an empty map.

3.2. Interaction

Interaction data is a struct used to maintain the data required to detect a soft navigation from a single interaction. It has the following items:

To get current interaction data, given a Document document, run the following steps:

  1. Let task id be the result of calling get current task ID.

  2. Let interaction id be document’s task id to interaction task id[task id] if it exists, or task id otherwise.

  3. Assert that document’s interaction task id to interaction data[interaction id] exists.

  4. Return document’s interaction task id to interaction data[interaction id].

To handle event callback, given an EventTarget target and a string event type, run the following steps:

  1. Let document be target’s associated Document.

  2. If document is not a top-level traversable, return.

  3. Let is click be true if event type equals "click", and false otherwise.

  4. Let is keyboard be true if target is an {{HTMLBodyElement} and event type equals "keydown", "keyup" or "keypress", and false otherwise.

  5. Let is navigation be true if event type equals "navigate", and false otherwise.

  6. If neither is click, is keyboard nor is navigation is true, return.

  7. Let task be the result of calling get current task ID.

  8. Append task to document’s potential soft navigation task ids.

  9. Let is new interaction be true if is click is true or if event type equals "keydown", and false otherwise.

  10. If is new interaction is false:

    1. Set document’s task id to interaction task id[task] to document’s last interaction task id.

    2. Return null.

  11. If document’s interaction task id to interaction data[task] exists, return null.

  12. Let interaction data be a new interaction data.

  13. Set document’s interaction task id to interaction data[task] to interaction data.

  14. Return interaction data.

To terminate event callback handling, given a Document document and null or interaction data interaction data, run the following steps:

  1. Set interaction data’s start time to the current high resolution time given document’s relevant global object.

3.3. Same document commit

To check soft navigation same document commit, with string url, run the following steps:

  1. Let interaction data be the result of calling get current interaction data with document.

  2. Let is soft navigation same document commit be the result of Check ancestor set for task given document’s potential soft navigation task ids.

  3. Set interaction data’s same document commit to is soft navigation same document commit.

  4. if is soft navigation same document commit is true, set interaction data’s url to url.

  5. Call check soft navigation with document.

3.4. Contentful paint

To check soft navigation contentful paint, with Element element and Document document, run the following steps:

  1. Let interaction data be the result of calling get current interaction data with document.

  2. If element’s appended by soft navigation is true, set interaction data’s contentful paint to true.

  3. Call check soft navigation with document.

4. HTML integration

4.1. Document

Each document has a potential soft navigation task ids, a set of task attribution ids.

Each document has a interaction task id to interaction data, a map, initially empty.

Each document has a task id to interaction task id, a map, initially empty.

Each document has a last interaction task id, a task attribution id.

4.2. History

In update document for history step application, before 5.5.1 (if documentsEntryChanged is true and if documentIsNew is false), call check soft navigation same document commit with entry’s url.

4.3. Event dispatch

At event dispatch, after step 5.4 ("Let isActivationEvent be true..."), add the following steps:

  1. If event’s isTrusted is true:

    1. Let interaction data be the result of calling handle event callback with target and event’s type.

At event dispatch, before step 6 (after callback invocation), add the following step:

  1. Call terminate event callback handling with document and interaction data.

4.4. Node

Each node has a appended by soft navigation flag, initially unset.

At node insert, add these initial steps:

  1. Let doc be parent’s node document.

  2. Let is soft navigation append be the result of running Check ancestor for task with the doc’s potential soft navigation task id and the result of calling get current task ID.

  3. Set node’s appended by soft navigation to is soft navigation append.

5. LCP integration

In potentially add a LargestContentfulPaint entry, add the following initial step:
  1. Call check soft navigation contentful paint with element and document.

6. Task Attibution Algorithms

The task attribution algorithms and their integration with HTML are likely to end up integrated into HTML directly. Integration with other specifications is likely to end up in these specifications directly.

The general principle behind task attribution is quite simple:

Each task maintains a connection to its parent task, enabling an implicit data structure that enables querying a task to find if another, specific one is its ancestor.

6.1. Task scope

A task scope is formally defined as a structure.

A task scope has a task, a task.

To create a task scope, given an optional parent task, a task, do the following:

  1. Let task be a new task.

  2. Set task’s task attribution ID to an implementation-defined unique value.

  3. If parent task is provided, set task’s parent task to parent task.

  4. Let scope be a new task scope.

  5. Set scope’s task to task.

  6. Push scope to the relevant agent's event loop's task scope stack.

To tear down a task scope, do the following:

  1. Pop scope from the relevant agent's event loop's task scope stack

6.2. Is ancestor

To check ancestor for task, given ancestor id, a task attribution ID, run the following:
  1. Let task be the result of get current task.

  2. While true:

    1. Let id betask’s task attribution ID.

    2. If id is unset, return false.

    3. If id equals ancestor id, return true.

    4. Set task to task’s parent task.

6.3. Is ancestor in set

To check ancestor set for task, given ancestor id set, a task attribution ID set, run the following:

  1. Let task be the result of get current task.

  2. While true:

    1. Let id be task’s task attribution ID if task is set, or be unset otherwise.

    2. If id is unset, return false.

    3. If ancestor id set contains id, return true.

    4. Set task to task’s parent task.

6.3.1. Get current task

To get current task, run the following steps:
  1. Let event loop be the relevant agent's event loop.

  2. Let scope be the result of peeking into the event loop’s task scope stack.

  3. Return scope’s task.

6.3.2. Get current task ID

To get current task id, run the following steps:
  1. Let task be the result of getting current task.

  2. Return task’s task attribution ID.

7. TaskAttribution integration

Note: Most of this integration is with the HTML spec, although some of it is with WebIDL and CSS ViewTransitions. The desired end state would be for these integrations to be embedded in the relevant specifications.

7.1. Task additions

A task has a task attribution ID, an implementation-defined value, representing a unique identifier. It is initially unset.

A task has a parent task, a task, initially unset.

7.2. Event Loop additions

Each event loop has a task scope stack, a stack of task scopes.

7.3. Script execution

A script element has a parent task task, initially unset.

In prepare the script element, add an initial step:

  1. Set el’s parent task to the result of running get current task.

Note: The parent task ensures that task creation through the injection of scripts can be properly attributed.

In Execute the script element, add initial steps:

  1. Create a task scope el’s parent task.

Also, add a terminating step:

  1. Tear down a task scope

7.4. Task queueing

In queue a task:

Add these steps after step 3, "Let task be a new task":

  1. Set task’s parent task to the result of getting current task.

  2. Create a task scope with task.

Add a terminating step:

  1. Tear down a task scope

7.5. Timers

In timer initialisation steps, before step 8, add the following steps:
  1. Let parent task be the result of getting current task.

  2. If handler is a Function, set handler’s parent task to parent task.

  3. Otherwise, create a task scope with parent task..

TODO: need a teardown here for the second case

7.6. Callbacks

A callback function has a parent task, a task, initially unset.

In invoke a callback function, add the following steps after step 7:

  1. Let task be callback’s parent task.

  2. create a task scope with task if set, and with nothing otherwise.

Add a terminating step:

  1. Tear down a task scope

In call a user object’s operation, add the following steps after step 7:

  1. Let task be value’s parent task.

  2. create a task scope with task if set, and with nothing otherwise.

Add a terminating step before returning:

  1. Tear down a task scope

May be this should be called in "prepare to run a callback"/"clean up after running a callback", but we’d need to pipe in the callback for that.

May be we need to define registration semantics for everything that doesn’t need anything more specific.

In clean up after running a callback, add the following step:

  1. Tear down a task scope.

7.7. Continuations

7.7.1. HostMakeJobCallback ##{#sec-hostmakejobcallback}

In HostMakeJobCallback, add the following steps:
  1. Let task be the result of getting current task.

  2. Let callable’s [=callback function/parent task| be task.

Note: This is needed to ensure the current task is registered on the promise continuations when they are created.

TODO: Figure out if we need to do something in particular with the FinalizationRegsitry.

7.7.2. HostCallJobCallback

In HostCallJobCallback, add initial steps:

  1. Let task be callback’s parent task.

  2. create a task scope with task if set, and with nothing otherwise.

Add a terminating step:

  1. Tear down a task scope

The above is called when promise continuations are run. That does not match Chromium where Blink is not notified when promise continuations are run inside of V8, and hence is subtly different than what’s implemented in Chromium. In Chromium the the continuation task overrides the task stack, where here it pushes a new child task of itself onto that stack. At the same time, there shouldn’t be any functional differences between the two.

7.8. MessagePorts

For message ports, we want the message event callback task to have the task that initiated the postMessage as its parent.

In message port post message steps, add the following steps.

Before step 7, which adds a task, add the following steps:

  1. Let parent task be the result of getting current task.

In step 7.3, which fires the messageerror event, call create a task scope with parent task before firing the event, and tear down a task scope after firing it..

Before step 7.6, which fires the message event, add the following steps:

  1. Call create a task scope with parent task.

After step 7.6, add the following steps:

  1. Call tear down a task scope.

7.9. Same-document navigations

A traversable navigable has a navigation task, a task, initially unset.

In append session history traversal steps, also set traversable’s navigation task to the result of getting current task.

In append session history synchronous navigation steps, also set traversable’s navigation task to the result of getting current task.

In apply the history step in step 14.11.2, pass the navigable’s task to update document for history step application.

In navigate to a fragment step 14, pass the result of getting current task.

In update document for history step application, before firing the popstate event in step 5.5.2, create a task scope with task. tear down a task scope after firing the event.

TODO: more formally define the above.

7.10. View transitions

In startViewTransition() add the following initial steps:

  1. Set updateCallback’s parent task to the result of getting current task.

8. Security & privacy considerations

Exposing Soft Navigations to the performance timeline doesn’t have security and privacy implications on its own. However, reseting the various paint timing entries as a result of a detected soft navigation can have implications, especially before visited links are partitioned. As such, exposing such paint operations without partitioning the :visited cache needs to only be done after careful analysis of the paint operations in question, to make sure they don’t expose the user’s history across origins.

Task Attribution as infrastructure doesn’t directly expose any data to the web, so it doesn’t have any privacy and security implications. Web exposed specifications that rely on this infrastructure could have such implications. As such, they need to be individually examined and have those implications outlined.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[FETCH]
Anne van Kesteren. Fetch Standard. Living Standard. URL: https://fetch.spec.whatwg.org/
[HR-TIME-3]
Yoav Weiss. High Resolution Time. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[LARGEST-CONTENTFUL-PAINT]
Yoav Weiss; Nicolas Pena Moreno. Largest Contentful Paint. URL: https://w3c.github.io/largest-contentful-paint/
[PERFORMANCE-TIMELINE]
Nicolas Pena Moreno. Performance Timeline. URL: https://w3c.github.io/performance-timeline/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

IDL Index

[Exposed=Window]
interface SoftNavigationEntry : PerformanceEntry {
};

Issues Index

May be this should be called in "prepare to run a callback"/"clean up after running a callback", but we’d need to pipe in the callback for that.
May be we need to define registration semantics for everything that doesn’t need anything more specific.