-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathindex.bs
More file actions
625 lines (481 loc) · 41 KB
/
index.bs
File metadata and controls
625 lines (481 loc) · 41 KB
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
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
<pre class=metadata>
Title: Soft Navigations and Interaction Contentful Paint
Status: CG-DRAFT
Shortname: soft-navigations
Group: wicg
Level: none
Editor: Michal Mocny, Google https://google.com, mmocny@chromium.org, w3cid 110398
Scott Haseley, Google https://google.com, shaseley@google.com
Former Editor: Yoav Weiss, Shopify https://shopify.com, yoav@yoav.ws, 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 mechanism for measuring interaction-initiated effects, enabling user agents to attribute page modifications and performance entries back to the user interactions that triggered them.
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/multipage/webappapis.html; spec: HTML;
type: dfn;
text: execute the script element; url: #execute-the-script-element;
text: timer initialisation steps; url: #timer-initialisation-steps;
text: HostMakeJobCallback; url: #hostmakejobcallback;
text: hostcalljobcallback; url: #hostcalljobcallback;
urlPrefix: https://html.spec.whatwg.org/multipage/browsing-the-web.html; spec: HTML;
type: dfn;
text: update document for history step application; url: #update-document-for-history-step-application;
text: top-level traversable; url: #top-level-traversable;
text: apply the history step; url: #apply-the-history-step;
text: append session history traversal steps; url: #tn-append-session-history-traversal-steps;
text: append session history synchronous navigation steps; url: #tn-append-session-history-sync-nav-steps;
text: navigate to a fragment; url: #navigate-fragid;
text: session history entry url; url: #she-url;
urlPrefix: https://html.spec.whatwg.org/multipage/scripting.html; spec: HTML;
type: dfn; text: prepare the script element; url: #prepare-the-script-element;
urlPrefix: https://html.spec.whatwg.org/multipage/web-messaging.html; spec: HTML;
type: dfn; text: message port post message steps; url: #message-port-post-message-steps;
urlPrefix: https://html.spec.whatwg.org/multipage/nav-history-apis.html; spec: HTML;
type: dfn; text: shared history push/replace steps; url: #shared-history-push/replace-steps;
urlPrefix: https://dom.spec.whatwg.org/; spec: DOM;
type: dfn;
text: event dispatch; url: #concept-event-dispatch;
text: node insert; url: #concept-node-insert;
text: isTrusted; url: #dom-event-istrusted;
text: event; url: #concept-event;
urlPrefix: https://www.w3.org/TR/css-view-transitions-1/
type: dfn; text: startViewTransition(); url: #dom-document-startviewtransition;
urlPrefix: https://w3c.github.io/performance-timeline/; spec: PERFORMANCE-TIMELINE
type: dfn; text: queue a PerformanceEntry; url: #dfn-queue-a-performanceentry;
urlPrefix: https://w3c.github.io/largest-contentful-paint/; spec: LARGEST-CONTENTFUL-PAINT
type: dfn;
text: has dispatched scroll event; url: #has-dispatched-scroll-event;
text: report largest contentful paint; url: #report-largest-contentful-paint;
urlPrefix: https://w3c.github.io/event-timing/
type: dfn;
text: has dispatched input event; url: #has-dispatched-input-event;
text: interaction id; url: #interaction-id;
text: interaction; url: #dfn-interaction;
urlPrefix: https://w3c.github.io/long-animation-frames/; spec: LONG-ANIMATION-FRAMES;
type: interface; text: PerformanceLongAnimationFrameTiming; url: #sec-PerformanceLongAnimationFrameTiming;
type: dfn; text: long animation frame; url: #dfn-long-animation-frame;
type: dfn; text: report long animation frames; url: #dfn-report-long-animation-frames;
urlPrefix: https://pr-preview.s3.amazonaws.com/mmocny/event-timing/pull/165.html; spec: EVENT-TIMING;
type: dfn; text: interaction count; url: #window-interaction-count;
type: dfn; text: initial interactionId value; url: #window-initial-interactionid-value;
type: dfn; text: interactionId increment; url: #window-interactionid-increment;
type: dfn; text: get the next interactionId; url: #get-the-next-interactionid;
urlPrefix: https://w3c.github.io/paint-timing/
type: dfn;
text: previously reported paints; url: #previously-reported-paints;
text: pending image record; url: #pending-image-record;
text: pending image record element; url: #pending-image-record-element;
text: paint timing info; url: #paint-timing-info;
type: dfn; for: paint timing info;
text: rendering update end time; url: #paint-timing-info-rendering-update-end-time;
text: implementation-defined presentation time; url: #paint-timing-info-implementation-defined-presentation-time;
type: interface; text: PaintTimingMixin; url: #paint-timing-mixin;
type: attribute; for: PaintTimingMixin;
text: paintTime; url: #dom-painttimingmixin-painttime;
text: presentationTime; url: #dom-painttimingmixin-presentationtime;
</pre>
<pre class=link-defaults>
spec:dom; type:dfn; text:element;
spec:dom; type:dfn; for:/; text:document;
spec:html; type:dfn; for:/; text:global object;
spec:dom; type:dfn; text:event;
spec:dom; type:dfn; for:event; text:type;
spec:hr-time-3; type:dfn; for:/; text:duration;
</pre>
<pre class=biblio>
{
"ASYNC-CONTEXT": {
"title": "AsyncContext",
"href": "https://github.com/tc39/proposal-async-context"
},
"CONTAINER-TIMING": {
"title": "Container Timing API",
"href": "https://github.com/WICG/container-timing"
}
}
</pre>
Introduction {#sec-intro}
=====================
<div class="non-normative">
<em>This section is non-normative.</em>
Modern web applications often dynamically update content in response to user interactions without performing a full page navigation. These interaction-initiated effects—such as structural DOM modifications, contentful paints, and history state changes—have historically been difficult to measure and attribute to the correct user actions.
Consider a typical Single Page Application pattern: a user clicks a product link, which triggers a `click` event handler. This handler initiates a network `fetch()` for product details. When the response arrives, a callback is executed that dynamically injects the new content into the DOM and uses the History or Navigation API to update the URL. While this appears to the user as a navigation, existing metrics like Largest Contentful Paint (LCP) ([[LARGEST-CONTENTFUL-PAINT]]) only measure the initial page load, and Interaction to Next Paint (INP) only measures the immediate visual feedback of the click itself, leaving the significant subsequent rendering and "soft" navigation uncaptured.
This specification leverages the [[EVENT-TIMING]] API to define interactions and the [[ASYNC-CONTEXT]] proposal to track causality across asynchronous task boundaries. It defines how browsers can identify and report these effects, including "Soft Navigations," by integrating with [[PAINT-TIMING]] and [[LARGEST-CONTENTFUL-PAINT]] to attribute rendering changes to the performance timeline.
Furthermore, this specification integrates with the [[CONTAINER-TIMING]] proposal. When a user interaction causes DOM modifications, those modified nodes are designated as new "container roots." Any subsequent contentful paints within those subtrees are attributed to their respective roots, which are then traced back to the original interaction. This allows for efficient and accurate measurement of rich, dynamic page updates that occur far after the initial event dispatch.
</div>
Interaction Infrastructure {#sec-infrastructure}
=====================
Navigation ID {#sec-nav-id}
-----------------
A <dfn export>navigation id</dfn> is a unique identifier assigned to each navigation (both hard and soft) within a [=global object=]'s lifetime.
Each [=/global object=] has a <dfn for="global object">current navigation id</dfn>, an `unsigned long long`, initially set to the same value that initializes the [=Document=]'s [=initial interactionId value=] and the initial `navigationId` for Navigation Timing.
Interaction Context Intro {#sec-interaction-context-intro}
-----------------
<div class="non-normative">
Soft navigation detection relies on the ability to track the causality of tasks and observe that certain operations (e.g., a DOM node append) were triggered by a specific user interaction.
This specification leverages the TC39 [[ASYNC-CONTEXT]] proposal to handle this propagation. Every new user interaction (as defined by Event Timing) or relevant navigation event creates a new **InteractionContext**. This context is stored in a hidden, internal-only `AsyncContext.Variable` (denoted as `[[ActiveInteractionContext]]`).
The web platform's integration with AsyncContext ensures that this variable is automatically attached to asynchronous continuations (e.g., `setTimeout`, `fetch`, `await`), allowing the browser to attribute later effects back to the original interaction.
In addition to script propagation, this specification defines how user interactions that modify the DOM establish **container roots** (via [[CONTAINER-TIMING]]). These roots allow subsequent rendering effects, like contentful paints, to be traced back to the initiating interaction context even when no asynchronous script is currently active.
</div>
The InteractionContext struct {#sec-interaction}
-----------------
<div dfn-for="InteractionContext">
<dfn>InteractionContext</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>id</dfn>, initially unset - The [=interaction id=] associated with this context.
* <dfn>start time</dfn>, initially unset - Represents the start time of the interaction.
* <dfn>navigation type</dfn>, initially unset - Represents the type of navigation (e.g., "push", "replace").
* <dfn>first URL value</dfn>, initially unset - Represents the value of the URL at the first modification.
* <dfn>first URL update timestamp</dfn>, initially unset - Represents the time of the first URL modification during this interaction.
* <dfn>first scroll timestamp</dfn>, initially unset - Represents the time of the first scroll event during this interaction.
* <dfn>first input timestamp</dfn>, initially unset - Represents the time of the first input event during this interaction.
* <dfn>first contentful paint</dfn>, initially null - The first {{InteractionContentfulPaint}} entry for this interaction.
* <dfn>largest contentful paint</dfn>, initially null - The largest {{InteractionContentfulPaint}} entry for this interaction so far.
* <dfn>total painted area</dfn>, initially 0 - The sum of the areas of all contentful paints attributed to this interaction.
* <dfn>last URL value</dfn>, initially unset - Represents the value of the most recent URL modification during this interaction.
* <dfn>emitted</dfn> flag, initially false - Indicates that a soft navigation entry was emitted by the interaction.
</div>
<div class="note">
Future versions of this specification might track all URL updates that occur during an interaction context to provide a more complete history of the navigation. The **last URL value** is tracked internally to ensure accurate attribution of effects back to the final state of the interaction, even if only the **first URL value** is currently exposed in the {{SoftNavigationEntry}}'s name.
</div>
<div class="note">
The concept of **total painted area** is expected to be refined as part of the [[CONTAINER-TIMING]] integration, potentially moving towards tracking specific updated regions.
</div>
Infrastructure Algorithms {#sec-infra-algos}
-----------------
<div algorithm="get current interaction context">
To <dfn>get current interaction context</dfn>, run the following steps:
1. Let |context| be the value of the internal `AsyncContext.Variable` `[[ActiveInteractionContext]]`.
1. Return |context|.
</div>
<div algorithm="get or create context for interaction">
To <dfn>get or create context for interaction</dfn>, given a [=Document=] |document|, an |interaction id|, and an [=Event=] |event|, run the following steps:
1. If |document|'s [=interaction id to interaction context=][|interaction id|] [=map/exists=], return |document|'s [=interaction id to interaction context=][|interaction id|].
1. Let |interaction context| be a new [=InteractionContext=].
1. Set |interaction context|'s [=InteractionContext/id=] to |interaction id|.
1. Set |interaction context|'s [=InteractionContext/start time=] to |event|'s `startTime`.
1. [=map/Set=] |document|'s [=interaction id to interaction context=][|interaction id|] to |interaction context|.
1. Return |interaction context|.
</div>
<div algorithm="update interaction contexts for event">
To <dfn>update interaction contexts for event</dfn>, given a [=Document=] |document| and an [=Event=] |event|, run the following steps:
1. Let |timestamp| be the [=current high resolution time=] given |document|'s [=relevant global object=].
1. Let |is scroll| be true if |event|'s type is "scroll", and false otherwise.
1. Let |is input| be true if |event|'s type is an event type that would trigger [=has dispatched input event=] (as defined in [[EVENT-TIMING]]), and false otherwise.
1. If |is scroll| is false and |is input| is false, return.
1. For each |interaction context| of |document|'s [=interaction id to interaction context=]'s values:
1. If |is scroll| is true and |interaction context|'s [=InteractionContext/first scroll timestamp=] is unset:
1. Set |interaction context|'s [=InteractionContext/first scroll timestamp=] to |timestamp|.
1. If |is input| is true and |interaction context|'s [=InteractionContext/first input timestamp=] is unset:
1. Set |interaction context|'s [=InteractionContext/first input timestamp=] to |timestamp|.
</div>
<div algorithm="interaction event processing start">
To <dfn>interaction event processing start</dfn>, given a [=Document=] |document|, an |interaction id|, and an [=Event=] |event|, run the following steps:
1. If |interaction id| is null, return null.
1. Let |interaction context| be the result of calling [=get or create context for interaction=] with |document|, |interaction id|, and |event|.
1. Set the value of the internal `AsyncContext.Variable` `[[ActiveInteractionContext]]` to |interaction context|.
1. Return |interaction context|.
</div>
<div algorithm="interaction event timing processing end">
To <dfn>interaction event timing processing end</dfn>, given null or [=InteractionContext=] |interaction context|, run the following steps:
1. If |interaction context| is null, return.
1. Set the value of the internal `AsyncContext.Variable` `[[ActiveInteractionContext]]` to null.
</div>
Interaction Contentful Paints {#sec-interaction-contentful-paint}
=====================
The `InteractionContentfulPaint` interface {#sec-interaction-contentful-paint-interface}
-----------------
<pre class=idl>
[Exposed=Window]
interface InteractionContentfulPaint : PerformanceEntry {
readonly attribute DOMHighResTimeStamp renderTime;
readonly attribute DOMHighResTimeStamp loadTime;
readonly attribute unsigned long long size;
readonly attribute DOMString id;
readonly attribute DOMString url;
readonly attribute Element? element;
readonly attribute unsigned long long interactionId;
object toJSON();
};
InteractionContentfulPaint includes PaintTimingMixin;
</pre>
Each {{InteractionContentfulPaint}} has an associated [=paint timing info=].
<div class="note">
Today this is modelled after the Largest Contentful Paint (LCP) performance entry, but a future direction is to model after Container Timing performance entry.
</div>
<div dfn-for="InteractionContentfulPaint">
The {{renderTime}} attribute's getter must return [=this=]'s associated [=paint timing info=]'s [=paint timing info/rendering update end time=].
The {{loadTime}} attribute's getter must return [=this=]'s associated [=paint timing info=]'s [=paint timing info/implementation-defined presentation time=].
The {{size}} attribute's getter must return the contentful paint's size.
The {{id}} attribute's getter must return the contentful paint's ID.
The {{url}} attribute's getter must return the contentful paint's URL.
The {{element}} attribute's getter must return the {{Element}} associated with the contentful paint, or null if the element has been removed from the document.
The {{InteractionContentfulPaint/interactionId}} attribute's getter must return the [=interaction id=] of the interaction that triggered this paint.
</div>
Interaction Contentful Paint Algorithms {#sec-interaction-contentful-paint-algos}
-----------------
<div algorithm="create an interaction contentful paint entry">
To <dfn>create an interaction contentful paint entry</dfn>, with a [=/global object=] |global|, an [=InteractionContext=] |interaction context|, and an [=/Element=] |element|, run the following steps:
1. Let |entry| be a new {{InteractionContentfulPaint}} object in |global|'s [=global object/realm=].
1. Set |entry|'s {{PerformanceEntry/entryType}} to be "interaction-contentful-paint".
1. Set |entry|'s {{InteractionContentfulPaint/element}} to be |element|.
1. Set |entry|'s {{InteractionContentfulPaint/interactionId}} to |interaction context|'s [=InteractionContext/id=].
1. Set |entry|'s associated [=paint timing info=] to a new [=paint timing info=].
1. Set entry's {{InteractionContentfulPaint/renderTime}}, {{InteractionContentfulPaint/loadTime}}, {{InteractionContentfulPaint/size}}, {{InteractionContentfulPaint/id}}, and {{InteractionContentfulPaint/url}} to the corresponding values of the contentful paint candidate as defined in the [=report largest contentful paint=] algorithm in [[LARGEST-CONTENTFUL-PAINT]].
1. Set |entry|'s {{PerformanceEntry/startTime}} to |interaction context|'s [=InteractionContext/start time=].
1. Set |entry|'s {{PerformanceEntry/duration}} to the difference between |entry|'s {{InteractionContentfulPaint/renderTime}} and |entry|'s {{PerformanceEntry/startTime}}.
1. Return |entry|.
</div>
<div algorithm="emit interaction contentful paint entry">
To <dfn>emit interaction contentful paint entry</dfn>, given an [=/Element=] |element|, a [=Document=] |document|, and an [=InteractionContext=] |interaction context|, run the following steps:
1. Let |global| be |document|'s [=relevant global object=].
1. Let |paint entry| be the result of calling [=create an interaction contentful paint entry=] with |global|, |interaction context|, and |element|.
Note: For elements that require resource loading (e.g., an image with a new `src`), the [[PAINT-TIMING]] specification is responsible for ensuring the "is loaded" criteria are met before triggering the contentful paint detection that eventually calls this algorithm.
1. If |interaction context|'s [=InteractionContext/first scroll timestamp=] is set and |paint entry|'s {{InteractionContentfulPaint/renderTime}} is greater than |interaction context|'s [=InteractionContext/first scroll timestamp=], return.
1. If |interaction context|'s [=InteractionContext/first input timestamp=] is set and |paint entry|'s {{InteractionContentfulPaint/renderTime}} is greater than |interaction context|'s [=InteractionContext/first input timestamp=], return.
1. [=queue a performanceentry|Queue=] |paint entry|.
1. Add |paint entry| to |global|'s [=performance entry buffer=].
1. If |interaction context|'s [=InteractionContext/first contentful paint=] is null:
1. Set |interaction context|'s [=InteractionContext/first contentful paint=] to |paint entry|.
1. Set |interaction context|'s [=InteractionContext/largest contentful paint=] to |paint entry|.
1. Increment |interaction context|'s [=InteractionContext/total painted area=] by |paint entry|'s size.
</div>
<div class="note">
Note: `InteractionContentfulPaint` entries are emitted to the performance timeline as they are detected, independently of whether the interaction eventually results in a soft navigation. This allows developers to monitor rendering updates for all interactions.
</div>
Soft Navigations {#sec-soft-navs}
=====================
The `SoftNavigationEntry` interface {#sec-interface}
-----------------
<pre class=idl>
[Exposed=Window]
interface SoftNavigationEntry : PerformanceEntry {
readonly attribute DOMString navigationType;
readonly attribute unsigned long long interactionId;
readonly attribute InteractionContentfulPaint? largestInteractionContentfulPaint;
};
SoftNavigationEntry includes PaintTimingMixin;
</pre>
Each {{SoftNavigationEntry}} has an associated [=paint timing info=].
<div dfn-for="SoftNavigationEntry">
The {{SoftNavigationEntry/navigationType}} attribute's getter must return the [=InteractionContext/navigation type=] of the interaction that triggered the soft navigation.
The {{SoftNavigationEntry/interactionId}} attribute's getter must return the [=interaction id=] of the interaction that triggered the soft navigation.
The {{SoftNavigationEntry/largestInteractionContentfulPaint}} attribute's getter must return an {{InteractionContentfulPaint}} entry representing the largest contentful paint that occurred as a result of the interaction, or null if no such paint has occurred.
The {{PerformanceEntry/name}} attribute's getter must return the [=InteractionContext/first URL value=] of the interaction that triggered the soft navigation.
The {{PerformanceEntry/startTime}} attribute's getter must return the [=InteractionContext/start time=] of the interaction that triggered the soft navigation.
The {{PerformanceEntry/duration}} attribute's getter must return the difference between [=this=]'s {{PaintTimingMixin/presentationTime}} and [=this=]'s {{PerformanceEntry/startTime}} at the time of emission.
</div>
<div class="note">
In practice, {{SoftNavigationEntry/largestInteractionContentfulPaint}} is expected to be non-null, as a confirmed soft navigation requires at least one detected interaction contentful paint to trigger its emission.
However, future iterations could evaluate whether a soft navigation could be considered committed immediately upon URL modification, prior to the first paint. In such a model, this field might be null at the time of emission.
The primary design consideration for this timing is the attribution of other timeline entries (e.g., `LayoutShift`, `PerformanceResourceTiming`, `LongAnimationFrameTiming`, etc) that occur in the interval between the URL modification and the first paint. Many sites commit the new URL immediately upon initiating a fetch request, for example, even while the current page remains in the previous navigation state. To ensure consistent timeline slicing, this specification attributes all such entries to the *previous* navigation identifier until the first paint confirms the transition.
</div>
Soft Navigation Algorithms {#sec-soft-nav-algos}
-----------------
A <dfn export>soft navigation</dfn> is a same-document navigation that satisfies the following conditions:
* A same-document URL change occurs while an [=InteractionContext=] is active.
* A contentful paint occurs that is attributed to the same [=InteractionContext=].
<div algorithm="evaluate soft navigation emission">
To <dfn>evaluate soft navigation emission</dfn>, given a [=Document=] |document| and an [=InteractionContext=] |interaction context|, run the following steps:
1. If |interaction context|'s [=InteractionContext/emitted=] is true, return.
1. If |document|'s [=document/active soft navigation candidate=] is not |interaction context|, return.
1. If |interaction context|'s [=InteractionContext/first URL value=] is unset, return.
1. If |interaction context|'s [=InteractionContext/first contentful paint=] is null, return.
1. Let |global| be |document|'s [=relevant global object=].
1. Let |url| be |interaction context|'s [=InteractionContext/first URL value=].
1. Let |entry| be the result of calling [=create a soft navigation entry=] with |global|, |interaction context|, |url|, and |interaction context|'s [=InteractionContext/start time=].
1. Call [=emit soft navigation entry=] with |global|, |entry|.
1. Set |interaction context|'s [=InteractionContext/emitted=] to true.
</div>
<div algorithm="create a soft navigation entry">
To <dfn>create a soft navigation entry</dfn>, with a [=/global object=] |global|, an [=InteractionContext=] |interaction context|, a [=string=] |url|, and 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 |first paint| be |interaction context|'s [=InteractionContext/first contentful paint=].
1. If |first paint| is not null:
1. Set |entry|'s associated [=paint timing info=] to |first paint|'s associated [=paint timing info=].
1. Set |entry|'s {{SoftNavigationEntry/interactionId}} to |interaction context|'s [=InteractionContext/id=] (if available).
1. Set |entry|'s {{SoftNavigationEntry/largestInteractionContentfulPaint}} to |interaction context|'s [=InteractionContext/largest contentful paint=].
1. Return |entry|.
</div>
<div class="note">
Note: `navigationId` is set further down, in [=queue a PerformanceEntry=].
</div>
<div algorithm="emit soft navigation entry">
To <dfn>emit soft navigation entry</dfn>, with a [=/global object=] |global|, and a {{SoftNavigationEntry}} |entry|, run the following steps:
1. Set |entry|'s [=PerformanceEntry/navigation id=] to |entry|'s {{SoftNavigationEntry/interactionId}}.
1. Set |global|'s [=global object/current navigation id=] to |entry|'s {{SoftNavigationEntry/interactionId}}.
1. [=queue a performanceentry|Queue=] |entry|.
1. Add |entry| to |global|'s [=performance entry buffer=].
</div>
<div algorithm="process same document commit">
To <dfn>process same document commit</dfn>, with a [=Document=] |document|, [=string=] |url|, and [=string=] |navigation type|, run the following steps:
1. Let |interaction context| be the result of calling [=get current interaction context=].
1. If |interaction context| is null, return.
1. Set |interaction context|'s [=InteractionContext/last URL value=] to |url|.
1. If |interaction context|'s [=InteractionContext/first URL update timestamp=] is unset:
1. Set |interaction context|'s [=InteractionContext/first URL update timestamp=] to the [=current high resolution time=] given |document|'s [=relevant global object=].
1. Set |interaction context|'s [=InteractionContext/first URL value=] to |url|.
1. Set |interaction context|'s [=InteractionContext/navigation type=] to |navigation type|.
1. Set |document|'s [=document/active soft navigation candidate=] to |interaction context|.
1. Call [=evaluate soft navigation emission=] with |document| and |interaction context|.
</div>
The `PerformanceEntry` extension {#sec-pe-extension}
==================================
<pre class=idl>
[Exposed=(Window,Worker)]
partial interface PerformanceEntry {
readonly attribute unsigned long long navigationId;
};
</pre>
<div dfn-for="PerformanceEntry">
Each {{PerformanceEntry}} has an associated <dfn for="PerformanceEntry">navigation id</dfn>, an `unsigned long long`, initially 0.
The {{navigationId}} attribute's getter must return [=this=]'s [=PerformanceEntry/navigation id=].
</div>
Specification Integrations {#sec-integrations}
=================
HTML integration {#sec-html}
-----------------
### Document ### {#sec-html-document}
Each [=document=] has an <dfn for=document>interaction id to interaction context</dfn>, a [=/map=], initially empty.
Each [=document=] has an <dfn for=document>active soft navigation candidate</dfn>, an [=InteractionContext=] or null, initially null.
### History ### {#sec-html-history}
<div algorithm="additions to history step application">
In [=update document for history step application=], before 5.5.1 (if `documentsEntryChanged` is true and if `documentIsNew` is false),
call [=process same document commit=] with the [=Document=], <var ignore>entry</var>'s [=session history entry url|url=], and "traverse".
</div>
<div algorithm="additions to shared history push/replace steps">
In the [shared history push/replace steps](https://html.spec.whatwg.org/multipage/nav-history-apis.html#shared-history-push/replace-steps) (as defined in [[HTML]]), after the URL is updated,
call [=process same document commit=] with the [=Document=], the new URL, and the operation type (either "push" or "replace").
</div>
### Node ### {#sec-html-node}
Each [=node=] has an <dfn for=node>associated interaction context</dfn>, initially null.
### Hard Navigation ### {#sec-html-hard-nav}
When a new [=global object=] |global| is created (e.g., during a "hard" navigation), its [=global object/current navigation id=] is initialized as specified in [[#sec-nav-id]].
<div algorithm="additions to Navigation Timing">
In [[NAVIGATION-TIMING]], when creating the {{PerformanceNavigationTiming}} entry for the initial navigation, the user agent must set its `navigationId` to the [=global object=]'s [=global object/current navigation id=].
</div>
Event Timing integration {#sec-event-timing-integration}
-----------------
This specification extends the [[EVENT-TIMING]] definition of interactions to include additional event types that are relevant for modern web applications and Single Page Applications.
The following event types are considered to be part of an [=interaction=] (as defined in [[EVENT-TIMING]]):
* `navigate`
* `popstate`
* `hashchange`
When these events are dispatched as a result of a user interaction, the user agent must assign them a unique [=interaction id=] obtained by running the steps to [=get the next interactionId=] for the document's [=relevant global object=]. If an event is triggered by a previous interaction that already has an assigned [=interaction id=], the user agent should reuse that same identifier.
<div class="note">
These events are only reported as primary interactions when they are the initiating event from a user action (e.g., clicking the browser UI's back button). In most other scenarios—such as a `click` handler manually manipulating history—the subsequent `popstate` or `navigate` events are not considered independent user interactions.
While these programmatically triggered events might not have the `isTrusted` flag set, they are correctly attributed back to the original interaction via [[ASYNC-CONTEXT]], as the history and navigation APIs are treated as asynchronous continuations of the initiating task.
</div>
<div class="note">
Note: The events `navigate`, `popstate`, and `hashchange` are used as internal signals for tracking soft navigations and assigning `interactionId`. This specification does not require these event types to be exposed as `PerformanceEventTiming` entries to the performance timeline, leaving that determination to the [[EVENT-TIMING]] specification.
</div>
<div class="note">
The [[EVENT-TIMING]] specification is expected to be updated to explicitly define `processingStart` and `processingEnd` hooks, and to ensure `interactionId` assignment happens early enough for this integration. Today, `interactionId` assignment is typically deferred until the end of the event processing.
<div class="note">
**TODO/Bugfix**: This specification uses `unsigned long long` for **interactionId** to ensure a safe global counter, while [[EVENT-TIMING]] currently defines it as `unsigned long`. This mismatch is expected to be resolved in future versions of both specifications.
</div>
</div>
<div algorithm="getting the next interactionId">
To <dfn>get the next interactionId</dfn> for a {{Window}} |window|:
1. Set |window|'s [=interaction count=] to |window|'s [=interaction count=] plus 1.
1. Return |window|'s [=initial interactionId value=] plus (|window|'s [=interaction count=] times |window|'s [=interactionId increment=]).
</div>
<div algorithm="additions to Event Timing processingStart">
At **processingStart** (as defined in [[EVENT-TIMING]]), given a [=Document=] |document| and an [=Event=] |event|, add the following steps:
1. Call [=update interaction contexts for event=] with |document| and |event|.
1. Let |interaction id| be the |event|'s [=interaction id=].
1. Call [=interaction event processing start=] with |document|, |interaction id|, and |event|.
</div>
<div algorithm="additions to Event Timing processingEnd">
At **processingEnd** (as defined in [[EVENT-TIMING]]), given a [=Document=] <var ignore>document</var> and an [=InteractionContext=] |interaction context|, add the following step:
1. Call [=interaction event timing processing end=] with |interaction context|.
</div>
Largest Contentful Paint (LCP) integration {#sec-lcp-integration}
-----------------
This specification hooks into the [[LARGEST-CONTENTFUL-PAINT]] algorithm to attribute paints to interactions.
<div algorithm="additions to report largest contentful paint">
In [=report largest contentful paint=]:
1. Let |contextToNewLargestCandidate| be a new [=/map=].
1. For each |record| of <var ignore>paintedImages</var>:
1. Let |element| be |record|'s [=pending image record element=].
1. Let |interaction context| be |element|'s [=node/associated interaction context=].
1. If |interaction context| is null, continue.
1. Let |size| be the [=effective visual size=] of |element|.
1. If |size| is null, continue.
1. If |interaction context|'s [=InteractionContext/largest contentful paint=] is not null and |size| is less than or equal to |interaction context|'s [=InteractionContext/largest contentful paint=]'s {{InteractionContentfulPaint/size}}, continue.
1. If |contextToNewLargestCandidate|[|interaction context|] is unset or |size| is greater than |contextToNewLargestCandidate|[|interaction context|]'s {{InteractionContentfulPaint/size}}:
1. [=map/Set=] |contextToNewLargestCandidate|[|interaction context|] to |element|.
1. For each |textNode| of <var ignore>paintedTextNodes</var>:
1. Let |interaction context| be |textNode|'s [=node/associated interaction context=].
1. If |interaction context| is null, continue.
1. Let |size| be the [=effective visual size=] of |textNode|.
1. If |size| is null, continue.
1. If |interaction context|'s [=InteractionContext/largest contentful paint=] is not null and |size| is less than or equal to |interaction context|'s [=InteractionContext/largest contentful paint=]'s {{InteractionContentfulPaint/size}}, continue.
1. If |contextToNewLargestCandidate|[|interaction context|] is unset or |size| is greater than |contextToNewLargestCandidate|[|interaction context|]'s {{InteractionContentfulPaint/size}}:
1. [=map/Set=] |contextToNewLargestCandidate|[|interaction context|] to |textNode|.
1. For each |interaction context| → |newLargestElement| in |contextToNewLargestCandidate|:
1. Call [=emit interaction contentful paint entry=] with |newLargestElement|, |document|, and |interaction context|.
1. Call [=evaluate soft navigation emission=] with |document| and |interaction context|.
</div>
<div class="note">
The [[#sec-container-timing-integration]] section defines how modifications to the DOM reset the **previously reported paints** for affected elements, ensuring they are re-evaluated by the [[LARGEST-CONTENTFUL-PAINT]] algorithm and correctly attributed to the current interaction.
</div>
<div class="note">
The [[LARGEST-CONTENTFUL-PAINT]] algorithm is expected to ignore any paint that has an associated interaction context, as these are handled by this specification's [[#sec-interaction-contentful-paint-interface]].
</div>
Container Timing integration {#sec-container-timing-integration}
-----------------
The [[CONTAINER-TIMING]] API provides a mechanism to group rendering effects by their common DOM ancestor. This specification integrates with that mechanism to attribute rendering changes back to user interactions.
<div algorithm="additions to node insert">
At [=node insert=], or when a [=node=] is modified in one of the following ways:
* The `class` or `style` attribute of an element is modified.
* A resource attribute (such as `src` on an `img` or `video` element) is modified.
Run the following steps:
1. Let |interaction context| be the result of calling [=get current interaction context=].
1. If |interaction context| is not null:
1. Set the node's [=node/associated interaction context=] to |interaction context|.
1. Designate the node as a **container root** (as defined in [[CONTAINER-TIMING]]).
1. For the node and each of its descendants, remove them from the [=Document=]'s [=previously reported paints=] (as defined in [[PAINT-TIMING]]).
</div>
<div class="note">
By removing the modified subtree from the set of **previously reported paints**, the user agent ensures that the next contentful paint for these elements will be re-detected and correctly attributed to the new **InteractionContext**.
</div>
<div class="note">
Note: The reference to "node insert" is a high-level description covering all operations that add new elements to the document structure (e.g., `appendChild`, `innerHTML` updates, etc.). It is up to the user agent to map these to internal implementation hooks that track document structure modifications.
</div>
<div class="note">
The user agent is encouraged to maintain a sparse tree of container roots. If a node is modified that is already a descendant of an existing container root associated with the same interaction context, the user agent might choose not to create a new redundant root.
Furthermore, to avoid expensive main-thread work during large DOM modifications, user agents are encouraged to optimize the removal of descendants from **previously reported paints**. Instead of an immediate exhaustive traversal, the user agent can mark the modified root as "dirty" and lazily propagate this state during subsequent tree walks (e.g., during layout or paint). Subtrees that are known to be non-visible or skipped by existing mechanisms like `content-visibility` or CSS containment could also be skipped during this reset process.
</div>
Performance Timeline integration {#sec-performance-timeline-integration}
-----------------
In [=queue a PerformanceEntry=], after step 1 (initializing the entry), add the following steps:
1. If |newEntry|'s [=PerformanceEntry/navigation id=] is 0:
1. Set |newEntry|'s [=PerformanceEntry/navigation id=] to |global|'s [=global object/current navigation id=].
Overlapping Interactions and Race Conditions {#sec-overlapping-interactions}
=================
<div class="non-normative">
<em>This section is non-normative.</em>
Web applications often process multiple user interactions in rapid succession. This specification handles such overlapping interactions through the following model:
* **Context Independence**: Each interaction manages its own [=InteractionContext=] independently. Multiple contexts can be "in flight" simultaneously, each tracking its own URL modifications and contentful paints.
* **Active Candidate Singleton**: While many interactions can be active, the [=Document=] recognizes only one [=document/active soft navigation candidate=] at any given time.
* **Preemption**: An interaction context becomes the [=document/active soft navigation candidate=] the moment it triggers its first same-document URL modification. If a subsequent interaction modification occurs, it preempts the previous one and becomes the new candidate.
* **Emission Validation**: A {{SoftNavigationEntry}} is only emitted for the interaction context that is the [=document/active soft navigation candidate=] at the moment the emission criteria (URL change + contentful paint) are met. This ensures that soft navigation reporting remains consistent with the document's current visual and navigation state.
* **Persistent Attribution**: Even if an interaction is preempted as a soft navigation candidate, it continues to attribute and report its own {{InteractionContentfulPaint}} entries as long as it remains active. This allows for accurate measurement of concurrent rendering updates that are not themselves considered "navigations."
</div>
Security & privacy considerations {#priv-sec}
===============================================
Exposing Soft Navigations to the performance timeline doesn't have security and privacy implications on its own.
However, resetting 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.