-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathindex.bs
More file actions
980 lines (790 loc) · 49.2 KB
/
Copy pathindex.bs
File metadata and controls
980 lines (790 loc) · 49.2 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
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
<!-- max-line-length: 100 -->
<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, w3cid 122093
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 set of APIs that web page authors can use to detect same-document ("soft") navigations and attribute subsequent page modifications and contentful paints back to the user interactions that triggered them.
Boilerplate: omit conformance
Default Highlight: js
Complain About: accidental-2119 yes
Markup Shorthands: markdown on
Indent: 2
</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;
text: node; url: #concept-node;
text: descendent; url: #concept-tree-descendant
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;
text: largest contentful paint candidate; url: #largest-contentful-paint-candidate;
text: compute a new largest contentful paint candidate; url: #compute-a-new-largest-contentful-paint-candidate;
text: create a LargestContentfulPaint entry; url: #create-a-largestcontentfulpaint-entry;
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;
text: initial interactionId value; url: #window-initial-interactionid-value;
text: interactionId increment; url: #window-interactionid-increment;
text: get the next interactionId; url: #get-the-next-interactionid;
text: initialize and record event timing processing start; url: #initialize-and-record-event-timing-processing-start;
text: record event timing processing end; url: #record-event-timing-processing-end;
text: associated event; url: #performanceeventtiming-associated-event;
text: has dispatched input event; url: #has-dispatched-input-event;
text: interaction; url: #dfn-interaction;
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;
text: default paint timestamp; url: #default-paint-timestamp;
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;
spec:infra; type:dfn; for:/; text:set
</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, mapping modified
DOM nodes to interactions, enabling attribution of contentful paints to interactions that caused
them. 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.
</div>
Performance Timeline Slicing {#sec-timeline-slicing}
====================================================
<div class="non-normative">
<em>This section is non-normative.</em>
To attribute performance metrics (such as event timings, layout shifts, resource loads, or
long animation frames) to their initiating navigation, the performance timeline is sliced into
segments using a {{PerformanceEntry/navigationId}} attribute on {{PerformanceEntry}}.
Determining the exact slice point for a transition that spans time is complex. To ensure
consistent attribution, this specification adopts a model where the timeline is not sliced
immediately upon every URL modification; instead, the transition is deferred until a
[=soft navigation=] is ready to be emitted to the performance timeline.
For a detailed discussion of the design trade-offs, attribution rules, and edge cases, see
the [[PERFORMANCE-TIMELINE]] issue
[comment](https://github.com/w3c/performance-timeline/issues/182#issuecomment-4460611895).
</div>
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 {{Window}} has:
* a <dfn for="Window">current navigation id</dfn>, a [=64-bit unsigned integer=],
initially set to the {{Window}}'s [=Window/initial navigation id value=].
* a <dfn for="Window">navigation count</dfn>, a [=64-bit unsigned integer=], initially 0.
* a <dfn for="Window">initial navigation id value</dfn>, a [=64-bit unsigned integer=],
initially set to a random integer between 100 and 10000.
* a <dfn for="Window">navigation id increment</dfn>, a [=64-bit unsigned integer=],
initially set to a small positive integer chosen by the user agent.
Note: The [=Window/initial navigation id value=] and [=Window/navigation id increment=] are
used to calculate {{PerformanceEntry/navigationId}} values (see
[=increment the current navigation id=]).
This discourages developers from relying on it to count the exact number of navigations or
assuming it starts at zero.
User agents are expected not to use shared global navigation values across different {{Window}}
objects to prevent cross-origin leaks.
<div algorithm="increment the current navigation id">
To <dfn>increment the current navigation id</dfn> given a {{Window}} |window|:
1. Set |window|'s [=Window/navigation count=] to |window|'s
[=Window/navigation count=] plus 1.
1. Let |newId| be |window|'s [=Window/initial navigation id value=] plus
(|window|'s [=Window/navigation count=] times
|window|'s [=Window/navigation id increment=]).
1. Set |window|'s [=Window/current navigation id=] to |newId|.
</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>,
a [=64-bit unsigned integer=], initially 0.
The {{navigationId}} attribute's getter must return [=this=]'s [=PerformanceEntry/navigation id=].
</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. If |global| is a {{Window}} object:
1. Set |newEntry|'s [=PerformanceEntry/navigation id=] to
|global|'s [=Window/current navigation id=].
Interaction Infrastructure {#sec-infrastructure}
=====================
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 [=record a node as modified for interaction paint timing|establish=] a set of modified nodes
associated with the interaction, allowing subsequent rendering effects, like contentful paints, to
be traced back to the initiating interaction context even when no asynchronous script is currently
running.
</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>, a [=64-bit unsigned integer=] respresenting the {{PerformanceEventTiming/interactionId}}
associated with this context.
* <dfn>context document</dfn>, a [=Document=].
* <dfn>start time</dfn>, a number.
* <dfn>navigation type</dfn>, a {{NavigationType}} or null, initially null.
* <dfn>first URL value</dfn>, a string or null, initially null.
* <dfn>first URL update timestamp</dfn>, a number, initially 0.
* <dfn>first scroll timestamp</dfn>, a number, initially 0.
* <dfn>first input timestamp</dfn>, a number, initially 0.
* <dfn>first contentful paint</dfn>, an {{InteractionContentfulPaint}} or null, initially null.
* <dfn>largest contentful paint</dfn>, an {{InteractionContentfulPaint}} or null, initially null.
* <dfn>current largest contentful paint candidate</dfn>, a [=largest contentful paint candidate=]
or null, initially null.
* <dfn>last URL value</dfn>, a string or null, initially null.
* <dfn>emitted</dfn>, a boolean, initially false.
</div>
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
{{PerformanceSoftNavigation}}'s name.
Infrastructure Algorithms {#sec-infra-algos}
-----------------
<div algorithm>
To <dfn>get the current interaction context</dfn> given a [=Document=] |document|:
1. Let |context| be the value of the internal `AsyncContext.Variable`
`[[ActiveInteractionContext]]`.
1. If |context| is not null and |context|'s [=InteractionContext/context document=] is not equal
to |document|, return null.
1. Return |context|.
</div>
<div algorithm>
To <dfn>get or create the context for an interaction</dfn> given a [=Document=] |document| and a
{{PerformanceEventTiming}} |timing entry|:
1. Let |interaction id| be |timing entry|'s {{PerformanceEventTiming/interactionId}}.
1. [=Assert=] |interaction id| is greater than 0.
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/context document=] to |document|.
1. Set |interaction context|'s [=InteractionContext/start time=] to |timing entry|'s
{{PerformanceEntry/startTime}}.
1. [=map/Set=] |document|'s [=interaction id to interaction context=][|interaction id|] to
|interaction context|.
1. Return |interaction context|.
</div>
<div algorithm>
To <dfn>update the interaction contexts for an event</dfn> given a [=Document=] |document| and an
[=Event=] |event|:
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 0:
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 0:
1. Set |interaction context|'s [=InteractionContext/first input timestamp=] to |timestamp|.
</div>
<div algorithm>
To <dfn>set the current interaction context for event dispatch</dfn> given null or a
{{PerformanceEventTiming}} object |timing entry|:
1. If |timing entry| is null, return null.
1. Let |interaction id| be |timing entry|'s {{PerformanceEventTiming/interactionId}}.
1. If |interaction id| is 0, return null.
1. Let |event| be |timing entry|'s [=associated event=].
1. Let |document| be |event|'s [=relevant global object=]'s [=associated Document=].
1. [=Update the interaction contexts for an event=] given |document| and |event|.
1. Let |interaction context| be the result of [=get or create the context for an
interaction|getting or createing the context for an interaction=] given |document| and
|timing entry|.
1. Set the value of the internal `AsyncContext.Variable` `[[ActiveInteractionContext]]` to
|interaction context|.
1. Return |interaction context|.
</div>
<div algorithm>
To <dfn>unset the current interaction context after event dispatch</dfn>:
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 LargestContentfulPaint largestContentfulPaint;
readonly attribute unsigned long long interactionId;
[Default] object toJSON();
};
InteractionContentfulPaint includes PaintTimingMixin;
</pre>
<div dfn-for="InteractionContentfulPaint">
Each {{InteractionContentfulPaint}} has:
* An associated <dfn>context</dfn>, an [=InteractionContext=].
* An associated <dfn>largest contentful paint</dfn>, a {{LargestContentfulPaint}} entry.
* An associated [=paint timing info=].
The {{largestContentfulPaint}} attribute's getter must return [=this=]'s
[=InteractionContentfulPaint/largest contentful paint=].
The {{InteractionContentfulPaint/interactionId}} attribute's getter must return [=this=]'s
[=InteractionContentfulPaint/context=]'s [=InteractionContext/id=].
The {{PerformanceEntry/name}} attribute's getter must return the empty string.
The {{PerformanceEntry/entryType}} attribute's getter must return `"interaction-contentful-paint"`.
The {{PerformanceEntry/startTime}} attribute's getter must return [=this=]'s
[=InteractionContentfulPaint/context=]'s [=InteractionContext/start time=].
The {{PerformanceEntry/duration}} attribute's getter must return the difference between the
[=default paint timestamp=] for [=this=]'s [=paint timing info=] and [=this=]'s
[=InteractionContentfulPaint/context=]'s [=InteractionContext/start time=].
When {{InteractionContentfulPaint/toJSON()}} is called, run [=default toJSON steps=].
Note: While the entry dynamically queries static interaction properties (like `interactionId` and
`startTime`) from its associated [=InteractionContext=], it captures a static snapshot of the
paint's [=paint timing info=] and the candidate {{LargestContentfulPaint}} entry at creation time to
ensure these metrics reflect the exact state of that visual update.
</div>
Interaction Contentful Paint Algorithms {#sec-interaction-contentful-paint-algos}
-----------------
<div algorithm>
To <dfn>create an interaction contentful paint entry</dfn>, given a {{Window}} |window|, an
[=InteractionContext=] |interaction context|, a {{LargestContentfulPaint}} |lcpEntry|, and a
[=paint timing info=] |paintTimingInfo|:
1. Let |entry| be a new {{InteractionContentfulPaint}} object in |window|'s
[=global object/realm=].
1. Set |entry|'s [=InteractionContentfulPaint/context=] to |interaction context|.
1. Set |entry|'s [=InteractionContentfulPaint/largest contentful paint=] to |lcpEntry|.
1. Set |entry|'s associated [=paint timing info=] to |paintTimingInfo|.
1. Return |entry|.
</div>
<div algorithm>
To <dfn>process a new attributed contentful paint candidate</dfn> given a
[=largest contentful paint candidate=] |lcpCandidate|, a [=Document=] |document|, an
[=InteractionContext=] |interaction context|, and a [=paint timing info=] |paintTimingInfo|:
1. If |interaction context|'s [=InteractionContext/first scroll timestamp=] is greater than 0 and
|lcpEntry|'s {{LargestContentfulPaint/renderTime}} is greater than |interaction context|'s
[=InteractionContext/first scroll timestamp=], return.
1. If |interaction context|'s [=InteractionContext/first input timestamp=] is greater than 0 and
|lcpEntry|'s {{LargestContentfulPaint/renderTime}} is greater than |interaction context|'s
[=InteractionContext/first input timestamp=], return.
1. Let |lcpEntry| be the result of [=creating a LargestContentfulPaint entry=] with
|lcpCandidate|, |paintTimingInfo|, and |document|.
1. Let |global| be |document|'s [=relevant global object=].
1. Let |paint timing info| be |lcpEntry|'s associated [=paint timing info=].
1. Let |icpEntry| be the result of [=creating an interaction contentful paint entry=] given
|global|, |interaction context|, |lcpEntry|, and |paint timing info|.
1. [=queue a performanceentry|Queue=] |icpEntry|.
1. Add |icpEntry| 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 |icpEntry|.
1. Set |interaction context|'s [=InteractionContext/largest contentful paint=] to |icpEntry|.
1. Set |interaction context|'s [=InteractionContext/current largest contentful paint candidate=]
to |lcpCandidate|.
</div>
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 algorithm>
To <dfn>report interaction contentful paints and soft navigations</dfn> given a {{Document}}
|document|, a [=paint timing info=] |paintTimingInfo|, an [=ordered set=] of
[=pending image records=] |paintedImages|, and an [=ordered set=] of [=/elements=]
|paintedTextNodes|:
1. Let |contextToCandidateSets| be a new [=/map=].
1. [=set/For each=] |record| of |paintedImages|:
1. Let |element| be |record|'s [=pending image record element|element=].
1. Let |interaction context| be the result of [=getting the paint attribution interaction context=]
for |element|.
1. If |interaction context| is not null:
1. If |contextToCandidateSets| does not [=map/contain=] |interaction context|, then set
|contextToCandidateSets|[|interaction context|] to « «», «» ».
1. [=set/Append=] |record| to |contextToCandidateSets|[|interaction context|][0].
1. [=set/For each=] |element| of |paintedTextNodes|:
1. Let |interaction context| be the result of [=getting the paint attribution interaction context=]
for |element|.
1. If |interaction context| is not null:
1. If |contextToCandidateSets| does not [=map/contain=] |interaction context|, then set
|contextToCandidateSets|[|interaction context|] to « «», «» ».
1. [=set/Append=] |element| to |contextToCandidateSets|[|interaction context|][1].
1. [=map/For each=] |interaction context| → |candidateSets| in |contextToCandidateSets|:
1. Let |newCandidate| be the result of [=computing a new largest contentful paint candidate=]
given |document|, |candidateSets|[0], |candidateSets|[1], and |interaction context|'s
[=InteractionContext/current largest contentful paint candidate=].
1. If |newCandidate| is not null:
1. [=Process a new attributed contentful paint candidate=] given |newCandidate|, |document|,
and |interaction context|, and |paintTimingInfo|.
1. [=Evaluate soft navigation emission=] given |document| and |interaction context|.
</div>
Interaction Contentful Paint Attribution {#sec-interaction-contentful-paint-attribution}
-----------------
<div dfn-for="InteractionPaintTimingNodeState">
An <dfn>InteractionPaintTimingNodeState</dfn> is a [=struct=] with the following [=struct/items=]:
* <dfn>context</dfn>, an [=InteractionContext=], initially null.
* <dfn>modification id</dfn>, a [=64-bit unsigned integer=], initially 0.
</div>
<br>
<div algorithm>
To determine if an [=InteractionPaintTimingNodeState=] |node state| is <dfn>more recent than</dfn>
an [=InteractionPaintTimingNodeState=] or null |other node state|:
1. Return true if |other node state| is null or |node state|'s
[=InteractionPaintTimingNodeState/modification id=] is greater than |other node state|'s
[=InteractionPaintTimingNodeState/modification id=], and false otherwise.
</div>
<div algorithm>
To <dfn>get the paint attribution interaction context</dfn> for a given [=Node=] |node|:
1. Let |document| be |node|'s [=Node/node document=].
1. Let |node state| be |document|'s [=document/propagated node state=][|node|]
[=map/with default=] null.
1. If |node state| is null return null, otherwise return |node state|'s
[=InteractionPaintTimingNodeState/context=].
</div>
<div algorithm>
To <dfn>record a node as modified for interaction paint timing</dfn> given a [=Node=]
|target node|:
1. Let |document| be |node|'s [=Node/node document=].
1. If |target node| is not [=exposed for paint timing=] given |document|, return.
1. Let |interaction context| be the result of [=getting the current interaction context=] given
|document|.
1. If |interaction context| is null, return.
1. If |document|'s [=document/last modification context=] is not equal to |interaction context|:
1. Set |document|'s [=document/last modification context=] to |interaction context|.
1. Increment |document|'s [=document/current modification generation id=] by 1.
1. Let |previous node state| be |document|'s [=document/marked node state=][|target node|]
[=map/with default=] null.
1. If |previous node state| is not null and |previous node state|'s
[=InteractionPaintTimingNodeState/modification id=] equals |document|'s
[=document/current modification generation id=], return.
1. Let |node state| to a new [=InteractionPaintTimingNodeState=].
1. Set |node state|'s [=InteractionPaintTimingNodeState/context=] to |interaction context|.
1. Set |node state|'s [=InteractionPaintTimingNodeState/modification id=] to
|document|'s [=document/current modification generation id=].
1. [=map/Set=] |document|'s [=document/marked node state=][|node|] to |node state|.
1. Set |document|'s [=document/is interaction paint timing dirty=] to true.
</div>
<div algorithm>
To <dfn>update the interaction context for a document and its descendants</dfn> given a
[=Document=] |document|:
1. If |document|'s [=document/is interaction paint timing dirty=] is false, return.
1. [=Update the interaction context for a node and its descendants=] given |document| and null.
1. Set |document|'s [=document/is interaction paint timing dirty=] to false.
</div>
<div algorithm>
To <dfn>update the interaction context for a node and its descendants</dfn> given a [=Node=]
|node| and an [=InteractionPaintTimingNodeState=] |inherited node state| or null:
1. Let |document| be |node|'s [=Node/node document=].
1. Let |node state| be |document|'s [=document/marked node state=][|node|] [=map/with default=] null.
1. If |node state| is not null:
1. If |node state| is [=more recent than=] |inherited node state|:
1. Set |inherited node state| to |node state|.
1. If |node| is a {{Text}} [=node=], then [=map/remove=] |document|'s
[=document/marked node state=][|node|].
1. Otherwise, [=map/remove=] |document|'s [=document/marked node state=][|node|].
1. If |inherited node state| is not null and |node| is [=timing-eligible=]:
1. Let |attribution element| be the result of [=getting the attribution element=] for |node|.
1. Let |previous propagated state| be [=document/propagated node state=][|attribution element|]
[=map/with default=] null.
1. If |inherited node state| is [=more recent than=] |previous propagated state|:
1. Set [=document/propagated node state=][|attribution element|] to |inherited node state|.
1. If |previous propagated state| is null or |previous propagated state|'s
[=InteractionPaintTimingNodeState/context=] is not equal to |inherited node state|'s
[=InteractionPaintTimingNodeState/context=], then [=reset paint tracking=] for
|attribution element|.
1. [=set/For each=] [=tree/child=] |child node| of |node|,
[=update the interaction context for a node and its descendants=] given |child node| and
|inherited node state|.
</div>
Note: For simplicity, this algorithm walks the entire DOM whenever the propagated state needs to to
be reprocessed. This can be optimized to only process parts of the DOM that are paintable, e.g.
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 process, as long as they
are updated before the next time they are painted. Also note that this can be further optimized by
merging these steps with another (pre-paint) tree walk, as long as the relevant state is updated
before paint.
Note: The [[CONTAINER-TIMING]] API provides a mechanism for grouping rendering effects by their
common DOM ancestor and report aggregated paint information to the performance timeline. In a future
version of this specification, we plan to expand Container Timing to support multiple clients for a
single container (i.e. the global "containertiming" attribute and per-interaction timing) to allow
implementing interaction paint timing in terms of Container Timing. Each [=record a node as modified
for interaction paint timing|recorded=] node would be considered a container for the interaction
that modified it, so that each interaction is associated with a set of containers. Then, this
specification would track the largest contentful paint that occurs in a marked container, for each
interaction. Furthermore, we could expose other useful information that Container Timing tracks,
e.g. the aggregated total painted area of containers for each interaction.
Soft Navigations {#sec-soft-navs}
=====================
The `PerformanceSoftNavigation` interface {#sec-interface}
-----------------
<pre class=idl>
[Exposed=Window]
interface PerformanceSoftNavigation : PerformanceEntry {
readonly attribute NavigationType navigationType;
readonly attribute unsigned long long interactionId;
InteractionContentfulPaint? getLargestInteractionContentfulPaint();
[Default] object toJSON();
};
PerformanceSoftNavigation includes PaintTimingMixin;
</pre>
<div dfn-for="PerformanceSoftNavigation">
Each {{PerformanceSoftNavigation}} has:
* An associated <dfn>context</dfn>, an [=InteractionContext=].
* An associated [=paint timing info=].
The {{navigationType}} attribute's getter must return [=this=]'s
[=PerformanceSoftNavigation/context=]'s [=InteractionContext/navigation type=].
The {{PerformanceSoftNavigation/interactionId}} attribute's getter must return [=this=]'s
[=PerformanceSoftNavigation/context=]'s [=InteractionContext/id=].
The {{getLargestInteractionContentfulPaint()}} method must return [=this=]'s
[=PerformanceSoftNavigation/context=]'s [=InteractionContext/largest contentful paint=].
The {{PerformanceEntry/name}} attribute's getter must return [=this=]'s
[=PerformanceSoftNavigation/context=]'s [=InteractionContext/first URL value=].
The {{PerformanceEntry/entryType}} attribute's getter must return `"soft-navigation"`.
The {{PerformanceEntry/startTime}} attribute's getter must return [=this=]'s
[=PerformanceSoftNavigation/context=]'s [=InteractionContext/start time=].
The {{PerformanceEntry/duration}} attribute's getter must return the difference between [=this=]'s
{{PaintTimingMixin/presentationTime}} and [=this=]'s [=PerformanceSoftNavigation/context=]'s
[=InteractionContext/start time=] at the time of emission.
When {{PerformanceSoftNavigation/toJSON()}} is called, run [=default toJSON steps=].
</div>
Note: In practice, {{getLargestInteractionContentfulPaint()}} is expected to return a non-null
entry, as a confirmed soft navigation requires at least one detected interaction contentful paint to
trigger its emission.
<br><br>
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 method might
return null at the time of emission.
<br><br>
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.
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>
To <dfn>evaluate soft navigation emission</dfn> given a [=Document=] |document| and an
[=InteractionContext=] |interaction context|:
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 null, return.
1. If |interaction context|'s [=InteractionContext/first contentful paint=] is null, return.
1. Let |window| be |document|'s [=relevant global object=].
1. Let |entry| be the result of [=creating a soft navigation entry=] given |window| and
|interaction context|.
1. [=Emit a soft navigation entry=] given |window| and |entry|.
1. Set |interaction context|'s [=InteractionContext/emitted=] to true.
</div>
<div algorithm>
To <dfn>create a soft navigation entry</dfn> given a {{Window}} |window|, and an
[=InteractionContext=] |interaction context|:
1. Let |entry| be a new {{PerformanceSoftNavigation}} object in |window|'s
[=global object/realm=].
1. Set |entry|'s [=PerformanceSoftNavigation/context=] to |interaction context|.
1. Let |first paint| be |interaction context|'s [=InteractionContext/first contentful paint=].
1. [=Assert=] |first paint| is not null.
1. Set |entry|'s associated [=paint timing info=] to |first paint|'s associated [=paint timing info=].
1. Return |entry|.
</div>
<div algorithm>
To <dfn>emit a soft navigation entry</dfn> given a {{Window}} |window| and a
{{PerformanceSoftNavigation}} |entry|:
1. [=Increment the current navigation id=] given |window|.
1. [=queue a performanceentry|Queue=] |entry|.
1. Add |entry| to |window|'s [=performance entry buffer=].
</div>
Note: The `navigationId` for this {{PerformanceSoftNavigation}} entry is set automatically as
part of queuing it to the performance timeline, matching the behavior of other
{{PerformanceEntry}} types.
<div algorithm>
To <dfn>process a same document commit</dfn> given a [=Document=] |document|, [=string=] |url|,
and {{NavigationType}} |navigation type|:
1. If |url| is equal to |document|'s [=Document/url=], return.
1. Let |interaction context| be the result of [=getting the current interaction context=] given
|document|.
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 null:
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. [=Evaluate soft navigation emission=] given |document| and |interaction context|.
</div>
Specification Integrations {#sec-integrations}
=================
DOM integration {#sec-dom-integration}
-----------------
### 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.
Each [=document=] has a <dfn for=document>current modification generation id</dfn>, a
[=64-bit unsigned integer=] initilized to 0.
Note: The [=document/current modification generation id=] changes when a relevant DOM modification
occurs with a different [=InteractionContext=] from the previous modification. This allows grouping
together related modifications, which enables comparing [=InteractionPaintTimingNodeState=] objects
in terms of recency and optimizing which nodes need to be tracked.
Each [=document=] has a <dfn for=document>last modification context</dfn>, an [=InteractionContext=]
or null, initially null.
Each [=document=] has a <dfn for=document>marked node state</dfn>, a [=map=] of [=Node=] to
[=InteractionPaintTimingNodeState=], initially empty.
Each [=document=] has a <dfn for=document>propagated node state</dfn>, a [=map=] of [=Node=] to
[=InteractionPaintTimingNodeState=], initially empty.
Each [=document=] has an <dfn for=document>is interaction paint timing dirty</dfn>, a boolean,
initially false.
### Node ### {#sec-html-node}
<div algorithm="additions to node insert">
At [=node insert=], or when a [=node=] |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. [=Record a node as modified for interaction paint timing=] given |node|.
</div>
HTML integration {#sec-html}
-----------------
### 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), [=process a same document commit=] given the [=Document=],
the target entry's [=session history entry url|url=], and "{{NavigationType/traverse}}".
</div>
<br>
<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, [=process a same document commit=] given the
[=Document=], the new URL, and the operation type (either "{{NavigationType/push}}" or
"{{NavigationType/replace}}").
</div>
### Hard Navigation ### {#sec-html-hard-nav}
When a new {{Window}} is created (e.g., during a "hard" navigation), its
[=Window/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 {{Window}}'s
[=Window/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 {{PerformanceEventTiming/interactionId}} obtained by [=getting the next interactionId=]
for the document's [=relevant global object=]. If an event is triggered by a previous interaction
that already has an assigned {{PerformanceEventTiming/interactionId}}, the user agent should reuse
that same identifier.
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.
<br><br>
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.
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.
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.
Issue: 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 algorithm>
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 DOM event dispatch">
In the modifications to the DOM specification's [=event dispatch=] algorithm defined in [[EVENT-TIMING]]:
1. Right after the step that sets |timingEntry| to the result of [=initialize and record event
timing processing start=], run the following step:
1. [=Set the current interaction context for event dispatch=] given |timingEntry|.
1. Right before the step that calls [=record event timing processing end=], run the following step:
1. [=Unset the current interaction context after event dispatch=].
</div>
Paint Timing integration {#sec-paint-timing-integration}
-----------------
This specification extends the [[PAINT-TIMING]] specification to reset paint tracking for elements
that have been modified by interactions and to trigger paint attribution for rendered documents.
<div algorithm="mark paint timing changes 1">
Change [=mark paint timing=] to add the following step after step 1, given a {{Document}}
|document|:
1. [=Update the interaction context for a document and its descendants=] given |document|.
</div>
Issue(w3c/paint-timing#126): [=Mark paint time=] mixes together steps that should come before and
after the UA paints the document. The step added here is expected to occur before paint.
<div algorithm="mark paint timing changes 2">
Change [=mark paint timing=] to add the following step after step 10.3 ([=report largest
contentful paint=]), given a {{Document}} |document|, a [=paint timing info=] |paintTimingInfo|,
an [=ordered set=] of [=pending image records=] |paintedImages|, and an [=ordered set=] of
[=/elements=] |paintedTextNodes|:
1. [=Report interaction contentful paints and soft navigations=] given |document|,
|paintTimingInfo|, |paintedImages|, and |paintedTextNodes|.
</div>
Add the following algorithm:
<div algorithm>
To <dfn>reset paint tracking</dfn> for a [=Node=] |node|:
1. Reset the text element tracking |node|.
1. Reset the image element tracking |node|.
</div>
Issue: This is a placeholder algorithm. We need to allow repaints for relevant elements, but the
repaints should not affect Element Timing or LCP. Text and images have different mechanisms for
detecting when a relevant paint has occurred, and both will need to be updated to support repaints.
Add the following algorithm:
<div algorithm>
To <dfn>get the attribution element</dfn> for a [=Node=] |node|:
1. If |node| is a {{Text}} [=node=], then return the {{Element}} that determines the
[=containing block=] of |node|.
1. Otherwise, return [=node=].
</div>
Largest Contentful Paint (LCP) integration {#sec-lcp-integration}
-----------------
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/explainers-by-googlers/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.
Similarly, exposing detailed paint timing information (such as through Interaction Contentful Paint)
could potentially be used to observe paint updates when spelling or grammar error decorations (via
`::spelling-error` or `::grammar-error`) are applied. In the absence of mitigations, this could
allow an attacker to programmatically cycle through words in a text field to check if they are
present in the user's dictionary. However, browsers mitigate this by limiting spelling and grammar
checks to only occur once per user interaction (e.g. key press or click) to [prevent user dictionary
leaks](https://explainers-by-googlers.github.io/user-dictionary-leaks/). Since timing and paint
information is already observable by the developer through other standard APIs, exposing these timing
entries does not introduce any new information leaks beyond what is already constrained by the
browser's spelling/grammar highlight rate limits.
Appendix: 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 {{PerformanceSoftNavigation}} 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>