1. Introduction
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 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:
-
Enable user agents to create heuristics that rely on causal link between one operation (e.g. a user initiated click event) and another (e,g. a DOM node append).
-
Enable user agents to make prioritization (of tasks as well as resource loading) "inheritable", and e.g. ensure that low-priority scripts cannot queue high-priority tasks.
-
Enable causal user activation delegation.
-
Enable accumulating knowledge of resource loading dependency chains, and enable developers to draw insights from them.
2. The SoftNavigationEntry
interface
[Exposed =Window ]interface :
SoftNavigationEntry PerformanceEntry { };
3. Algorithms
-
Its navigating task is a descendent of a user interaction task.
-
There exists a DOM node append operation whose task is a descendent of the same user interaction task.
To check soft navigation, with a Document document, run the following steps:
-
Let interaction data be the result of calling get current interaction data with document.
-
If interaction data’s same document commit is false or interaction data’s contentful paint is false, return.
-
Let global be document’s relevant global object.
-
Let url be interaction data’s url.
-
Let start time be interaction data’s start time.
-
Let entry be the result of calling create a soft navigation entry with global, url, and start time.
-
Call emit soft navigation entry with global, entry, url and start time.
-
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, aDOMHighResTimeStamp
start time, run the following steps:
-
Let entry be a new
SoftNavigationEntry
object in global’s realm. -
Set entry’s
name
to be url. -
Set entry’s
entryType
to be "soft-navigation". -
Set entry’s
startTime
to be start time. -
Let now be the current high resolution time given global;
-
Let duration be the duration between now and start time.
-
Set entry’s
duration
to be duration. -
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:
-
Queue entry.
-
Add entry to global’s performance entry buffer.
-
Set global’s has dispatched scroll event and has dispatched input event to false.
-
Let doc be global’s associated Document.
-
Set doc’s previously reported paints to the empty set.
-
Set doc’s interaction task id to interaction data to an empty map.
-
Set doc’s task id to interaction task id to an empty map.
-
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:
-
url, initially unset - Represents the soft navigation’s URL.
-
start time, initially unset - Represents the user interaction event processing start time..
-
same document commit flag, initially unset - Indicates if a same-document commit happened as a result of the interaction.
-
contentful paint flag, initially false - Indicates that a contentful paint happened as a result of an element added by the interaction.
-
emitted flag, initially false - Indicates that a soft navigation entry was emitted by the interaction.
To get current interaction data, given a Document document, run the following steps:
-
Let task id be the result of calling get current task ID.
-
Let interaction id be document’s task id to interaction task id[task id] if it exists, or task id otherwise.
-
Assert that document’s interaction task id to interaction data[interaction id] exists.
-
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:
-
Let document be target’s associated Document.
-
If document is not a top-level traversable, return.
-
Let is click be true if event type equals "click", and false otherwise.
-
Let is keyboard be true if target is an {{HTMLBodyElement} and event type equals "keydown", "keyup" or "keypress", and false otherwise.
-
Let is navigation be true if event type equals "navigate", and false otherwise.
-
If neither is click, is keyboard nor is navigation is true, return.
-
Let task be the result of calling get current task ID.
-
Append task to document’s potential soft navigation task ids.
-
Let is new interaction be true if is click is true or if event type equals "keydown", and false otherwise.
-
If is new interaction is false:
-
Set document’s task id to interaction task id[task] to document’s last interaction task id.
-
Return null.
-
-
If document’s interaction task id to interaction data[task] exists, return null.
-
Let interaction data be a new interaction data.
-
Set document’s interaction task id to interaction data[task] to interaction data.
-
Return interaction data.
To terminate event callback handling, given a Document document and null or interaction data interaction data, run the following steps:
-
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:
-
Let interaction data be the result of calling get current interaction data with document.
-
Let is soft navigation same document commit be the result of Check ancestor set for task given document’s potential soft navigation task ids.
-
Set interaction data’s same document commit to is soft navigation same document commit.
-
if is soft navigation same document commit is true, set interaction data’s url to url.
-
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:
-
Let interaction data be the result of calling get current interaction data with document.
-
If element’s appended by soft navigation is true, set interaction data’s contentful paint to true.
-
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:
-
If event’s isTrusted is true:
-
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:
-
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:
-
Let doc be parent’s node document.
-
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.
-
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:-
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:
-
Script execution creates a task scope
-
Tasks and microtasks that are queued during a task scope’s lifetime are considered its descendents.
-
Certain registered callbacks get an explicit parent task defined. (e.g. the task that registered the callback)
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:
-
Let task be a new task.
-
Set task’s task attribution ID to an implementation-defined unique value.
-
If parent task is provided, set task’s parent task to parent task.
-
Let scope be a new task scope.
-
Set scope’s task to task.
-
Push scope to the relevant agent's event loop's task scope stack.
To tear down a task scope, do the following:
-
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:-
Let task be the result of get current task.
-
While true:
-
Let id betask’s task attribution ID.
-
If id is unset, return false.
-
If id equals ancestor id, return true.
-
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:
-
Let task be the result of get current task.
-
While true:
-
Let id be task’s task attribution ID if task is set, or be unset otherwise.
-
If id is unset, return false.
-
If ancestor id set contains id, return true.
-
Set task to task’s parent task.
-
6.3.1. Get current task
To get current task, run the following steps:-
Let event loop be the relevant agent's event loop.
-
Let scope be the result of peeking into the event loop’s task scope stack.
-
Return scope’s task.
6.3.2. Get current task ID
To get current task id, run the following steps:-
Let task be the result of getting current task.
-
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:
-
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:
Also, add a terminating step:
7.4. Task queueing
In queue a task:Add these steps after step 3, "Let task be a new task":
-
Set task’s parent task to the result of getting current task.
-
Create a task scope with task.
Add a terminating step:
7.5. Timers
In timer initialisation steps, before step 8, add the following steps:-
Let parent task be the result of getting current task.
-
If handler is a Function, set handler’s parent task to parent task.
-
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:
-
Let task be callback’s parent task.
-
create a task scope with task if set, and with nothing otherwise.
Add a terminating step:
In call a user object’s operation, add the following steps after step 7:
-
Let task be value’s parent task.
-
create a task scope with task if set, and with nothing otherwise.
Add a terminating step before returning:
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:
7.7. Continuations
7.7.1. HostMakeJobCallback ##{#sec-hostmakejobcallback}
In HostMakeJobCallback, add the following steps:-
Let task be the result of getting current task.
-
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:
-
Let task be callback’s parent task.
-
create a task scope with task if set, and with nothing otherwise.
Add a terminating step:
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:
-
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:
-
Call create a task scope with parent task.
After step 7.6, add the following steps:
-
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:
-
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.