<pre class=metadata>
Title: Layout Instability API
Status: CG-Draft
Shortname: layout-instability
Group: WICG
Level: 1
Editor: Steve Kobes, Google, https://google.com, skobes@chromium.org
        Nicolás Peña Moreno, Google, https://google.com, npm@chromium.org
        Emily Hanley, Google, https://google.com, eyaich@chromium.org
URL: https://wicg.github.io/layout-instability
Repository: https://github.com/WICG/layout-instability
Abstract: This document defines an API that provides web page authors with insights into the stability of their pages based on movements of the elements on the page.
Default Highlight: js
Complain About: accidental-2119 yes
</pre>

<pre class=anchors>
urlPrefix: https://dom.spec.whatwg.org/; spec: DOM;
    type: dfn; url: #interface-element; text: element
    type: dfn; url: #concept-shadow-including-descendant; text: shadow-including descendants;
urlPrefix: https://www.w3.org/TR/css-writing-modes-4/; spec: CSS-WRITING-MODES-4;
    type: dfn; url: #flow-relative; text: flow-relative offset;
urlPrefix: https://w3c.github.io/performance-timeline/; spec: PERFORMANCE-TIMELINE-2;
    type: interface; url: #the-performanceentry-interface; text: PerformanceEntry;
    type: attribute; for: PerformanceEntry;
        text: name; url: #dom-performanceentry-name;
        text: entryType; url: #dom-performanceentry-entrytype;
        text: startTime; url: #dom-performanceentry-starttime;
        text: duration; url: #dom-performanceentry-duration;
    type: dfn; url: #dfn-register-a-performance-entry-type; text: register a performance entry type;
    type: dfn; url: #dfn-queue-a-performanceentry; text: Queue the PerformanceEntry;
    type: dfn; url: #getentriesbytype-method-0; text: getEntriesByType;
    type: dfn; url: #dom-performanceobserver; text: PerformanceObserver;
    type: attribute; for: PerformanceObserver;
        text: supportedEntryTypes; url: #supportedentrytypes-attribute;
    type: dfn; url: #dom-performanceobserverinit-buffered; text: buffered;
urlPrefix: https://w3c.github.io/resource-timing/; spec: RESOURCE-TIMING;
    type: dfn; url: #sec-privacy-security; text: statistical fingerprinting;
urlPrefix: https://w3c.github.io/hr-time/; spec: HR-TIME-2;
    type: typedef; url: #idl-def-domhighrestimestamp; text: DOMHighResTimeStamp;
    type: interface; url: #dfn-performance; text: Performance;
    type: method; for:Performance;
        text: now(); url: #dom-performance-now;
    type: dfn; text: current high resolution time; url: #dfn-current-high-resolution-time;
    type: attribute; for: WindowOrWorkerGlobalScope;
        text: performance; url: #dom-windoworworkerglobalscope-performance;
urlPrefix: https://w3c.github.io/paint-timing/; spec: PAINT-TIMING;
    type: dfn; url: #mark-paint-timing; text: mark paint timing;
urlPrefix: https://www.w3.org/TR/css-box-3/; spec: CSS-BOX-3;
    type: dfn; url: #border-box; text: border box;
urlPrefix: https://www.w3.org/TR/css-break-3/; spec: CSS-BREAK-3;
    type: dfn; url: #box-fragment; text: box fragment;
urlPrefix: https://www.w3.org/TR/cssom-view-1/; spec: CSSOM-VIEW-1;
    type: dfn; url: #css-pixels; text: CSS pixels;
    type: dfn; url: #viewport; text: viewport;
urlPrefix: https://www.w3.org/TR/css-values-4/; spec: CSS-VALUES-4;
    type: dfn; url: #pixel-unit; text: pixel units;
urlPrefix: https://www.w3.org/TR/CSS2/visudet.html; spec: CSS2;
    type: dfn; url: #containing-block-details; text: initial containing block;
urlPrefix: https://wicg.github.io/visual-viewport/index.html; spec: VISUAL-VIEWPORT;
    type: dfn; url: #dom-visualviewport-width; text: visual viewport width;
    type: dfn; url: #dom-visualviewport-height; text: visual viewport height;
urlPrefix: https://www.w3.org/TR/css-text-3/; spec: CSS-TEXT-3;
    type: dfn; url: #line-breaking; text: line box;
</pre>
<pre class=link-defaults>
spec:css-transforms-1; type:dfn; text:local coordinate system
spec:css-break-4; type:dfn; text:fragment
</pre>

<div class="non-normative">

Introduction {#sec-intro}
=====================

<em>This section is non-normative.</em>

The shifting of DOM elements on a webpage detracts from the user's experience,
and occurs frequently on the web today. This shifting is often due to content
loading asynchronously and displacing other elements on the page.

The layout Instability API identifies these unstable pages by reporting a value
(the "layout shift") for each animation frame in the user's session.  This
specification presents a method for a user agent to compute the layout shift
value.

The layout shift value is expected to have a general correspondence to
the severity of layout instability at a particular time.  The method of computing
it considers both the area of the region impacted by instability and the distance
by which elements on the page are shifted.

The developer can use the layout shift values that are reported by this API
to compute a cumulative score (the "cumulative layout shift score").

The cumulative layout shift score is expected to have a general correspondence to
the severity of layout instability for the lifetime of a page.

End of session signal {#end-of-session}
---------------------------------------

<em>This section is non-normative.</em>

The developer can compute a cumulative layout shift score by summing the layout
shift values as they are reported to the observer.

A "final" score for the user's session can be reported by listening to the
<a href="https://developers.google.com/web/updates/2018/07/page-lifecycle-api#event-visibilitychange">visibilitychange event</a>,
and taking the value of the cumulative layout shift score at that time.

This strategy is illustrated in the usage example.

Usage example {#example}
------------------------

<em>This section is non-normative.</em>

<pre class="example highlight">
    // Stores the current layout shift score for the page.
    let cumulativeLayoutShiftScore = 0;

    // Detects new layout shift occurrences and updates the
    // `cumulativeLayoutShiftScore` variable.
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        // Only count layout shifts without recent user input.
        if (!entry.hadRecentInput) {
          cumulativeLayoutShiftScore += entry.value;
        }
      }
    });

    observer.observe({type: 'layout-shift', buffered: true});

    // Sends the final score to your analytics back end once
    // the page's lifecycle state becomes hidden.
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') {
        // Force any pending records to be dispatched.
        observer.takeRecords();

        // Send the final score to your analytics back end
        // (assumes `sendToAnalytics` is defined elsewhere).
        sendToAnalytics({cumulativeLayoutShiftScore});
      }
    });
</pre>

The layout shift score is only one signal, which correlates in an approximate
manner with the user experience of "jumpiness".

Developers should avoid relying on the precise value of the layout shift score,
as the metric might evolve over time, and user agents might compromise precision
in the interest of calculation efficiency.

</div>

Terminology {#sec-terminology}
==============================

Basic Concepts {#sec-basic-concepts}
------------------------------------

The <dfn export>starting point</dfn> of a {{Node}} |N| in a coordinate space |C|
is defined as follows:

* If |N| is an {{Element}} which generates one or more <a>boxes</a>, the
    starting point of |N| in |C| is the two-dimensional offset in <a>pixel
    units</a> from the origin of |C| to the <a>flow-relative</a> starting corner
    of the first <a>fragment</a> of the <a>principal box</a> of |N|.

* If |N| is a <a>text node</a>, the starting point of |N| in |C| is the
    two-dimensional offset in <a>pixel units</a> from the origin of C to the
    <a>flow-relative</a> starting corner of the first <a>line box</a> generated
    by |N|.

The <dfn export>visual representation</dfn> of a {{Node}} |N| is defined as
follows:

* If |N| is an {{Element}} which generates one or more <a>boxes</a>, the visual
    representation of |N| is the set of all points that lie within the bounds of
    any <a>fragment</a> of any <a>box</a> generated by |N|, in the coordinate
    space of the <a>viewport</a>, excluding any points that lie outside of the
    <a>viewport</a>.

* If |N| is a <a>text node</a>, the visual representation of |N| is the set of
    all points that lie within the bounds of any <a>line box</a> generated by
    |N|, in the coordinate space of the <a>viewport</a>, excluding any points
    that lie outside of the <a>viewport</a>.

A condition holds <dfn export>in the previous frame</dfn> if it was true at the
point in time immediately after the most recently completed invocation of the
<a>report the layout shift</a> algorithm.

The <dfn export>previous frame starting point</dfn> of a {{Node}} |N| in a
coordinate space |C| is the point which, <a>in the previous frame</a>, was the
<a>starting point</a> of |N| in |C|.

The <dfn export>previous frame visual representation</dfn> of a {{Node}} |N| is
the set which, <a>in the previous frame</a>, was the <a>visual
representation</a> of |N|.

Unstable Nodes {#sec-unstable-nodes}
------------------------------------

A {{Node}} |N| <dfn export>has shifted</dfn> in a coordinate space |C| if the
<a>starting point</a> of |N| in |C| differs from the <a>previous frame starting
point</a> of |N| in |C| by 3 or more <a>pixel units</a> in either the horizontal
or vertical direction.

Otherwise, |N| <dfn export>has not shifted</dfn> in |C|.

A {{Node}} |N| is <dfn export>unstable</dfn> if:

* |N| is either
    * an {{Element}} which generates one or more <a>boxes</a>, or
    * a <a>text node</a>; and
* |N| <a>has shifted</a> in the coordinate space of the <a>viewport</a>; and
* |N| <a>has shifted</a> in the coordinate space of the <a>initial containing
    block</a>; and
* there does not exist an {{Element}} |P| such that
    1. currently and <a>in the previous frame</a>, |P| is in the <a>containing
        block chain</a> of |N|, and
    1. currently and <a>in the previous frame</a>, |P| has a <a>local coordinate
        system</a> or a <a>scrollable overflow region</a>, and
    1. |P| is not <a>unstable</a>, and
    1. |N| <a>has not shifted</a> in the coordinate space of the <a>local
        coordinate system</a> or <a>scrollable overflow region</a> of |P|.

The <dfn export>unstable node set</dfn> of a {{Document}} |D| is the set
containing every <a>unstable</a> <a>shadow-including descendant</a> of |D|.

NOTE: In the first frame, the previous frame starting point does not exist for
any node, and therefore the unstable node set is empty.

Layout Shift Value {#sec-layout-shift-value}
--------------------------------------------

The <dfn export>viewport base distance</dfn> is the greater of the <a>visual
viewport width</a> or the <a>visual viewport height</a>.

The <dfn export>move vector</dfn> of a {{Node}} |N| is the two-dimensional
offset in <a>pixel units</a> from

* the <a>previous frame starting point</a> of |N| in the coordinate space of the
    <a>viewport</a>, to
* the <a>starting point</a> of |N| in the coordinate space of the
    <a>viewport</a>.

The <dfn export>move distance</dfn> of a {{Node}} |N| is the greater of

* the absolute value of the horizontal component of the <a>move vector</a> of
    |N|, or
* the absolute value of the vertical component of the <a>move vector</a> of |N|.

The <dfn export>maximum move distance</dfn> of a {{Document}} |D| is the
greatest <a>move distance</a> of any {{Node}} in the <a>unstable node set</a> of
|D|, or 0 if the <a>unstable node set</a> of |D| is empty.

The <dfn export>distance fraction</dfn> of a {{Document}} |D| is the lesser of

* the <a>maximum move distance</a> of |D| divided by the <a>viewport base
    distance</a>, or
* 1.0.

The <dfn export>impact region</dfn> of a {{Document}} |D| is the set containing

* every point in the <a>visual representation</a> of any {{Node}} in the
    <a>unstable node set</a> of |D|, and
* every point in the <a>previous frame visual representation</a> of any {{Node}}
    in the <a>unstable node set</a> of |D|.

The <dfn export>impact fraction</dfn> of a {{Document}} |D| is the area of the
<a>impact region</a> divided by the area of the <a>viewport</a>.

The <dfn export>layout shift value</dfn> of a {{Document}} |D| is the <a>impact
fraction</a> of |D| multiplied by the <a>distance fraction</a> of |D|.

NOTE: The layout shift value takes into account both the fraction of the
viewport that has been impacted by layout instability as well as the greatest
distance by which any given element has moved. This recognizes that a large
element which moves only a small distance can have a low impact on the perceived
instability of the page.

Input Exclusion {#sec-input-exclusion}
--------------------------------------

An <dfn export>excluding input</dfn> is any event from an input device which
signals a user's active interaction with the document, or any event which
directly changes the size of the <a>viewport</a>.

Excluding inputs generally include
<a href="https://www.w3.org/TR/uievents/#event-type-mousedown">mousedown</a>,
<a href="https://www.w3.org/TR/uievents/#keydown">keydown</a>, and
<a href="https://www.w3.org/TR/pointerevents/#the-pointerdown-event">pointerdown</a>.
However, an event whose only effect is to begin or update a fling or scroll
gesture is not an excluding input.

The user agent may delay the reporting of layout shifts after a
<a href="https://www.w3.org/TR/pointerevents/#the-pointerdown-event">pointerdown</a> event
until such time as it is known that the event does not begin a fling or scroll
gesture.

The <a href="https://www.w3.org/TR/uievents/#event-type-mousemove">mousemove</a> and
<a href="https://www.w3.org/TR/pointerevents/#the-pointermove-event">pointermove</a>
events are also not excluding inputs.

{{LayoutShift}} interface {#sec-layout-shift}
=======================================

<pre class="idl">
  [Exposed=Window]
  interface LayoutShift : PerformanceEntry {
    readonly attribute long value;
    readonly attribute boolean hadRecentInput;
    readonly attribute DOMHighResTimeStamp lastInputTime;
    [Default] object toJSON();
  };
</pre>

All attributes have the values which are assigned to them by the steps to
<a>report the layout shift</a>.

A user agent implementing the Layout Instability API MUST include
<code>"layout-shift"</code> in {{PerformanceObserver/supportedEntryTypes}} for
<a href="https://html.spec.whatwg.org/multipage/window-object.html#the-window-object">Window</a>
contexts. This allows developers to detect support for the Layout Instability API.

Processing model {#sec-processing-model}
========================================

Within the <a>update the rendering</a> step of the <a>event loop processing
model</a>, a user agent implementing the Layout Instability API MUST perform the
following step after the step that invokes the <a>mark paint timing</a>
algorithm:

1. For each fully active {{Document}} in <em>docs</em>, invoke the algorithm to
    <a>report the layout shift</a> for that {{Document}}.

Report the layout shift {#sec-report-layout-shift}
-----------------------------------------------------

<div algorithm="report the layout shift">
When asked to <dfn export>report the layout shift</dfn> for an active
{{Document}} |D|, run the following steps:

1. If the current <a>layout shift value</a> of |D| is not 0:
    1. Create a new {{LayoutShift}} object |newEntry|.
    1. Set |newEntry|'s {{PerformanceEntry/name}} attribute to
        <code>"layout-shift"</code>.
    1. Set |newEntry|'s {{PerformanceEntry/entryType}} attribute to
        <code>"layout-shift"</code>.
    1. Set |newEntry|'s {{PerformanceEntry/startTime}} attribute to <a>current
        high resolution time</a>.
    1. Set |newEntry|'s {{PerformanceEntry/duration}} attribute to 0.
    1. Set |newEntry|'s <dfn attribute for=LayoutShift>value</dfn> attribute to
        the current <a>layout shift value</a> of |D|.
    1. Set |newEntry|'s <dfn attribute for=LayoutShift>lastInputTime</dfn>
        attribute to the time of the most recent <a>excluding input</a>, or 0 if
        no excluding input has occurred during the browsing session.
    1. Set |newEntry|'s <dfn attribute for=LayoutShift>hadRecentInput</dfn>
        attribute to <code>true</code> if {{LayoutShift/lastInputTime}} is less
        than 500 milliseconds in the past, and <code>false</code> otherwise.
    1. <a href="https://w3c.github.io/performance-timeline/#dfn-queue-a-performanceentry">Queue the PerformanceEntry</a>
        |newEntry| object.

</div>

Security & privacy considerations {#priv-sec}
===============================================

Layout instability bears an indirect relationship to <a href="https://w3c.github.io/resource-timing/">resource timing</a>, as slow resources could cause intermediate layouts that would not otherwise be performed. Resource timing information can be used by malicious websites for <a>statistical fingerprinting</a>.
The layout instability API only reports instability in the current browsing context. It does not directly provide any aggregation of instability scores across multiple browsing contexts. Developers can implement such aggregation manually, but browsing contexts with different <a href="https://html.spec.whatwg.org/multipage/origin.html#concept-origin">origins</a> would need to cooperate to share instability scores.

