-
Notifications
You must be signed in to change notification settings - Fork 6
/
Copy pathsoft-navigations.bs
457 lines (341 loc) · 23.8 KB
/
soft-navigations.bs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
<pre class=metadata>
Title: Soft Navigations
Status: CG-DRAFT
Shortname:soft-navigations
Group: wicg
Level: none
Editor: Yoav Weiss, Google https://google.com, [email protected], w3cid 58673
URL: https://wicg.github.io/soft-navigations/
Repository: https://github.com/WICG/soft-navigations
Test Suite: https://github.com/web-platform-tests/wpt/tree/master/soft-navigation-heuristics
Abstract: This document defines a heuristic that would enable browsers to report metrics related to Single Page Apps soft navigations.
Boilerplate: omit conformance
Default Highlight: js
Complain About: accidental-2119 yes
Markup Shorthands: markdown on
</pre>
<pre class=anchors>
urlPrefix: https://html.spec.whatwg.org/C; spec: HTML;
type: dfn; url: #execute-the-script-element; text: execute the script element;
type: dfn; url: #update-document-for-history-step-application; text: update document for history step application;
type: dfn; url: #top-level-traversable; text: top-level traversable;
type: dfn; url: #timer-initialisation-steps; text: timer initialisation steps;
type: dfn; url: #hostmakejobcallback; text: HostMakeJobCallback;
type: dfn; url: #message-port-post-message-steps; text: message port post message steps;
type: dfn; url: #apply-the-history-step; text: apply the history step;
type: dfn; url: #tn-append-session-history-traversal-steps; text: append session history traversal steps;
type: dfn; url: #tn-append-session-history-sync-nav-steps; text: append session history synchronous navigation steps;
type: dfn; url: #prepare-the-script-element; text: prepare the script element;
type: dfn; url: #navigate-fragid; text: navigate to a fragment;
type: dfn; url: #hostcalljobcallback;text: hostcalljobcallback;
type: dfn; url: #she-url; text: session history entry url;
urlPrefix: https://dom.spec.whatwg.org/; spec: DOM;
type: dfn; url: #concept-event-dispatch; text: event dispatch;
type: dfn; url: #concept-node-insert; text: node insert;
type: dfn; url: #dom-event-istrusted; text: isTrusted;
urlPrefix: https://www.w3.org/TR/css-view-transitions-1/
type: dfn; url: #dom-document-startviewtransition; text: startViewTransition();
urlPrefix: https://w3c.github.io/largest-contentful-paint/
type: dfn; url: #has-dispatched-scroll-event; text: has dispatched scroll event;
urlPrefix: https://w3c.github.io/event-timing/
type: dfn; url: #has-dispatched-input-event; text: has dispatched input event;
urlPrefix: https://www.w3.org/TR/paint-timing/
type: dfn; url: #previously-reported-paints; text: previously reported paints;
</pre>
<pre class=link-defaults>
spec:html; type:dfn; text:script;
spec:infra; type:dfn; text:peek;
spec:dom; type:dfn; text:element;
</pre>
Introduction {#sec-intro}
=====================
<div class="non-normative">
<em>This section is non-normative.</em>
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.
</div>
Task Attribution {#sec-task-attribution-intro}
-----------------
<div class="non-normative">
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:
* 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.
</div>
The `SoftNavigationEntry` interface {#sec-interface}
==================================
<pre class=idl>
[Exposed=Window]
interface SoftNavigationEntry : PerformanceEntry {
};
</pre>
Algorithms {#sec-algos}
=====================
<div class=note>
A Soft Navigation is a same document navigation which satisfies the following conditions:
* 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.
</div>
To <dfn>check soft navigation</dfn>, with a [=Document=] |document|, run the following steps:
1. Let |interaction data| be the result of calling [=get current interaction data=] with |document|.
1. If |interaction data|'s [=same document commit=] is false or |interaction data|'s [=contentful paint=] is false, return.
1. Let |global| be |document|'s [=relevant global object=].
1. Let |url| be |interaction data|'s [=interaction data/url=].
1. Let |start time| be |interaction data|'s [=interaction data/start time=].
1. Let |entry| be the result of calling [=create a soft navigation entry=] with |global|, |url|, and |start time|.
1. Call [=emit soft navigation entry=] with |global|, |entry|, |url| and |start time|.
1. Set |interaction data|'s [=interaction data/emitted=] to true.
## Soft navigation entry ## {#sec-entry}
To <dfn>create a soft navigation entry</dfn>, 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 [=global object/realm=].
1. Set |entry|'s {{PerformanceEntry/name}} to be |url|.
1. Set |entry|'s {{PerformanceEntry/entryType}} to be "soft-navigation".
1. Set |entry|'s {{PerformanceEntry/startTime}} to be |start time|.
1. Let |now| be the [=current high resolution time=] given |global|;
1. Let |duration| be the [=duration=] between |now| and |start time|.
1. Set |entry|'s {{PerformanceEntry/duration}} to be |duration|.
1. Return |entry|.
Note: `id` and `navigationId` are set further down, in [=queue a PerformanceEntry=].
To <dfn>emit soft navigation entry</dfn>, with a [=/global object=] |global|, and a {{SoftNavigationEntry}} |entry|, run the following steps:
1. [=queue a performanceentry|Queue=] |entry|.
1. Add |entry| to |global|'s [=performance entry buffer=].
1. Set |global|'s [=has dispatched scroll event=] and [=has dispatched input event=] to false.
1. Let |doc| be |global|'s [=associated Document=].
1. Set |doc|'s [=previously reported paints=] to the empty [=/set=].
1. Set |doc|'s [=interaction task id to interaction data=] to an empty [=/map=].
1. Set |doc|'s [=task id to interaction task id=] to an empty [=/map=].
1. Set |doc|'s [=last interaction task id=] to an empty [=/map=].
## Interaction ## {#sec-interaction}
<dfn>Interaction data</dfn> is a [=struct=] used to maintain the data required to detect a soft navigation from a single interaction.
It has the following [=struct/items=]:
* <dfn for="interaction data">url</dfn>, initially unset - Represents the soft navigation's URL.
* <dfn for="interaction data">start time</dfn>, initially unset - Represents the user interaction event processing start time..
* <dfn for="interaction data">same document commit</dfn> flag, initially unset - Indicates if a same-document commit happened as a result of the interaction.
* <dfn for="interaction data">contentful paint</dfn> flag, initially false - Indicates that a contentful paint happened as a result of an element added by the interaction.
* <dfn for="interaction data">emitted</dfn> flag, initially false - Indicates that a soft navigation entry was emitted by the interaction.
To <dfn>get current interaction data</dfn>, given a [=Document=] |document|, run the following steps:
1. Let |task id| be the result of calling [=get current task ID=].
1. Let |interaction id| be |document|'s [=task id to interaction task id=][|task id|] if it [=map/exists=], or |task id| otherwise.
1. Assert that |document|'s [=interaction task id to interaction data=][|interaction id|] [=map/exists=].
1. Return |document|'s [=interaction task id to interaction data=][|interaction id|].
To <dfn>handle event callback</dfn>, given an {{EventTarget}} |target| and a [=string=] |event type|, run the following steps:
1. Let |document| be |target|'s [=associated Document=].
1. If |document| is not a [=top-level traversable=], return.
1. Let |is click| be true if |event type| [=equals=] "click", and false otherwise.
1. Let |is keyboard| be true if |target| is an {{HTMLBodyElement} and |event type| [=equals=] "keydown", "keyup" or "keypress", and false otherwise.
1. Let |is navigation| be true if |event type| [=equals=] "navigate", and false otherwise.
1. If neither |is click|, |is keyboard| nor |is navigation| is true, return.
1. Let |task| be the result of calling [=get current task ID=].
1. [=set/Append=] |task| to |document|'s [=potential soft navigation task ids=].
1. Let |is new interaction| be true if |is click| is true or if |event type| [=equals=] "keydown", and false otherwise.
1. If |is new interaction| is false:
1. [=map/Set=] |document|'s [=task id to interaction task id=][|task|] to |document|'s [=last interaction task id=].
1. Return null.
1. If |document|'s [=interaction task id to interaction data=][|task|] [=map/exists=], return null.
1. Let |interaction data| be a new [=interaction data=].
1. [=map/Set=] |document|'s [=interaction task id to interaction data=][|task|] to |interaction data|.
1. Return |interaction data|.
To <dfn>terminate event callback handling</dfn>, 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=].
## Same document commit ## {#sec-same-document-commit}
To <dfn>check soft navigation same document commit</dfn>, with [=string=] |url|, run the following steps:
1. Let |interaction data| be the result of calling [=get current interaction data=] with |document|.
1. Let |is soft navigation same document commit| be the result of [=Check ancestor set for task=] given
|document|'s [=potential soft navigation task ids=].
1. [=map/Set=] |interaction data|'s [=same document commit=] to |is soft navigation same document commit|.
1. if |is soft navigation same document commit| is true, [=map/set=] |interaction data|'s [=interaction data/url=] to |url|.
1. Call [=check soft navigation=] with |document|.
## Contentful paint ## {#sec-contentful-paint}
To <dfn>check soft navigation contentful paint</dfn>, 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|.
1. If |element|'s [=node/appended by soft navigation=] is true, set |interaction data|'s [=contentful paint=] to true.
Otherwise, if |interaction data|'s [=interaction data/emitted=] is false, return false.
1. Call [=check soft navigation=] with |document|.
1. Return true.
HTML integration {#sec-html}
=================
Document {#sec-html-document}
----------
Each [=document=] has a <dfn for=document>potential soft navigation task ids</dfn>, a [=/set=] of [=task attribution id=]s.
Each [=document=] has a <dfn for=document>interaction task id to interaction data</dfn>, a [=/map=], initially empty.
Each [=document=] has a <dfn for=document>task id to interaction task id</dfn>, a [=/map=], initially empty.
Each [=document=] has a <dfn for=document>last interaction task id</dfn>, a [=task attribution id=].
History {#sec-html-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 <var ignore>entry</var>'s [=session history entry url|url=].
Event dispatch {#sec-html-events}
----------
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|.
Node {#sec-html-node}
----------
Each [=node=] has a <dfn for=node>appended by soft navigation</dfn> flag, initially unset.
At [=node insert=], add these initial steps:
1. Let |doc| be <var ignore>parent</var>'s [=node document=].
1. 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=].
1. Set <var ignore>node</var>'s [=node/appended by soft navigation=] to |is soft navigation append|.
LCP integration {#sec-lcp-integration}
==========================
In [=potentially add a LargestContentfulPaint entry=], add the following initial step:
1. If the result of calling [=check soft navigation contentful paint=] with <var ignore>element</var> and <var ignore>document</var> is false, return.
Task Attibution Algorithms {#sec-task-attribution-algorithms}
=========================
<p class=note>
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.
</p>
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.
## Task scope ## {#sec-task-scope}
A <dfn id=concept-task-scope>task scope</dfn> is formally defined as a structure.
A [=task scope=] has a <dfn for="task scope">task</dfn>, a [=/task=].
To <dfn>create a task scope</dfn>, given an optional |parent task|, a [=/task=], do the following:
1. Let |task| be a new [=/task=].
1. Set |task|'s [=task attribution ID=] to an [=implementation-defined=] unique value.
1. If |parent task| is provided, set |task|'s [=task/parent task=] to |parent task|.
1. Let |scope| be a new [=task scope=].
1. Set |scope|'s [=task scope/task=] to |task|.
1. Push |scope| to the [=relevant agent=]'s [=agent/event loop=]'s [=task scope stack=].
To <dfn>tear down a task scope</dfn>, do the following:
1. Pop |scope| from the [=relevant agent=]'s [=agent/event loop=]'s [=task scope stack=]
## Is ancestor ## {#sec-is-ancestor}
To <dfn export>check ancestor for task</dfn>, given |ancestor id|, a [=task attribution ID=], run the following:
1. Let |task| be the result of [=get current task=].
1. While true:
1. Let |id| be|task|'s [=task attribution ID=].
1. If |id| is unset, return false.
1. If |id| equals |ancestor id|, return true.
1. Set |task| to |task|'s [=task/parent task=].
## Is ancestor in set ## {#sec-is-ancestor-in-set}
To <dfn export>check ancestor set for task</dfn>, given |ancestor id set|, a [=task attribution ID=] [=/set=], run the following:
1. Let |task| be the result of [=get current task=].
1. While true:
1. Let |id| be |task|'s [=task attribution ID=] if |task| is set, or be unset otherwise.
1. If |id| is unset, return false.
1. If |ancestor id set| [=contains=] |id|, return true.
1. Set |task| to |task|'s [=task/parent task=].
### Get current task ### {#sec-current-task}
To <dfn export>get current task</dfn>, run the following steps:
1. Let |event loop| be the [=relevant agent=]'s [=agent/event loop=].
1. Let |scope| be the result of [=peeking=] into the |event loop|'s [=task scope stack=].
1. Return |scope|'s [=task scope/task=].
### Get current task ID ### {#sec-current-task-id}
To <dfn export>get current task id</dfn>, run the following steps:
1. Let |task| be the result of [=getting current task=].
1. Return |task|'s [=task attribution ID=].
TaskAttribution integration {#sec-task-attribution-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.
## Task additions ## {#sec-task}
A [=/task=] has a <dfn for="task">task attribution ID</dfn>, an [=implementation-defined=] value,
representing a unique identifier. It is initially unset.
A [=/task=] has a <dfn for="task">parent task</dfn>, a [=/task=], initially unset.
## Event Loop additions ##{#sec-event-loop}
Each [=/event loop=] has a <dfn>task scope stack</dfn>, a [=stack=] of <a>task scopes</a>.
## Script execution ##{#sec-script}
A [=script=] element has a <dfn for="script">parent task</dfn> [=/task=], initially unset.
In [=prepare the script element=], add an initial step:
1. Set <var ignore>el</var>'s [=script/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=] <var ignore>el</var>'s [=script/parent task=].
Also, add a terminating step:
1. [=Tear down a task scope=]
## Task queueing ##{#sec-task-queueing}
In [=queue a task=]:
Add these steps after step 3, "Let task be a new task":
1. Set |task|'s [=task/parent task=] to the result of [=getting current task=].
1. [=Create a task scope=] with |task|.
Add a terminating step:
1. [=Tear down a task scope=]
## Timers ##{#sec-timers}
In [=timer initialisation steps=], before step 8, add the following steps:
1. Let |parent task| be the result of [=getting current task=].
1. If |handler| is a Function, set |handler|'s [=callback function/parent task=] to |parent task|.
1. Otherwise, [=create a task scope=] with |parent task|..
TODO: need a teardown here for the second case
## Callbacks ##{#sec-callbacks}
A [=callback function=] has a <dfn for="callback function">parent task</dfn>, a [=/task=], initially unset.
In [=invoke|invoke a callback function=], add the following steps after step 7:
1. Let |task| be <var ignore>callback</var>'s [=callback function/parent task=].
1. [=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 <var ignore>value</var>'s [=callback function/parent task=].
1. [=create a task scope=] with |task| if set, and with nothing otherwise.
Add a terminating step before returning:
1. [=Tear down a task scope=]
ISSUE: 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.
ISSUE: 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=].
## Continuations ##{#sec-continuations}
### HostMakeJobCallback ##{#sec-hostmakejobcallback}
In [=HostMakeJobCallback=], add the following steps:
1. Let |task| be the result of [=getting current task=].
1. Let <var ignore>callable</var>'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.
### HostCallJobCallback ###{#sec-hostcalljobcallback}
In [=HostCallJobCallback=], add initial steps:
1. Let |task| be <var ignore>callback</var>'s [=callback function/parent task=].
1. [=create a task scope=] with |task| if set, and with nothing otherwise.
Add a terminating step:
1. [=Tear down a task scope=]
<p class=note>
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.
</p>
## MessagePorts ## {#sec-messageport}
<p class=note>
For message ports, we want the `message` event callback task to have the task that initiated the `postMessage` as its parent.
</p>
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=].
## Same-document navigations ## {#sec-same-document-navigations}
A [=/traversable navigable=] has a <dfn>navigation task</dfn>, 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.
## View transitions ## {#sec-view-transitions}
In [=startViewTransition()=] add the following initial steps:
1. Set updateCallback's [=callback function/parent task=] to the result of [=getting current task=].
Security & privacy considerations {#priv-sec}
===============================================
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](https://github.com/kyraseevers/Partitioning-visited-links-history).
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.