<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN"
"http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head><meta http-equiv="content-type" content="text/html; charset=utf-8" />
<title>[52065] trunk: Media: Refine the heuristics to exclude certain images and iframes from being lazy-loaded to improve performance.</title>
</head>
<body>

<style type="text/css"><!--
#msg dl.meta { border: 1px #006 solid; background: #369; padding: 6px; color: #fff; }
#msg dl.meta dt { float: left; width: 6em; font-weight: bold; }
#msg dt:after { content:':';}
#msg dl, #msg dt, #msg ul, #msg li, #header, #footer, #logmsg { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt;  }
#msg dl a { font-weight: bold}
#msg dl a:link    { color:#fc3; }
#msg dl a:active  { color:#ff0; }
#msg dl a:visited { color:#cc6; }
h3 { font-family: verdana,arial,helvetica,sans-serif; font-size: 10pt; font-weight: bold; }
#msg pre { white-space: pre-line; overflow: auto; background: #ffc; border: 1px #fa0 solid; padding: 6px; }
#logmsg { background: #ffc; border: 1px #fa0 solid; padding: 1em 1em 0 1em; }
#logmsg p, #logmsg pre, #logmsg blockquote { margin: 0 0 1em 0; }
#logmsg p, #logmsg li, #logmsg dt, #logmsg dd { line-height: 14pt; }
#logmsg h1, #logmsg h2, #logmsg h3, #logmsg h4, #logmsg h5, #logmsg h6 { margin: .5em 0; }
#logmsg h1:first-child, #logmsg h2:first-child, #logmsg h3:first-child, #logmsg h4:first-child, #logmsg h5:first-child, #logmsg h6:first-child { margin-top: 0; }
#logmsg ul, #logmsg ol { padding: 0; list-style-position: inside; margin: 0 0 0 1em; }
#logmsg ul { text-indent: -1em; padding-left: 1em; }#logmsg ol { text-indent: -1.5em; padding-left: 1.5em; }
#logmsg > ul, #logmsg > ol { margin: 0 0 1em 0; }
#logmsg pre { background: #eee; padding: 1em; }
#logmsg blockquote { border: 1px solid #fa0; border-left-width: 10px; padding: 1em 1em 0 1em; background: white;}
#logmsg dl { margin: 0; }
#logmsg dt { font-weight: bold; }
#logmsg dd { margin: 0; padding: 0 0 0.5em 0; }
#logmsg dd:before { content:'\00bb';}
#logmsg table { border-spacing: 0px; border-collapse: collapse; border-top: 4px solid #fa0; border-bottom: 1px solid #fa0; background: #fff; }
#logmsg table th { text-align: left; font-weight: normal; padding: 0.2em 0.5em; border-top: 1px dotted #fa0; }
#logmsg table td { text-align: right; border-top: 1px dotted #fa0; padding: 0.2em 0.5em; }
#logmsg table thead th { text-align: center; border-bottom: 1px solid #fa0; }
#logmsg table th.Corner { text-align: left; }
#logmsg hr { border: none 0; border-top: 2px dashed #fa0; height: 1px; }
#header, #footer { color: #fff; background: #636; border: 1px #300 solid; padding: 6px; }
#patch { width: 100%; }
#patch h4 {font-family: verdana,arial,helvetica,sans-serif;font-size:10pt;padding:8px;background:#369;color:#fff;margin:0;}
#patch .propset h4, #patch .binary h4 {margin:0;}
#patch pre {padding:0;line-height:1.2em;margin:0;}
#patch .diff {width:100%;background:#eee;padding: 0 0 10px 0;overflow:auto;}
#patch .propset .diff, #patch .binary .diff  {padding:10px 0;}
#patch span {display:block;padding:0 10px;}
#patch .modfile, #patch .addfile, #patch .delfile, #patch .propset, #patch .binary, #patch .copfile {border:1px solid #ccc;margin:10px 0;}
#patch ins {background:#dfd;text-decoration:none;display:block;padding:0 10px;}
#patch del {background:#fdd;text-decoration:none;display:block;padding:0 10px;}
#patch .lines, .info {color:#888;background:#fff;}
--></style>
<div id="msg">
<dl class="meta" style="font-size: 105%">
<dt style="float: left; width: 6em; font-weight: bold">Revision</dt> <dd><a style="font-weight: bold" href="https://core.trac.wordpress.org/changeset/52065">52065</a><script type="application/ld+json">{"@context":"http://schema.org","@type":"EmailMessage","description":"Review this Commit","action":{"@type":"ViewAction","url":"https://core.trac.wordpress.org/changeset/52065","name":"Review Commit"}}</script></dd>
<dt style="float: left; width: 6em; font-weight: bold">Author</dt> <dd>flixos90</dd>
<dt style="float: left; width: 6em; font-weight: bold">Date</dt> <dd>2021-11-09 00:34:17 +0000 (Tue, 09 Nov 2021)</dd>
</dl>

<pre style='padding-left: 1em; margin: 2em 0; border-left: 2px solid #ccc; line-height: 1.25; font-size: 105%; font-family: sans-serif'>Media: Refine the heuristics to exclude certain images and iframes from being lazy-loaded to improve performance.

This changeset implements the refined lazy-loading behavior outlined in https://make.wordpress.org/core/2021/07/15/refining-wordpress-cores-lazy-loading-implementation/ in order to improve the Largest Contentful Paint metric, which can see a regression from images or iframes above the fold being lazy-loaded. Adjusting this so far has been possible for developers via filters and still is, however this enhancement brings a more accurate behavior out of the box for the majority of themes.

Specifically, this changeset skips the very first "content image or iframe" on the page from being lazy-loaded. "Content image or iframe" denotes any image or iframe that is found within content of any post in the current main query loop as well as any featured image of such a post. This applies both to "singular" as well as "archive" content: On a "singular" page the first image/iframe of the post is not lazy-loaded, while on an "archive" page the first image/iframe of the _first_ post in the query is not lazy-loaded.

This approach refines the lazy-loading behavior correctly for the majority of themes, which use a single-column layout for post content. For themes with multi-column layouts, a new `wp_omit_loading_attr_threshold` filter can be used to change how many of the first images/iframes are being skipped from lazy-loaded (default is `1`). For example, a theme using a three-column grid of latest posts for archives could use the filter to override the threshold to `3` on archive pages, so that the first three content images/iframes would not be lazy-loaded.

Props adamsilverstein, azaozz, flixos90, hellofromtonya, jonoaldersonwp, mte90, rviscomi, tweetythierry, westonruter.
Fixes <a href="https://core.trac.wordpress.org/ticket/53675">#53675</a>. See <a href="https://core.trac.wordpress.org/ticket/50425">#50425</a>.</pre>

<h3>Modified Paths</h3>
<ul>
<li><a href="#trunksrcwpincludesmediaphp">trunk/src/wp-includes/media.php</a></li>
<li><a href="#trunksrcwpincludespluggablephp">trunk/src/wp-includes/pluggable.php</a></li>
<li><a href="#trunksrcwpincludespostthumbnailtemplatephp">trunk/src/wp-includes/post-thumbnail-template.php</a></li>
<li><a href="#trunktestsphpunittestsmediaphp">trunk/tests/phpunit/tests/media.php</a></li>
</ul>

</div>
<div id="patch">
<h3>Diff</h3>
<a id="trunksrcwpincludesmediaphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/media.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/media.php   2021-11-09 00:22:34 UTC (rev 52064)
+++ trunk/src/wp-includes/media.php     2021-11-09 00:34:17 UTC (rev 52065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1046,7 +1046,7 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                // Add `loading` attribute.
</span><span class="cx" style="display: block; padding: 0 10px">                if ( wp_lazy_loading_enabled( 'img', 'wp_get_attachment_image' ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $default_attr['loading'] = 'lazy';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $default_attr['loading'] = wp_get_loading_attr_default( 'wp_get_attachment_image' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                $attr = wp_parse_args( $attr, $default_attr );
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1820,39 +1820,45 @@
</span><span class="cx" style="display: block; padding: 0 10px">                _prime_post_caches( $attachment_ids, false, true );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        foreach ( $images as $image => $attachment_id ) {
-               $filtered_image = $image;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ // Iterate through the matches in order of occurrence as it is relevant for whether or not to lazy-load.
+       foreach ( $matches as $match ) {
+               // Filter an image match.
+               if ( isset( $images[ $match[0] ] ) ) {
+                       $filtered_image = $match[0];
+                       $attachment_id  = $images[ $match[0] ];
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Add 'width' and 'height' attributes if applicable.
-               if ( $attachment_id > 0 && false === strpos( $filtered_image, ' width=' ) && false === strpos( $filtered_image, ' height=' ) ) {
-                       $filtered_image = wp_img_tag_add_width_and_height_attr( $filtered_image, $context, $attachment_id );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Add 'width' and 'height' attributes if applicable.
+                       if ( $attachment_id > 0 && false === strpos( $filtered_image, ' width=' ) && false === strpos( $filtered_image, ' height=' ) ) {
+                               $filtered_image = wp_img_tag_add_width_and_height_attr( $filtered_image, $context, $attachment_id );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Add 'srcset' and 'sizes' attributes if applicable.
-               if ( $attachment_id > 0 && false === strpos( $filtered_image, ' srcset=' ) ) {
-                       $filtered_image = wp_img_tag_add_srcset_and_sizes_attr( $filtered_image, $context, $attachment_id );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Add 'srcset' and 'sizes' attributes if applicable.
+                       if ( $attachment_id > 0 && false === strpos( $filtered_image, ' srcset=' ) ) {
+                               $filtered_image = wp_img_tag_add_srcset_and_sizes_attr( $filtered_image, $context, $attachment_id );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Add 'loading' attribute if applicable.
-               if ( $add_img_loading_attr && false === strpos( $filtered_image, ' loading=' ) ) {
-                       $filtered_image = wp_img_tag_add_loading_attr( $filtered_image, $context );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Add 'loading' attribute if applicable.
+                       if ( $add_img_loading_attr && false === strpos( $filtered_image, ' loading=' ) ) {
+                               $filtered_image = wp_img_tag_add_loading_attr( $filtered_image, $context );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $filtered_image !== $image ) {
-                       $content = str_replace( $image, $filtered_image, $content );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( $filtered_image !== $match[0] ) {
+                               $content = str_replace( $match[0], $filtered_image, $content );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        }
</del><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        foreach ( $iframes as $iframe => $attachment_id ) {
-               $filtered_iframe = $iframe;
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         // Filter an iframe match.
+               if ( isset( $iframes[ $match[0] ] ) ) {
+                       $filtered_iframe = $match[0];
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                // Add 'loading' attribute if applicable.
-               if ( $add_iframe_loading_attr && false === strpos( $filtered_iframe, ' loading=' ) ) {
-                       $filtered_iframe = wp_iframe_tag_add_loading_attr( $filtered_iframe, $context );
-               }
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 // Add 'loading' attribute if applicable.
+                       if ( $add_iframe_loading_attr && false === strpos( $filtered_iframe, ' loading=' ) ) {
+                               $filtered_iframe = wp_iframe_tag_add_loading_attr( $filtered_iframe, $context );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                if ( $filtered_iframe !== $iframe ) {
-                       $content = str_replace( $iframe, $filtered_iframe, $content );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 if ( $filtered_iframe !== $match[0] ) {
+                               $content = str_replace( $match[0], $filtered_iframe, $content );
+                       }
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1869,6 +1875,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">  * @return string Converted `img` tag with `loading` attribute added.
</span><span class="cx" style="display: block; padding: 0 10px">  */
</span><span class="cx" style="display: block; padding: 0 10px"> function wp_img_tag_add_loading_attr( $image, $context ) {
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // Get loading attribute value to use. This must occur before the conditional check below so that even images that
+       // are ineligible for being lazy-loaded are considered.
+       $value = wp_get_loading_attr_default( $context );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         // Images should have source and dimension attributes for the `loading` attribute to be added.
</span><span class="cx" style="display: block; padding: 0 10px">        if ( false === strpos( $image, ' src="' ) || false === strpos( $image, ' width="' ) || false === strpos( $image, ' height="' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                return $image;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1883,11 +1893,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.5.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string|bool $value   The `loading` attribute value. Returning a falsey value will result in
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         *                             the attribute being omitted for the image. Default 'lazy'.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  *                             the attribute being omitted for the image.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @param string      $image   The HTML `img` tag to be filtered.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string      $context Additional context about how the function was called or where the img tag is.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $value = apply_filters( 'wp_img_tag_add_loading_attr', 'lazy', $image, $context );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $value = apply_filters( 'wp_img_tag_add_loading_attr', $value, $image, $context );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( $value ) {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! in_array( $value, array( 'lazy', 'eager' ), true ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -1995,6 +2005,10 @@
</span><span class="cx" style="display: block; padding: 0 10px">                return $iframe;
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+        // Get loading attribute value to use. This must occur before the conditional check below so that even iframes that
+       // are ineligible for being lazy-loaded are considered.
+       $value = wp_get_loading_attr_default( $context );
+
</ins><span class="cx" style="display: block; padding: 0 10px">         // Iframes should have source and dimension attributes for the `loading` attribute to be added.
</span><span class="cx" style="display: block; padding: 0 10px">        if ( false === strpos( $iframe, ' src="' ) || false === strpos( $iframe, ' width="' ) || false === strpos( $iframe, ' height="' ) ) {
</span><span class="cx" style="display: block; padding: 0 10px">                return $iframe;
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2009,11 +2023,11 @@
</span><span class="cx" style="display: block; padding: 0 10px">         * @since 5.7.0
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string|bool $value   The `loading` attribute value. Returning a falsey value will result in
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-         *                             the attribute being omitted for the iframe. Default 'lazy'.
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+  *                             the attribute being omitted for the iframe.
</ins><span class="cx" style="display: block; padding: 0 10px">          * @param string      $iframe  The HTML `iframe` tag to be filtered.
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string      $context Additional context about how the function was called or where the iframe tag is.
</span><span class="cx" style="display: block; padding: 0 10px">         */
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-        $value = apply_filters( 'wp_iframe_tag_add_loading_attr', 'lazy', $iframe, $context );
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+ $value = apply_filters( 'wp_iframe_tag_add_loading_attr', $value, $iframe, $context );
</ins><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        if ( $value ) {
</span><span class="cx" style="display: block; padding: 0 10px">                if ( ! in_array( $value, array( 'lazy', 'eager' ), true ) ) {
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -5177,3 +5191,97 @@
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">        return compact( 'width', 'height', 'type' );
</span><span class="cx" style="display: block; padding: 0 10px"> }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+/**
+ * Gets the default value to use for a `loading` attribute on an element.
+ *
+ * This function should only be called for a tag and context if lazy-loading is generally enabled.
+ *
+ * The function usually returns 'lazy', but uses certain heuristics to guess whether the current element is likely to
+ * appear above the fold, in which case it returns a boolean `false`, which will lead to the `loading` attribute being
+ * omitted on the element. The purpose of this refinement is to avoid lazy-loading elements that are within the initial
+ * viewport, which can have a negative performance impact.
+ *
+ * Under the hood, the function uses {@see wp_increase_content_media_count()} every time it is called for an element
+ * within the main content. If the element is the very first content element, the `loading` attribute will be omitted.
+ * This default threshold of 1 content element to omit the `loading` attribute for can be customized using the
+ * {@see 'wp_omit_loading_attr_threshold'} filter.
+ *
+ * @since 5.9.0
+ *
+ * @param string $context Context for the element for which the `loading` attribute value is requested.
+ * @return string|bool The default `loading` attribute value. Either 'lazy', 'eager', or a boolean `false`, to indicate
+ *                     that the `loading` attribute should be skipped.
+ */
+function wp_get_loading_attr_default( $context ) {
+       // Only elements with 'the_content' or 'the_post_thumbnail' context have special handling.
+       if ( 'the_content' !== $context && 'the_post_thumbnail' !== $context ) {
+               return 'lazy';
+       }
+
+       // Only elements within the main query loop have special handling.
+       if ( is_admin() || ! in_the_loop() || ! is_main_query() ) {
+               return 'lazy';
+       }
+
+       // Increase the counter since this is a main query content element.
+       $content_media_count = wp_increase_content_media_count();
+
+       // If the count so far is below the threshold, return `false` so that the `loading` attribute is omitted.
+       if ( $content_media_count <= wp_omit_loading_attr_threshold() ) {
+               return false;
+       }
+
+       // For elements after the threshold, lazy-load them as usual.
+       return 'lazy';
+}
+
+/**
+ * Gets the threshold for how many of the first content media elements to not lazy-load.
+ *
+ * This function runs the {@see 'wp_omit_loading_attr_threshold'} filter, which uses a default threshold value of 1.
+ * The filter is only run once per page load, unless the `$force` parameter is used.
+ *
+ * @since 5.9.0
+ *
+ * @param bool $force Optional. If set to true, the filter will be (re-)applied even if it already has been before.
+ *                    Default false.
+ * @return int The number of content media elements to not lazy-load.
+ */
+function wp_omit_loading_attr_threshold( $force = false ) {
+       static $omit_threshold;
+
+       // This function may be called multiple times. Run the filter only once per page load.
+       if ( ! isset( $omit_threshold ) || $force ) {
+               /**
+                * Filters the threshold for how many of the first content media elements to not lazy-load.
+                *
+                * For these first content media elements, the `loading` attribute will be omitted. By default, this is the case
+                * for only the very first content media element.
+                *
+                * @since 5.9.0
+                *
+                * @param int $omit_threshold The number of media elements where the `loading` attribute will not be added. Default 1.
+                */
+               $omit_threshold = apply_filters( 'wp_omit_loading_attr_threshold', 1 );
+       }
+
+       return $omit_threshold;
+}
+
+/**
+ * Increases an internal content media count variable.
+ *
+ * @since 5.9.0
+ * @access private
+ *
+ * @param int $amount Optional. Amount to increase by. Default 1.
+ * @return int The latest content media count, after the increase.
+ */
+function wp_increase_content_media_count( $amount = 1 ) {
+       static $content_media_count = 0;
+
+       $content_media_count += $amount;
+
+       return $content_media_count;
+}
</ins></span></pre></div>
<a id="trunksrcwpincludespluggablephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/pluggable.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/pluggable.php       2021-11-09 00:22:34 UTC (rev 52064)
+++ trunk/src/wp-includes/pluggable.php 2021-11-09 00:34:17 UTC (rev 52065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -2678,7 +2678,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( wp_lazy_loading_enabled( 'img', 'get_avatar' ) ) {
</span><del style="background-color: #fdd; text-decoration:none; display:block; padding: 0 10px">-                        $defaults['loading'] = 'lazy';
</del><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                 $defaults['loading'] = wp_get_loading_attr_default( 'get_avatar' );
</ins><span class="cx" style="display: block; padding: 0 10px">                 }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                if ( empty( $args ) ) {
</span></span></pre></div>
<a id="trunksrcwpincludespostthumbnailtemplatephp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/src/wp-includes/post-thumbnail-template.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/src/wp-includes/post-thumbnail-template.php 2021-11-09 00:22:34 UTC (rev 52064)
+++ trunk/src/wp-includes/post-thumbnail-template.php   2021-11-09 00:34:17 UTC (rev 52065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -186,6 +186,19 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        update_post_thumbnail_cache();
</span><span class="cx" style="display: block; padding: 0 10px">                }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                // Get the 'loading' attribute value to use as default, taking precedence over the default from
+               // `wp_get_attachment_image()`.
+               $loading = wp_get_loading_attr_default( 'the_post_thumbnail' );
+
+               // Add the default to the given attributes unless they already include a 'loading' directive.
+               if ( empty( $attr ) ) {
+                       $attr = array( 'loading' => $loading );
+               } elseif ( is_array( $attr ) && ! array_key_exists( 'loading', $attr ) ) {
+                       $attr['loading'] = $loading;
+               } elseif ( is_string( $attr ) && ! preg_match( '/(^|&)loading=', $attr ) ) {
+                       $attr .= '&loading=' . $loading;
+               }
+
</ins><span class="cx" style="display: block; padding: 0 10px">                 $html = wp_get_attachment_image( $post_thumbnail_id, $size, false, $attr );
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px">                /**
</span></span></pre></div>
<a id="trunktestsphpunittestsmediaphp"></a>
<div class="modfile"><h4 style="background-color: #eee; color: inherit; margin: 1em 0; padding: 1.3em; font-size: 115%">Modified: trunk/tests/phpunit/tests/media.php</h4>
<pre class="diff"><span>
<span class="info" style="display: block; padding: 0 10px; color: #888">--- trunk/tests/phpunit/tests/media.php       2021-11-09 00:22:34 UTC (rev 52064)
+++ trunk/tests/phpunit/tests/media.php 2021-11-09 00:34:17 UTC (rev 52065)
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3024,6 +3024,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">        /**
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 50425
</span><span class="cx" style="display: block; padding: 0 10px">         * @ticket 53463
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+         * @ticket 53675
</ins><span class="cx" style="display: block; padding: 0 10px">          * @dataProvider data_wp_lazy_loading_enabled_context_defaults
</span><span class="cx" style="display: block; padding: 0 10px">         *
</span><span class="cx" style="display: block; padding: 0 10px">         * @param string $context  Function context.
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3046,6 +3047,7 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        'widget_block_content => true'    => array( 'widget_block_content', true ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'get_avatar => true'              => array( 'get_avatar', true ),
</span><span class="cx" style="display: block; padding: 0 10px">                        'arbitrary context => true'       => array( 'something_completely_arbitrary', true ),
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+                        'the_post_thumbnail => true'      => array( 'the_post_thumbnail', true ),
</ins><span class="cx" style="display: block; padding: 0 10px">                 );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="lines" style="display: block; padding: 0 10px; color: #888">@@ -3186,6 +3188,178 @@
</span><span class="cx" style="display: block; padding: 0 10px">                        array( 'trash-attachment', '/?attachment_id=%ID%', false ),
</span><span class="cx" style="display: block; padding: 0 10px">                );
</span><span class="cx" style="display: block; padding: 0 10px">        }
</span><ins style="background-color: #dfd; text-decoration:none; display:block; padding: 0 10px">+
+       /**
+        * @ticket 53675
+        * @dataProvider data_wp_get_loading_attr_default
+        *
+        * @param string $context
+        */
+       function test_wp_get_loading_attr_default( $context ) {
+               global $wp_query, $wp_the_query;
+
+               // Return 'lazy' by default.
+               $this->assertSame( 'lazy', wp_get_loading_attr_default( 'test' ) );
+               $this->assertSame( 'lazy', wp_get_loading_attr_default( 'wp_get_attachment_image' ) );
+
+               // Return 'lazy' if not in the loop or the main query.
+               $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
+
+               $wp_query = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
+               $this->reset_content_media_count();
+               $this->reset_omit_loading_attr_filter();
+
+               while ( have_posts() ) {
+                       the_post();
+
+                       // Return 'lazy' if in the loop but not in the main query.
+                       $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
+
+                       // Set as main query.
+                       $wp_the_query = $wp_query;
+
+                       // For contexts other than for the main content, still return 'lazy' even in the loop
+                       // and in the main query, and do not increase the content media count.
+                       $this->assertSame( 'lazy', wp_get_loading_attr_default( 'wp_get_attachment_image' ) );
+
+                       // Return `false` if in the loop and in the main query and it is the first element.
+                       $this->assertFalse( wp_get_loading_attr_default( $context ) );
+
+                       // Return 'lazy' if in the loop and in the main query for any subsequent elements.
+                       $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
+
+                       // Yes, for all subsequent elements.
+                       $this->assertSame( 'lazy', wp_get_loading_attr_default( $context ) );
+               }
+       }
+
+       function data_wp_get_loading_attr_default() {
+               return array(
+                       array( 'the_content' ),
+                       array( 'the_post_thumbnail' ),
+               );
+       }
+
+       /**
+        * @ticket 53675
+        */
+       function test_wp_omit_loading_attr_threshold_filter() {
+               global $wp_query, $wp_the_query;
+
+               $wp_query     = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
+               $wp_the_query = $wp_query;
+               $this->reset_content_media_count();
+               $this->reset_omit_loading_attr_filter();
+
+               // Use the filter to alter the threshold for not lazy-loading to the first three elements.
+               add_filter(
+                       'wp_omit_loading_attr_threshold',
+                       function() {
+                               return 3;
+                       }
+               );
+
+               while ( have_posts() ) {
+                       the_post();
+
+                       // Due to the filter, now the first three elements should not be lazy-loaded, i.e. return `false`.
+                       for ( $i = 0; $i < 3; $i++ ) {
+                               $this->assertFalse( wp_get_loading_attr_default( 'the_content' ) );
+                       }
+
+                       // For following elements, lazy-load them again.
+                       $this->assertSame( 'lazy', wp_get_loading_attr_default( 'the_content' ) );
+               }
+       }
+
+       /**
+        * @ticket 53675
+        */
+       function test_wp_filter_content_tags_with_wp_get_loading_attr_default() {
+               global $wp_query, $wp_the_query;
+
+               $img1         = get_image_tag( self::$large_id, '', '', '', 'large' );
+               $iframe1      = '<iframe src="https://www.example.com" width="640" height="360"></iframe>';
+               $img2         = get_image_tag( self::$large_id, '', '', '', 'medium' );
+               $img3         = get_image_tag( self::$large_id, '', '', '', 'thumbnail' );
+               $iframe2      = '<iframe src="https://wordpress.org" width="640" height="360"></iframe>';
+               $lazy_img2    = wp_img_tag_add_loading_attr( $img2, 'the_content' );
+               $lazy_img3    = wp_img_tag_add_loading_attr( $img3, 'the_content' );
+               $lazy_iframe2 = wp_iframe_tag_add_loading_attr( $iframe2, 'the_content' );
+
+               // Use a threshold of 2.
+               add_filter(
+                       'wp_omit_loading_attr_threshold',
+                       function() {
+                               return 2;
+                       }
+               );
+
+               // Following the threshold of 2, the first two content media elements should not be lazy-loaded.
+               $content_unfiltered = $img1 . $iframe1 . $img2 . $img3 . $iframe2;
+               $content_expected   = $img1 . $iframe1 . $lazy_img2 . $lazy_img3 . $lazy_iframe2;
+
+               $wp_query     = new WP_Query( array( 'post__in' => array( self::$post_ids['publish'] ) ) );
+               $wp_the_query = $wp_query;
+               $this->reset_content_media_count();
+               $this->reset_omit_loading_attr_filter();
+
+               while ( have_posts() ) {
+                       the_post();
+
+                       add_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
+                       $content_filtered = wp_filter_content_tags( $content_unfiltered, 'the_content' );
+                       remove_filter( 'wp_img_tag_add_srcset_and_sizes_attr', '__return_false' );
+               }
+
+               // After filtering, the first image should not be lazy-loaded while the other ones should be.
+               $this->assertSame( $content_expected, $content_filtered );
+       }
+
+       /**
+        * @ticket 53675
+        */
+       public function test_wp_omit_loading_attr_threshold() {
+               $this->reset_omit_loading_attr_filter();
+
+               // Apply filter, ensure default value of 1.
+               $omit_threshold = wp_omit_loading_attr_threshold();
+               $this->assertSame( 1, $omit_threshold );
+
+               // Add a filter that changes the value to 3. However, the filter is not applied a subsequent time in a single
+               // page load by default, so the value is still 1.
+               add_filter(
+                       'wp_omit_loading_attr_threshold',
+                       function() {
+                               return 3;
+                       }
+               );
+               $omit_threshold = wp_omit_loading_attr_threshold();
+               $this->assertSame( 1, $omit_threshold );
+
+               // Only by enforcing a fresh check, the filter gets re-applied.
+               $omit_threshold = wp_omit_loading_attr_threshold( true );
+               $this->assertSame( 3, $omit_threshold );
+       }
+
+       private function reset_content_media_count() {
+               // Get current value without increasing.
+               $content_media_count = wp_increase_content_media_count( 0 );
+
+               // Decrease it by its current value to "reset" it back to 0.
+               wp_increase_content_media_count( - $content_media_count );
+       }
+
+       private function reset_omit_loading_attr_filter() {
+               // Add filter to "reset" omit threshold back to null (unset).
+               add_filter( 'wp_omit_loading_attr_threshold', '__return_null', 100 );
+
+               // Force filter application to re-run.
+               wp_omit_loading_attr_threshold( true );
+
+               // Clean up the above filter.
+               remove_filter( 'wp_omit_loading_attr_threshold', '__return_null', 100 );
+       }
</ins><span class="cx" style="display: block; padding: 0 10px"> }
</span><span class="cx" style="display: block; padding: 0 10px"> 
</span><span class="cx" style="display: block; padding: 0 10px"> /**
</span></span></pre>
</div>
</div>

</body>
</html>