Soft Navigations and Interaction Contentful Paint

Draft Community Group Report,

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

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.

Status of this document

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

1. Introduction

This section is non-normative.

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.

2. Interaction Infrastructure

2.1. Navigation ID

A navigation id is a unique identifier assigned to each navigation (both hard and soft) within a global object’s lifetime.

Each global object has a current navigation id, 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.

2.2. Interaction Context Intro

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.

2.3. The InteractionContext struct

InteractionContext is a struct used to maintain the data required to detect a soft navigation from a single interaction. It has the following items:
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.
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.

2.4. Infrastructure Algorithms

To get current interaction context, run the following steps:
  1. Let context be the value of the internal AsyncContext.Variable [[ActiveInteractionContext]].

  2. Return context.

To get or create context for interaction, 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] exists, return document’s interaction id to interaction context[interaction id].

  2. Let interaction context be a new InteractionContext.

  3. Set interaction context’s id to interaction id.

  4. Set interaction context’s start time to event’s startTime.

  5. Set document’s interaction id to interaction context[interaction id] to interaction context.

  6. Return interaction context.

To update interaction contexts for event, 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.

  2. Let is scroll be true if event’s type is "scroll", and false otherwise.

  3. 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.

  4. If is scroll is false and is input is false, return.

  5. For each interaction context of document’s interaction id to interaction context’s values:

    1. If is scroll is true and interaction context’s first scroll timestamp is unset:

      1. Set interaction context’s first scroll timestamp to timestamp.

    2. If is input is true and interaction context’s first input timestamp is unset:

      1. Set interaction context’s first input timestamp to timestamp.

To interaction event processing start, given a Document document, an interaction id, and an Event event, run the following steps:
  1. If interaction id is null, return null.

  2. Let interaction context be the result of calling get or create context for interaction with document, interaction id, and event.

  3. Set the value of the internal AsyncContext.Variable [[ActiveInteractionContext]] to interaction context.

  4. Return interaction context.

To interaction event timing processing end, given null or InteractionContext interaction context, run the following steps:
  1. If interaction context is null, return.

  2. Set the value of the internal AsyncContext.Variable [[ActiveInteractionContext]] to null.

3. Interaction Contentful Paints

3.1. The InteractionContentfulPaint interface

[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;

Each InteractionContentfulPaint has an associated paint timing info.

Today this is modelled after the Largest Contentful Paint (LCP) performance entry, but a future direction is to model after Container Timing performance entry.
The renderTime attribute’s getter must return this’s associated paint timing info’s rendering update end time.

The loadTime attribute’s getter must return this’s associated paint timing info’s 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 interactionId attribute’s getter must return the interaction id of the interaction that triggered this paint.

3.2. Interaction Contentful Paint Algorithms

To create an interaction contentful paint entry, 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 realm.

  2. Set entry’s entryType to be "interaction-contentful-paint".

  3. Set entry’s element to be element.

  4. Set entry’s interactionId to interaction context’s id.

  5. Set entry’s associated paint timing info to a new paint timing info.

  6. Set entry’s renderTime, loadTime, size, id, and url to the corresponding values of the contentful paint candidate as defined in the report largest contentful paint algorithm in [LARGEST-CONTENTFUL-PAINT].

  7. Set entry’s startTime to interaction context’s start time.

  8. Set entry’s duration to the difference between entry’s renderTime and entry’s startTime.

  9. Return entry.

To emit interaction contentful paint entry, 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.

  2. 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.

  3. If interaction context’s first scroll timestamp is set and paint entry’s renderTime is greater than interaction context’s first scroll timestamp, return.

  4. If interaction context’s first input timestamp is set and paint entry’s renderTime is greater than interaction context’s first input timestamp, return.

  5. Queue paint entry.

  6. Add paint entry to global’s performance entry buffer.

  7. If interaction context’s first contentful paint is null:

    1. Set interaction context’s first contentful paint to paint entry.

  8. Set interaction context’s largest contentful paint to paint entry.

  9. Increment interaction context’s total painted area by paint entry’s size.

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.

4. Soft Navigations

4.1. The SoftNavigationEntry interface

[Exposed=Window]
interface SoftNavigationEntry : PerformanceEntry {
    readonly attribute DOMString navigationType;
    readonly attribute unsigned long long interactionId;
    readonly attribute InteractionContentfulPaint? largestInteractionContentfulPaint;
};

SoftNavigationEntry includes PaintTimingMixin;

Each SoftNavigationEntry has an associated paint timing info.

The navigationType attribute’s getter must return the navigation type of the interaction that triggered the soft navigation.

The interactionId attribute’s getter must return the interaction id of the interaction that triggered the soft navigation.

The 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 name attribute’s getter must return the first URL value of the interaction that triggered the soft navigation.

The startTime attribute’s getter must return the start time of the interaction that triggered the soft navigation.

The duration attribute’s getter must return the difference between this’s presentationTime and this’s startTime at the time of emission.

In practice, 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.

4.2. Soft Navigation Algorithms

A soft navigation is a same-document navigation that satisfies the following conditions:

To evaluate soft navigation emission, given a Document document and an InteractionContext interaction context, run the following steps:
  1. If interaction context’s emitted is true, return.

  2. If document’s active soft navigation candidate is not interaction context, return.

  3. If interaction context’s first URL value is unset, return.

  4. If interaction context’s first contentful paint is null, return.

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

  6. Let url be interaction context’s first URL value.

  7. Let entry be the result of calling create a soft navigation entry with global, interaction context, url, and interaction context’s start time.

  8. Call emit soft navigation entry with global, entry.

  9. Set interaction context’s emitted to true.

To create a soft navigation entry, 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 realm.

  2. Set entry’s name to be url.

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

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

  5. Let first paint be interaction context’s first contentful paint.

  6. If first paint is not null:

    1. Set entry’s associated paint timing info to first paint’s associated paint timing info.

  7. Set entry’s interactionId to interaction context’s id (if available).

  8. Set entry’s largestInteractionContentfulPaint to interaction context’s largest contentful paint.

  9. Return entry.

Note: navigationId is set further down, in queue a PerformanceEntry.
To emit soft navigation entry, with a global object global, and a SoftNavigationEntry entry, run the following steps:
  1. Set entry’s navigation id to entry’s interactionId.

  2. Set global’s current navigation id to entry’s interactionId.

  3. Queue entry.

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

To process same document commit, 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.

  2. If interaction context is null, return.

  3. Set interaction context’s last URL value to url.

  4. If interaction context’s first URL update timestamp is unset:

    1. Set interaction context’s first URL update timestamp to the current high resolution time given document’s relevant global object.

    2. Set interaction context’s first URL value to url.

    3. Set interaction context’s navigation type to navigation type.

  5. Set document’s active soft navigation candidate to interaction context.

  6. Call evaluate soft navigation emission with document and interaction context.

5. The PerformanceEntry extension

[Exposed=(Window,Worker)]
partial interface PerformanceEntry {
    readonly attribute unsigned long long navigationId;
};
Each PerformanceEntry has an associated navigation id, an unsigned long long, initially 0.

The navigationId attribute’s getter must return this’s navigation id.

6. Specification Integrations

6.1. HTML integration

6.1.1. Document

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

Each document has an active soft navigation candidate, an InteractionContext or null, initially null.

6.1.2. History

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, entry’s url, and "traverse".
In the 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").

6.1.3. Node

Each node has an associated interaction context, initially null.

6.1.4. Hard Navigation

When a new global object global is created (e.g., during a "hard" navigation), its current navigation id is initialized as specified in § 2.1 Navigation ID.

In [NAVIGATION-TIMING], when creating the PerformanceNavigationTiming entry for the initial navigation, the user agent must set its navigationId to the global object’s current navigation id.

6.2. 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]):

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.

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.

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.
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.
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.
To get the next interactionId for a Window window:
  1. Set window’s interaction count to window’s interaction count plus 1.

  2. Return window’s initial interactionId value plus (window’s interaction count times window’s interactionId increment).

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.

  2. Let interaction id be the event’s interaction id.

  3. Call interaction event processing start with document, interaction id, and event.

At processingEnd (as defined in [EVENT-TIMING]), given a Document document and an InteractionContext interaction context, add the following step:
  1. Call interaction event timing processing end with interaction context.

6.3. Largest Contentful Paint (LCP) integration

This specification hooks into the [LARGEST-CONTENTFUL-PAINT] algorithm to attribute paints to interactions.

In report largest contentful paint:
  1. Let contextToNewLargestCandidate be a new map.

  2. For each record of paintedImages:

    1. Let element be record’s pending image record element.

    2. Let interaction context be element’s associated interaction context.

    3. If interaction context is null, continue.

    4. Let size be the effective visual size of element.

    5. If size is null, continue.

    6. If interaction context’s largest contentful paint is not null and size is less than or equal to interaction context’s largest contentful paint’s size, continue.

    7. If contextToNewLargestCandidate[interaction context] is unset or size is greater than contextToNewLargestCandidate[interaction context]'s size:

      1. Set contextToNewLargestCandidate[interaction context] to element.

  3. For each textNode of paintedTextNodes:

    1. Let interaction context be textNode’s associated interaction context.

    2. If interaction context is null, continue.

    3. Let size be the effective visual size of textNode.

    4. If size is null, continue.

    5. If interaction context’s largest contentful paint is not null and size is less than or equal to interaction context’s largest contentful paint’s size, continue.

    6. If contextToNewLargestCandidate[interaction context] is unset or size is greater than contextToNewLargestCandidate[interaction context]'s size:

      1. Set contextToNewLargestCandidate[interaction context] to textNode.

  4. For each interaction contextnewLargestElement in contextToNewLargestCandidate:

    1. Call emit interaction contentful paint entry with newLargestElement, document, and interaction context.

    2. Call evaluate soft navigation emission with document and interaction context.

The § 6.4 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.
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 § 3.1 The InteractionContentfulPaint interface.

6.4. 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.

At node insert, or when a node is modified in one of the following ways:

Run the following steps:

  1. Let interaction context be the result of calling get current interaction context.

  2. If interaction context is not null:

    1. Set the node’s associated interaction context to interaction context.

    2. Designate the node as a container root (as defined in [CONTAINER-TIMING]).

    3. For the node and each of its descendants, remove them from the Document’s previously reported paints (as defined in [PAINT-TIMING]).

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.
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.
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.

6.5. Performance Timeline integration

In queue a PerformanceEntry, after step 1 (initializing the entry), add the following steps:

  1. If newEntry’s navigation id is 0:

    1. Set newEntry’s navigation id to global’s current navigation id.

7. Overlapping Interactions and Race Conditions

This section is non-normative.

Web applications often process multiple user interactions in rapid succession. This specification handles such overlapping interactions through the following model:

8. Security & privacy considerations

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. 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.

Index

Terms defined by this specification

Terms defined by reference

References

Normative References

[DOM]
Anne van Kesteren. DOM Standard. Living Standard. URL: https://dom.spec.whatwg.org/
[EVENT-TIMING]
Michal Mocny. Event Timing API. URL: https://w3c.github.io/event-timing/
[HR-TIME-3]
Yoav Weiss. High Resolution Time. URL: https://w3c.github.io/hr-time/
[HTML]
Anne van Kesteren; et al. HTML Standard. Living Standard. URL: https://html.spec.whatwg.org/multipage/
[INFRA]
Anne van Kesteren; Domenic Denicola. Infra Standard. Living Standard. URL: https://infra.spec.whatwg.org/
[LARGEST-CONTENTFUL-PAINT]
Yoav Weiss. Largest Contentful Paint. URL: https://w3c.github.io/largest-contentful-paint/
[NAVIGATION-TIMING-2]
Yoav Weiss; Noam Rosenthal. Navigation Timing Level 2. URL: https://w3c.github.io/navigation-timing/
[PERFORMANCE-TIMELINE]
Nicolas Pena Moreno. Performance Timeline. URL: https://w3c.github.io/performance-timeline/
[WEBIDL]
Edgar Chen; Timothy Gu. Web IDL Standard. Living Standard. URL: https://webidl.spec.whatwg.org/

Informative References

[ASYNC-CONTEXT]
AsyncContext. URL: https://github.com/tc39/proposal-async-context
[CONTAINER-TIMING]
Container Timing API. URL: https://github.com/WICG/container-timing
[NAVIGATION-TIMING]
Zhiheng Wang. Navigation Timing. 17 December 2012. REC. URL: https://www.w3.org/TR/navigation-timing/
[PAINT-TIMING]
Ian Clelland; Noam Rosenthal. Paint Timing. URL: https://w3c.github.io/paint-timing/

IDL Index

[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;

[Exposed=Window]
interface SoftNavigationEntry : PerformanceEntry {
    readonly attribute DOMString navigationType;
    readonly attribute unsigned long long interactionId;
    readonly attribute InteractionContentfulPaint? largestInteractionContentfulPaint;
};

SoftNavigationEntry includes PaintTimingMixin;

[Exposed=(Window,Worker)]
partial interface PerformanceEntry {
    readonly attribute unsigned long long navigationId;
};