Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 40 additions & 60 deletions index.bs
Original file line number Diff line number Diff line change
Expand Up @@ -127,20 +127,17 @@ A <dfn>largest contentful paint candidate</dfn> is a [=struct=] containing the
following members:

* <dfn for="largest contentful paint candidate">element</dfn>, an [=/element=]
* <dfn for="largest contentful paint candidate">size</dfn>, a [=/number=]
* <dfn for="largest contentful paint candidate">request</dfn>, a [=/Request=] or null
* <dfn for="largest contentful paint candidate">loadTime</dfn>, a [=/number=]

An [=largest contentful paint candidate=] |candidate| is <dfn>eligible to be largest contentful paint</dfn> if it meets the
following criteria:

* |candidate|'s [=largest contentful paint candidate/element=]'s opacity is > 0
* |candidate|'s [=largest contentful paint candidate/element=] is a text node, or |candidate|'s [=largest contentful paint candidate/request=]'s
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: This wording stays but moves down into the "effective visual size" algo, because that algo calls this algo, and that was circular.

However, I removed the opacity>0 check because there are other similar checks (i.e. visibility true) that paint-timing enforces, and because paint-timing already checks that text nodes are paintable, which implies opacity... And also all these checks should apply consistently to element timing.

Entropy and effective visual size are unique to LCP, though.

[=/response=]'s content length in bytes is >= |candidate|'s [=largest contentful paint candidate/element=]'s [=effective visual size=] * 0.004

Note: This heuristic tests whether the image resource contains sufficient data to be seen as contentful to the user. It compares the transferred file size with the number of pixels which are actually produced, after decoding and any image scaling is applied. Images which encode a very large number of pixels with in a very small number of bytes are typically low-content backgrounds, gradients, and the like, and are not considered as [=largest contentful paint candidates=].

Largest Contentful Paint {#sec-largest-contentful-paint}
=======================================

Note: A user agent implementing the Largest Contentful Paint API would need to include <code>"largest-contentful-paint"</code> in {{PerformanceObserver/supportedEntryTypes}} for {{Window}} contexts.
This allows developers to detect support for the API.

Largest Contentful Paint involves the following new interface:

{{LargestContentfulPaint}} interface {#sec-largest-contentful-paint-interface}
Expand Down Expand Up @@ -195,9 +192,6 @@ The {{LargestContentfulPaint/element}} attribute's getter must perform the follo
Note: The above algorithm defines that an element that is no longer a [=tree/descendant=] of the {{Document}} will no longer be returned by {{LargestContentfulPaint/element}}'s attribute getter, including elements that are inside a shadow DOM.

This specification also extends {{Document}} by adding to it a <dfn>largest contentful paint size</dfn> concept, initially set to 0.
It also adds an associated <dfn>content set</dfn>, which is initially an empty <a spec=infra for=/>set</a>. The [=content set=] will be filled with ({{Element}}, {{Request}}) <a>tuples</a>. This is used for performance, to enable the algorithm to only consider each content once.

Note: The user agent needs to maintain the [=content set=] so that removed content does not introduce memory leaks. In particular, it can tie the lifetime of the <a>tuples</a> to weak pointers to the {{Element|Elements}} so that it can be cleaned up sometime after the {{Element|Elements}} are removed. Since the <a spec=infra for=/>set</a> is not exposed to web developers, this does not expose garbage collection timing.

Processing model {#sec-processing-model}
========================================
Expand All @@ -218,31 +212,41 @@ Modifications to the DOM specification {#sec-modifications-DOM}
* If |target|'s [=relevant global object=] is a {{Window}} object, <var ignore>event</var>'s {{Event/type}} is {{Document/scroll}} and its {{Event/isTrusted}} is true, set |target|'s [=relevant global object=]'s [=has dispatched scroll event=] to true.
</div>


Report Largest Contentful Paint {#sec-report-largest-contentful-paint}
----------------------------------------------------------------------

<div export algorithm="report largest contentful paint">
When asked to <dfn>report largest contentful paint</dfn> given a {{Document}} |document|, a [=PaintTimingMixin/paint timing info=] |paintTimingInfo|, an [=ordered set=] of [=pending image records=] |paintedImages|, and an [=ordered set=] of [=/elements=] |paintedTextNodes|, perform the following steps:

Note: Each pending image record in |paintedImages| and text element in |paintedTextNodes| will only be reported exactly once, from [=mark paint timing=], for the first paint where the element is considered paintable (i.e. has opacity and visibility) and contentful (i.e. image resource or blocking fonts are sufficiently loaded).

1. Let |window| be |document|’s [=relevant global object=].
1. If either of |window|'s [=has dispatched scroll event=] or [=has dispatched input event=] is true, return.
1. Let |newCandidateSize| be |document|'s [=largest contentful paint size=].
1. Let |newCandidate| be null.
1. [=list/For each=] |record| of |paintedImages|:
1. Let |imageElement| be |record|'s [=pending image record/element=].
1. If |imageElement| is not [=exposed for paint timing=], given |document|, continue.
1. Let |request| be |record|'s [=pending image record/request=].
1. Let |candidate| be (|imageElement|, |request|)
1. Let |intersectionRect| be the value returned by the intersection rect algorithm using |imageElement| as the target and viewport as the root.
1. <a>Potentially add a LargestContentfulPaint entry</a> with |candidate|, |intersectionRect|, |paintTimingInfo|, |record|'s [=pending image record/loadTime=] and |document|.
1. Let |size| be the [=effective visual size=] of |imageElement| given |intersectionRect| and |record|'s [=pending image record/request=].
1. If |size| is less than or equal to |newCandidateSize|, continue.
1. Set |newCandidateSize| to |size|.
1. Set |newCandidate| to be a new [=largest contentful paint candidate=] with its [=largest contentful paint candidate/element=] set to |imageElement|, its [=largest contentful paint candidate/size=] set to |size|, its [=largest contentful paint candidate/request=] set to |record|'s [=pending image record/request=], and its [=largest contentful paint candidate/loadTime=] set to |record|'s [=pending image record/loadTime=].
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviews: An alternative is to skip the "candidate" struct and just create the LCP entry here, without emitting it until the end of this algorithm for the one largest entry created.

The upside of that would also be that you can more easily do custom image vs text related things.

However, that would require refactoring yet more of the spec and its already a large patch.

1. [=list/For each=] |textNode| of |paintedTextNodes|,
1. If |textNode| is not [=exposed for paint timing=], given |document|, continue.
1. If |textNode| has [=alpha channel=] value <=0 or [=opacity=] value <=0:
1. If |textNode|'s <a property>text-shadow</a> value is none, |textNode|'s 'stroke-color' value is [=transparent=] and |textNode|'s 'stroke-image' value is none, continue.
1. Let |candidate| be (|textNode|, null)
1. Let |intersectionRect| be an empty rectangle.
1. [=set/For each=] {{Text}} <a>node</a> |text| of |textNode|'s <a>set of owned text nodes</a>:
1. Augment |intersectionRect| to be smallest rectangle containing the border box of |text| and |intersectionRect|.
1. Intersect |intersectionRect| with the visual viewport.
1. <a>Potentially add a LargestContentfulPaint entry</a> with |candidate|, |intersectionRect|, |paintTimingInfo|, 0, and |document|.
1. Let |intersectionRect| be the union of the border boxes of all {{Text}} <a>nodes</a> in |textNode|'s <a>set of owned text nodes</a>, intersected with the visual viewport.
1. Let |size| be the [=effective visual size=] of |textNode| given |intersectionRect| and null.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only thing this does for text nodes is remove 100% width/height content, which is mostly only useful for images that act as full page backgrounds.

For 100% full viewport text the only example I can think of is "gaming" where ~invisible text is rendered, but I don't think this is meant to be a protection against that.

We might be able to skip this step, and that simplifies a few things in small ways.

1. If |size| is less than or equal to |newCandidateSize|, continue.
1. Set |newCandidateSize| to |size|.
1. Set |newCandidate| to be a new [=largest contentful paint candidate=] with its [=largest contentful paint candidate/element=] set to |textNode|, its [=largest contentful paint candidate/size=] set to |size|, its [=largest contentful paint candidate/request=] set to null, and its [=largest contentful paint candidate/loadTime=] set to 0.
1. If |newCandidate| is not null:
1. <a>Create a LargestContentfulPaint entry</a> with |newCandidate|, |paintTimingInfo|, and |document|.
</div>


Determine the effective visual size of an element {#sec-effective-visual-size}
------------------------------------------------------------------------------

Expand All @@ -252,7 +256,7 @@ run the following steps:
<div algorithm="LargestContentfulPaint effective-visual-size">
: Input
:: |intersectionRect|, a {{DOMRectReadOnly}}
:: |imageRequest|, a {{Request}}
:: |imageRequest|, a {{Request}} or null
:: |element|, an [=/Element=]
:: |document|, a <a>Document</a>
: Output
Expand All @@ -264,8 +268,12 @@ run the following steps:
1. Let |rootWidth| be |root|'s <a>visual viewport</a>'s width, excluding any scrollbars.
1. Let |rootHeight| be |root|'s <a>visual viewport</a>'s height, excluding any scrollbars.
1. If |size| is equal to |rootWidth| times |rootHeight|, return null.
1. If |imageRequest| is not [=eligible to be largest contentful paint=], return null.

1. If |imageRequest| is not null, run the following steps to adjust for image position and upscaling:
1. If |imageRequest|'s [=/response=]'s content length in bytes is less than |size| * 0.004, then return null.

Note: This heuristic tests whether the image resource contains sufficient data to be seen as contentful to the user. It compares the transferred file size with the number of pixels which are actually produced, after decoding and any image scaling is applied. Images which encode a very large number of pixels with in a very small number of bytes are typically low-content backgrounds, gradients, and the like, and are not considered as LCP candidates.

1. Let |concreteDimensions| be |imageRequest|'s [=concrete object size=] within |element|.
1. Let |visibleDimensions| be |concreteDimensions|, adjusted for positioning by 'object-position' or 'background-position' and |element|'s [=content box=].

Expand All @@ -275,7 +283,7 @@ run the following steps:
1. Let |intersectingClientContentRect| be the intersection of |clientContentRect| with |intersectionRect|.
1. Set |size| to <code>|intersectingClientContentRect|'s {{DOMRectReadOnly/width}} * |intersectingClientContentRect|'s {{DOMRectReadOnly/height}}</code>.

Note: this ensures that we only intersect with the image itself and not with the element's decorations.
Note: This ensures that we only intersect with the image itself and not with the element's decorations.

1. Let |naturalArea| be <code>|imageRequest|'s [=natural width=] * |imageRequest|'s [=natural height=]</code>.
1. If |naturalArea| is 0, then return null.
Expand All @@ -286,59 +294,31 @@ run the following steps:
1. Return |size|.
</div>

Potentially add LargestContentfulPaint entry {#sec-add-lcp-entry}
-----------------------------------------------------------------

Note: A user agent implementing the Largest Contentful Paint API would need to include <code>"largest-contentful-paint"</code> in {{PerformanceObserver/supportedEntryTypes}} for {{Window}} contexts.
This allows developers to detect support for the API.

In order to <dfn export>potentially add a {{LargestContentfulPaint}} entry</dfn>, the user agent must run the following steps:
<div algorithm="LargestContentfulPaint potentially-add-entry">
: Input
:: |candidate|, a [=largest contentful paint candidate=]
:: |intersectionRect|, a {{DOMRectReadOnly}}
:: |paintTimingInfo|, a [=PaintTimingMixin/paint timing info=]
:: |loadTime|, a DOMHighResTimestamp
:: |document|, a <a>Document</a>
: Output
:: None
1. If |document|'s [=content set=] <a for=set>contains</a> |candidate|, return.
1. <a for=set>Append</a> |candidate| to |document|'s [=content set=]
1. Let |window| be |document|’s [=relevant global object=].
1. If either of |window|'s [=has dispatched scroll event=] or [=has dispatched input event=] is true, return.
1. Let |size| be the [=effective visual size=] of |candidate|'s [=/element=] given |intersectionRect|.
1. If |size| is less than or equal to |document|'s [=largest contentful paint size=], return.
1. Let |url| be the empty string.
1. If |candidate|'s [=largest contentful paint candidate/request=] is not null, set |url| to be |candidate|'s [=largest contentful paint candidate/request=]'s [=request URL=].
1. Let |id| be |candidate|'s [=largest contentful paint candidate/element=]'s <a attribute for=Element>element id</a>.
1. Let |contentInfo| be a <a>map</a> with |contentInfo|["size"] = |size|, |contentInfo|["url"] = |url|, |contentInfo|["id"] = |id|, |contentInfo|["loadTime"] = |loadTime|, and contentInfo["element"] = |candidate|'s [=largest contentful paint candidate/element=].
1. <a>Create a LargestContentfulPaint entry</a> with |contentInfo|, |paintTimingInfo|, and |document| as inputs.
</div>

Create a LargestContentfulPaint entry {#sec-create-entry}
--------------------------------------------------------

In order to <dfn>create a {{LargestContentfulPaint}} entry</dfn>, the user agent must run the following steps:

<div algorithm="LargestContentfulPaint create-entry">
: Input
:: |contentInfo|, a <a>map</a>
:: |candidate|, a [=largest contentful paint candidate=]
:: |paintTimingInfo|, a [=PaintTimingMixin/paint timing info=]
:: |document|, a {{Document}}
: Output
:: None
1. Set |document|'s [=largest contentful paint size=] to |contentInfo|["size"].
1. Set |document|'s [=largest contentful paint size=] to |candidate|'s [=largest contentful paint candidate/size=].
1. Let |url| be the empty string.
1. If |candidate|'s [=largest contentful paint candidate/request=] is not null, set |url| to be |candidate|'s [=largest contentful paint candidate/request=]'s [=request URL=].
1. Let |entry| be a new {{LargestContentfulPaint}} entry with |document|'s [=relevant realm=], whose [=PaintTimingMixin/paint timing info=] is |paintTimingInfo|, with its
* {{LargestContentfulPaint/size}} set to |contentInfo|["size"],
* {{LargestContentfulPaint/url}} set to |contentInfo|["url"],
* {{LargestContentfulPaint/id}} set to |contentInfo|["id"],
* {{LargestContentfulPaint/loadTime}} set to |contentInfo|["loadTime"],
* and {{LargestContentfulPaint/element}} set to |contentInfo|["element"].
* {{LargestContentfulPaint/size}} set to |candidate|'s [=largest contentful paint candidate/size=],
* {{LargestContentfulPaint/url}} set to |url|,
* {{LargestContentfulPaint/id}} set to |candidate|'s [=largest contentful paint candidate/element=]'s <a attribute for=Element>element id</a>,
* {{LargestContentfulPaint/loadTime}} set to |candidate|'s [=largest contentful paint candidate/loadTime=],
* and {{LargestContentfulPaint/element}} set to |candidate|'s [=largest contentful paint candidate/element=].
1. [=Queue the PerformanceEntry=] |entry|.
</div>

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

This API relies on Paint Timing for its underlying primitives. Unlike the similar API Element Timing, LCP may expose timing details of some elements with small sizes, if they are still the largest elements to be painted up until that point in the page's loading. That does not seem to expose any sensitive information beyond what Element Timing already enables.