WordPress.org

Make WordPress Core


Ignore:
Timestamp:
11/09/2021 12:34:17 AM (3 weeks ago)
Author:
flixos90
Message:

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 #53675. See #50425.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-includes/media.php

    r51903 r52065  
    10471047        // Add `loading` attribute.
    10481048        if ( wp_lazy_loading_enabled( 'img', 'wp_get_attachment_image' ) ) {
    1049             $default_attr['loading'] = 'lazy';
     1049            $default_attr['loading'] = wp_get_loading_attr_default( 'wp_get_attachment_image' );
    10501050        }
    10511051
     
    18211821    }
    18221822
    1823     foreach ( $images as $image => $attachment_id ) {
    1824         $filtered_image = $image;
    1825 
    1826         // Add 'width' and 'height' attributes if applicable.
    1827         if ( $attachment_id > 0 && false === strpos( $filtered_image, ' width=' ) && false === strpos( $filtered_image, ' height=' ) ) {
    1828             $filtered_image = wp_img_tag_add_width_and_height_attr( $filtered_image, $context, $attachment_id );
    1829         }
    1830 
    1831         // Add 'srcset' and 'sizes' attributes if applicable.
    1832         if ( $attachment_id > 0 && false === strpos( $filtered_image, ' srcset=' ) ) {
    1833             $filtered_image = wp_img_tag_add_srcset_and_sizes_attr( $filtered_image, $context, $attachment_id );
    1834         }
    1835 
    1836         // Add 'loading' attribute if applicable.
    1837         if ( $add_img_loading_attr && false === strpos( $filtered_image, ' loading=' ) ) {
    1838             $filtered_image = wp_img_tag_add_loading_attr( $filtered_image, $context );
    1839         }
    1840 
    1841         if ( $filtered_image !== $image ) {
    1842             $content = str_replace( $image, $filtered_image, $content );
    1843         }
    1844     }
    1845 
    1846     foreach ( $iframes as $iframe => $attachment_id ) {
    1847         $filtered_iframe = $iframe;
    1848 
    1849         // Add 'loading' attribute if applicable.
    1850         if ( $add_iframe_loading_attr && false === strpos( $filtered_iframe, ' loading=' ) ) {
    1851             $filtered_iframe = wp_iframe_tag_add_loading_attr( $filtered_iframe, $context );
    1852         }
    1853 
    1854         if ( $filtered_iframe !== $iframe ) {
    1855             $content = str_replace( $iframe, $filtered_iframe, $content );
     1823    // Iterate through the matches in order of occurrence as it is relevant for whether or not to lazy-load.
     1824    foreach ( $matches as $match ) {
     1825        // Filter an image match.
     1826        if ( isset( $images[ $match[0] ] ) ) {
     1827            $filtered_image = $match[0];
     1828            $attachment_id  = $images[ $match[0] ];
     1829
     1830            // Add 'width' and 'height' attributes if applicable.
     1831            if ( $attachment_id > 0 && false === strpos( $filtered_image, ' width=' ) && false === strpos( $filtered_image, ' height=' ) ) {
     1832                $filtered_image = wp_img_tag_add_width_and_height_attr( $filtered_image, $context, $attachment_id );
     1833            }
     1834
     1835            // Add 'srcset' and 'sizes' attributes if applicable.
     1836            if ( $attachment_id > 0 && false === strpos( $filtered_image, ' srcset=' ) ) {
     1837                $filtered_image = wp_img_tag_add_srcset_and_sizes_attr( $filtered_image, $context, $attachment_id );
     1838            }
     1839
     1840            // Add 'loading' attribute if applicable.
     1841            if ( $add_img_loading_attr && false === strpos( $filtered_image, ' loading=' ) ) {
     1842                $filtered_image = wp_img_tag_add_loading_attr( $filtered_image, $context );
     1843            }
     1844
     1845            if ( $filtered_image !== $match[0] ) {
     1846                $content = str_replace( $match[0], $filtered_image, $content );
     1847            }
     1848        }
     1849
     1850        // Filter an iframe match.
     1851        if ( isset( $iframes[ $match[0] ] ) ) {
     1852            $filtered_iframe = $match[0];
     1853
     1854            // Add 'loading' attribute if applicable.
     1855            if ( $add_iframe_loading_attr && false === strpos( $filtered_iframe, ' loading=' ) ) {
     1856                $filtered_iframe = wp_iframe_tag_add_loading_attr( $filtered_iframe, $context );
     1857            }
     1858
     1859            if ( $filtered_iframe !== $match[0] ) {
     1860                $content = str_replace( $match[0], $filtered_iframe, $content );
     1861            }
    18561862        }
    18571863    }
     
    18701876 */
    18711877function wp_img_tag_add_loading_attr( $image, $context ) {
     1878    // Get loading attribute value to use. This must occur before the conditional check below so that even images that
     1879    // are ineligible for being lazy-loaded are considered.
     1880    $value = wp_get_loading_attr_default( $context );
     1881
    18721882    // Images should have source and dimension attributes for the `loading` attribute to be added.
    18731883    if ( false === strpos( $image, ' src="' ) || false === strpos( $image, ' width="' ) || false === strpos( $image, ' height="' ) ) {
     
    18841894     *
    18851895     * @param string|bool $value   The `loading` attribute value. Returning a falsey value will result in
    1886      *                             the attribute being omitted for the image. Default 'lazy'.
     1896     *                             the attribute being omitted for the image.
    18871897     * @param string      $image   The HTML `img` tag to be filtered.
    18881898     * @param string      $context Additional context about how the function was called or where the img tag is.
    18891899     */
    1890     $value = apply_filters( 'wp_img_tag_add_loading_attr', 'lazy', $image, $context );
     1900    $value = apply_filters( 'wp_img_tag_add_loading_attr', $value, $image, $context );
    18911901
    18921902    if ( $value ) {
     
    19962006    }
    19972007
     2008    // Get loading attribute value to use. This must occur before the conditional check below so that even iframes that
     2009    // are ineligible for being lazy-loaded are considered.
     2010    $value = wp_get_loading_attr_default( $context );
     2011
    19982012    // Iframes should have source and dimension attributes for the `loading` attribute to be added.
    19992013    if ( false === strpos( $iframe, ' src="' ) || false === strpos( $iframe, ' width="' ) || false === strpos( $iframe, ' height="' ) ) {
     
    20102024     *
    20112025     * @param string|bool $value   The `loading` attribute value. Returning a falsey value will result in
    2012      *                             the attribute being omitted for the iframe. Default 'lazy'.
     2026     *                             the attribute being omitted for the iframe.
    20132027     * @param string      $iframe  The HTML `iframe` tag to be filtered.
    20142028     * @param string      $context Additional context about how the function was called or where the iframe tag is.
    20152029     */
    2016     $value = apply_filters( 'wp_iframe_tag_add_loading_attr', 'lazy', $iframe, $context );
     2030    $value = apply_filters( 'wp_iframe_tag_add_loading_attr', $value, $iframe, $context );
    20172031
    20182032    if ( $value ) {
     
    51785192    return compact( 'width', 'height', 'type' );
    51795193}
     5194
     5195/**
     5196 * Gets the default value to use for a `loading` attribute on an element.
     5197 *
     5198 * This function should only be called for a tag and context if lazy-loading is generally enabled.
     5199 *
     5200 * The function usually returns 'lazy', but uses certain heuristics to guess whether the current element is likely to
     5201 * appear above the fold, in which case it returns a boolean `false`, which will lead to the `loading` attribute being
     5202 * omitted on the element. The purpose of this refinement is to avoid lazy-loading elements that are within the initial
     5203 * viewport, which can have a negative performance impact.
     5204 *
     5205 * Under the hood, the function uses {@see wp_increase_content_media_count()} every time it is called for an element
     5206 * within the main content. If the element is the very first content element, the `loading` attribute will be omitted.
     5207 * This default threshold of 1 content element to omit the `loading` attribute for can be customized using the
     5208 * {@see 'wp_omit_loading_attr_threshold'} filter.
     5209 *
     5210 * @since 5.9.0
     5211 *
     5212 * @param string $context Context for the element for which the `loading` attribute value is requested.
     5213 * @return string|bool The default `loading` attribute value. Either 'lazy', 'eager', or a boolean `false`, to indicate
     5214 *                     that the `loading` attribute should be skipped.
     5215 */
     5216function wp_get_loading_attr_default( $context ) {
     5217    // Only elements with 'the_content' or 'the_post_thumbnail' context have special handling.
     5218    if ( 'the_content' !== $context && 'the_post_thumbnail' !== $context ) {
     5219        return 'lazy';
     5220    }
     5221
     5222    // Only elements within the main query loop have special handling.
     5223    if ( is_admin() || ! in_the_loop() || ! is_main_query() ) {
     5224        return 'lazy';
     5225    }
     5226
     5227    // Increase the counter since this is a main query content element.
     5228    $content_media_count = wp_increase_content_media_count();
     5229
     5230    // If the count so far is below the threshold, return `false` so that the `loading` attribute is omitted.
     5231    if ( $content_media_count <= wp_omit_loading_attr_threshold() ) {
     5232        return false;
     5233    }
     5234
     5235    // For elements after the threshold, lazy-load them as usual.
     5236    return 'lazy';
     5237}
     5238
     5239/**
     5240 * Gets the threshold for how many of the first content media elements to not lazy-load.
     5241 *
     5242 * This function runs the {@see 'wp_omit_loading_attr_threshold'} filter, which uses a default threshold value of 1.
     5243 * The filter is only run once per page load, unless the `$force` parameter is used.
     5244 *
     5245 * @since 5.9.0
     5246 *
     5247 * @param bool $force Optional. If set to true, the filter will be (re-)applied even if it already has been before.
     5248 *                    Default false.
     5249 * @return int The number of content media elements to not lazy-load.
     5250 */
     5251function wp_omit_loading_attr_threshold( $force = false ) {
     5252    static $omit_threshold;
     5253
     5254    // This function may be called multiple times. Run the filter only once per page load.
     5255    if ( ! isset( $omit_threshold ) || $force ) {
     5256        /**
     5257         * Filters the threshold for how many of the first content media elements to not lazy-load.
     5258         *
     5259         * For these first content media elements, the `loading` attribute will be omitted. By default, this is the case
     5260         * for only the very first content media element.
     5261         *
     5262         * @since 5.9.0
     5263         *
     5264         * @param int $omit_threshold The number of media elements where the `loading` attribute will not be added. Default 1.
     5265         */
     5266        $omit_threshold = apply_filters( 'wp_omit_loading_attr_threshold', 1 );
     5267    }
     5268
     5269    return $omit_threshold;
     5270}
     5271
     5272/**
     5273 * Increases an internal content media count variable.
     5274 *
     5275 * @since 5.9.0
     5276 * @access private
     5277 *
     5278 * @param int $amount Optional. Amount to increase by. Default 1.
     5279 * @return int The latest content media count, after the increase.
     5280 */
     5281function wp_increase_content_media_count( $amount = 1 ) {
     5282    static $content_media_count = 0;
     5283
     5284    $content_media_count += $amount;
     5285
     5286    return $content_media_count;
     5287}
Note: See TracChangeset for help on using the changeset viewer.