From b6fde03ef7f9a1d89940984f37532d2076cb9e6e Mon Sep 17 00:00:00 2001 From: Felix Arntz Date: Mon, 26 Jun 2023 16:15:12 +0000 Subject: [PATCH] Media: Automatically add `fetchpriority="high"` to hero image to improve load time performance. This changeset adds support for the `fetchpriority` attribute, which is typically added to a single image in each HTML response with a value of "high". This enhances load time performance (also Largest Contentful Paint, or LCP) by telling the browser to prioritize this image for downloading even before the layout of the page has been computed. In lab tests, this has shown to improve LCP performance by ~10% on average. Specifically, `fetchpriority="high"` is added to the first image that satisfies all of the following conditions: * The image is not lazy-loaded, i.e. does not have `loading="lazy"`. * The image does not already have a (conflicting) `fetchpriority` attribute. * The size of of the image (i.e. width * height) is greater than 50,000 squarepixels. While these heuristics are based on several field analyses, there will always be room for optimization. Sites can customize the squarepixel threshold using a new filter `wp_min_priority_img_pixels` which should return an integer for the value. Since the logic for adding `fetchpriority="high"` is heavily intertwined with the logic for adding `loading="lazy"`, yet the features should work decoupled from each other, the majority of code changes in this changeset is refactoring of the existing lazy-loading logic to be reusable. For this purpose, a new function `wp_get_loading_optimization_attributes()` has been introduced which returns an associative array of performance-relevant attributes for a given HTML element. This function replaces `wp_get_loading_attr_default()`, which has been deprecated. As another result of that change, a new function `wp_img_tag_add_loading_optimization_attrs()` replaces the more specific `wp_img_tag_add_loading_attr()`, which has been deprecated as well. See https://make.wordpress.org/core/2023/05/02/proposal-for-enhancing-lcp-image-performance-with-fetchpriority/ for the original proposal and additional context. Props thekt12, joemcgill, spacedmonkey, mukesh27, costdev, 10upsimon. Fixes #58235. git-svn-id: https://develop.svn.wordpress.org/trunk@56037 602fd350-edb4-49c9-b593-d223f7449a82 --- src/wp-includes/deprecated.php | 146 ++++++ src/wp-includes/media.php | 351 ++++++++++--- src/wp-includes/pluggable.php | 23 +- tests/phpunit/tests/media.php | 878 +++++++++++++++++++++++++++++++-- 4 files changed, 1292 insertions(+), 106 deletions(-) diff --git a/src/wp-includes/deprecated.php b/src/wp-includes/deprecated.php index 36be77efdd..1626995290 100644 --- a/src/wp-includes/deprecated.php +++ b/src/wp-includes/deprecated.php @@ -4658,3 +4658,149 @@ function wp_queue_comments_for_comment_meta_lazyload( $comments ) { wp_lazyload_comment_meta( $comment_ids ); } + +/** + * 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 3 content elements to omit the `loading` attribute for can be customized using the + * {@see 'wp_omit_loading_attr_threshold'} filter. + * + * @since 5.9.0 + * @deprecated 6.3.0 Use wp_get_loading_optimization_attributes() instead. + * @see wp_get_loading_optimization_attributes() + * + * @global WP_Query $wp_query WordPress Query object. + * + * @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 ) { + _deprecated_function( __FUNCTION__, '6.3.0', 'wp_get_loading_optimization_attributes' ); + global $wp_query; + + // Skip lazy-loading for the overall block template, as it is handled more granularly. + if ( 'template' === $context ) { + return false; + } + + /* + * Do not lazy-load images in the header block template part, as they are likely above the fold. + * For classic themes, this is handled in the condition below using the 'get_header' action. + */ + $header_area = WP_TEMPLATE_PART_AREA_HEADER; + if ( "template_part_{$header_area}" === $context ) { + return false; + } + + // Special handling for programmatically created image tags. + if ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context ) { + /* + * Skip programmatically created images within post content as they need to be handled together with the other + * images within the post content. + * Without this clause, they would already be counted below which skews the number and can result in the first + * post content image being lazy-loaded only because there are images elsewhere in the post content. + */ + if ( doing_filter( 'the_content' ) ) { + return false; + } + + // Conditionally skip lazy-loading on images before the loop. + if ( + // Only apply for main query but before the loop. + $wp_query->before_loop && $wp_query->is_main_query() + /* + * Any image before the loop, but after the header has started should not be lazy-loaded, + * except when the footer has already started which can happen when the current template + * does not include any loop. + */ + && did_action( 'get_header' ) && ! did_action( 'get_footer' ) + ) { + return false; + } + } + + /* + * The first elements in 'the_content' or 'the_post_thumbnail' should not be lazy-loaded, + * as they are likely above the fold. + */ + if ( 'the_content' === $context || 'the_post_thumbnail' === $context ) { + // 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'; + } + + // Lazy-load by default for any unknown context. + return 'lazy'; +} + +/** + * Adds `loading` attribute to an `img` HTML tag. + * + * @since 5.5.0 + * @deprecated 6.3.0 Use wp_img_tag_add_loading_optimization_attrs() instead. + * @see wp_img_tag_add_loading_optimization_attrs() + * + * @param string $image The HTML `img` tag where the attribute should be added. + * @param string $context Additional context to pass to the filters. + * @return string Converted `img` tag with `loading` attribute added. + */ +function wp_img_tag_add_loading_attr( $image, $context ) { + _deprecated_function( __FUNCTION__, '6.3.0', 'wp_img_tag_add_loading_optimization_attrs' ); + /* + * 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 ); + + // Images should have source and dimension attributes for the `loading` attribute to be added. + if ( ! str_contains( $image, ' src="' ) || ! str_contains( $image, ' width="' ) || ! str_contains( $image, ' height="' ) ) { + return $image; + } + + /** + * Filters the `loading` attribute value to add to an image. Default `lazy`. + * + * Returning `false` or an empty string will not add the attribute. + * Returning `true` will add the default value. + * + * @since 5.5.0 + * + * @param string|bool $value The `loading` attribute value. Returning a falsey value will result in + * the attribute being omitted for the image. + * @param string $image The HTML `img` tag to be filtered. + * @param string $context Additional context about how the function was called or where the img tag is. + */ + $value = apply_filters( 'wp_img_tag_add_loading_attr', $value, $image, $context ); + + if ( $value ) { + if ( ! in_array( $value, array( 'lazy', 'eager' ), true ) ) { + $value = 'lazy'; + } + + return str_replace( ']+>/', $content, $matches, PREG_SET_ORDER ) ) { @@ -1857,10 +1867,8 @@ function wp_filter_content_tags( $content, $context = null ) { $filtered_image = wp_img_tag_add_srcset_and_sizes_attr( $filtered_image, $context, $attachment_id ); } - // Add 'loading' attribute if applicable. - if ( $add_img_loading_attr && ! str_contains( $filtered_image, ' loading=' ) ) { - $filtered_image = wp_img_tag_add_loading_attr( $filtered_image, $context ); - } + // Add loading optimization attributes if applicable. + $filtered_image = wp_img_tag_add_loading_optimization_attrs( $filtered_image, $context ); // Add 'decoding=async' attribute unless a 'decoding' attribute is already present. if ( ! str_contains( $filtered_image, ' decoding=' ) ) { @@ -1914,45 +1922,101 @@ function wp_filter_content_tags( $content, $context = null ) { } /** - * Adds `loading` attribute to an `img` HTML tag. + * Adds optimization attributes to an `img` HTML tag. * - * @since 5.5.0 + * @since 6.3.0 * * @param string $image The HTML `img` tag where the attribute should be added. * @param string $context Additional context to pass to the filters. - * @return string Converted `img` tag with `loading` attribute added. + * @return string Converted `img` tag with optimization attributes added. */ -function wp_img_tag_add_loading_attr( $image, $context ) { - // 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 ); +function wp_img_tag_add_loading_optimization_attrs( $image, $context ) { + $width = preg_match( '/ width=["\']([0-9]+)["\']/', $image, $match_width ) ? (int) $match_width[1] : null; + $height = preg_match( '/ height=["\']([0-9]+)["\']/', $image, $match_height ) ? (int) $match_height[1] : null; + $loading_val = preg_match( '/ loading=["\']([A-Za-z]+)["\']/', $image, $match_loading ) ? $match_loading[1] : null; + $fetchpriority_val = preg_match( '/ fetchpriority=["\']([A-Za-z]+)["\']/', $image, $match_fetchpriority ) ? $match_fetchpriority[1] : null; - // Images should have source and dimension attributes for the `loading` attribute to be added. + /* + * Get loading optimization attributes to use. + * This must occur before the conditional check below so that even images + * that are ineligible for being lazy-loaded are considered. + */ + $optimization_attrs = wp_get_loading_optimization_attributes( + 'img', + array( + 'width' => $width, + 'height' => $height, + 'loading' => $loading_val, + 'fetchpriority' => $fetchpriority_val, + ), + $context + ); + + // Images should have source and dimension attributes for the loading optimization attributes to be added. if ( ! str_contains( $image, ' src="' ) || ! str_contains( $image, ' width="' ) || ! str_contains( $image, ' height="' ) ) { return $image; } - /** - * Filters the `loading` attribute value to add to an image. Default `lazy`. - * - * Returning `false` or an empty string will not add the attribute. - * Returning `true` will add the default value. - * - * @since 5.5.0 - * - * @param string|bool $value The `loading` attribute value. Returning a falsey value will result in - * the attribute being omitted for the image. - * @param string $image The HTML `img` tag to be filtered. - * @param string $context Additional context about how the function was called or where the img tag is. - */ - $value = apply_filters( 'wp_img_tag_add_loading_attr', $value, $image, $context ); + // Retained for backward compatibility. + $loading_attrs_enabled = wp_lazy_loading_enabled( 'img', $context ); - if ( $value ) { - if ( ! in_array( $value, array( 'lazy', 'eager' ), true ) ) { - $value = 'lazy'; + if ( empty( $loading_val ) && $loading_attrs_enabled ) { + /** + * Filters the `loading` attribute value to add to an image. Default `lazy`. + * This filter is added in for backward compatibility. + * + * Returning `false` or an empty string will not add the attribute. + * Returning `true` will add the default value. + * `true` and `false` usage supported for backward compatibility. + * + * @since 5.5.0 + * + * @param string|bool $loading Current value for `loading` attribute for the image. + * @param string $image The HTML `img` tag to be filtered. + * @param string $context Additional context about how the function was called or where the img tag is. + */ + $filtered_loading_attr = apply_filters( + 'wp_img_tag_add_loading_attr', + isset( $optimization_attrs['loading'] ) ? $optimization_attrs['loading'] : false, + $image, + $context + ); + + // Validate the values after filtering. + if ( isset( $optimization_attrs['loading'] ) && ! $filtered_loading_attr ) { + // Unset `loading` attributes if `$filtered_loading_attr` is set to `false`. + unset( $optimization_attrs['loading'] ); + } elseif ( in_array( $filtered_loading_attr, array( 'lazy', 'eager' ), true ) ) { + /* + * If the filter changed the loading attribute to "lazy" when a fetchpriority attribute + * with value "high" is already present, trigger a warning since those two attribute + * values should be mutually exclusive. + * + * The same warning is present in `wp_get_loading_optimization_attributes()`, and here it + * is only intended for the specific scenario where the above filtered caused the problem. + */ + if ( isset( $optimization_attrs['fetchpriority'] ) && 'high' === $optimization_attrs['fetchpriority'] && + ( isset( $optimization_attrs['loading'] ) ? $optimization_attrs['loading'] : false ) !== $filtered_loading_attr && + 'lazy' === $filtered_loading_attr + ) { + _doing_it_wrong( + __FUNCTION__, + __( 'An image should not be lazy-loaded and marked as high priority at the same time.' ), + '6.3.0' + ); + } + + // The filtered value will still be respected. + $optimization_attrs['loading'] = $filtered_loading_attr; } - return str_replace( ' str_contains( $iframe, ' width="' ) ? 100 : null, + 'height' => str_contains( $iframe, ' height="' ) ? 100 : null, + // This function is never called when a 'loading' attribute is already present. + 'loading' => null, + ), + $context + ); // Iframes should have source and dimension attributes for the `loading` attribute to be added. if ( ! str_contains( $iframe, ' src="' ) || ! str_contains( $iframe, ' width="' ) || ! str_contains( $iframe, ' height="' ) ) { return $iframe; } + $value = isset( $optimization_attrs['loading'] ) ? $optimization_attrs['loading'] : false; + /** * Filters the `loading` attribute value to add to an iframe. Default `lazy`. * @@ -5469,45 +5550,102 @@ function wp_get_webp_info( $filename ) { } /** - * Gets the default value to use for a `loading` attribute on an element. + * Gets loading optimization attributes. * - * This function should only be called for a tag and context if lazy-loading is generally enabled. + * This function returns an array of attributes that should be merged into the given attributes array to optimize + * loading performance. Potential attributes returned by this function are: + * - `loading` attribute with a value of "lazy" + * - `fetchpriority` attribute with a value of "high" * - * 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. + * If any of these attributes are already present in the given attributes, they will not be modified. Note that no + * element should have both `loading="lazy"` and `fetchpriority="high"`, so the function will trigger a warning in case + * both attributes are present with those values. * - * 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 3 content elements to omit the `loading` attribute for can be customized using the - * {@see 'wp_omit_loading_attr_threshold'} filter. - * - * @since 5.9.0 + * @since 6.3.0 * * @global WP_Query $wp_query WordPress Query object. * - * @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. + * @param string $tag_name The tag name. + * @param array $attr Array of the attributes for the tag. + * @param string $context Context for the element for which the loading optimization attribute is requested. + * @return array Loading optimization attributes. */ -function wp_get_loading_attr_default( $context ) { +function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { global $wp_query; - // Skip lazy-loading for the overall block template, as it is handled more granularly. + /* + * Closure for postprocessing logic. + * It is here to avoid duplicate logic in many places below, without having + * to introduce a very specific private global function. + */ + $postprocess = static function( $loading_attributes, $with_fetchpriority = false ) use ( $tag_name, $attr, $context ) { + // Potentially add `fetchpriority="high"`. + if ( $with_fetchpriority ) { + $loading_attributes = wp_maybe_add_fetchpriority_high_attr( $loading_attributes, $tag_name, $attr ); + } + // Potentially strip `loading="lazy"` if the feature is disabled. + if ( isset( $loading_attributes['loading'] ) && ! wp_lazy_loading_enabled( $tag_name, $context ) ) { + unset( $loading_attributes['loading'] ); + } + return $loading_attributes; + }; + + $loading_attrs = array(); + + /* + * Skip lazy-loading for the overall block template, as it is handled more granularly. + * The skip is also applicable for `fetchpriority`. + */ if ( 'template' === $context ) { - return false; + return $loading_attrs; } - // Do not lazy-load images in the header block template part, as they are likely above the fold. - // For classic themes, this is handled in the condition below using the 'get_header' action. + // For now this function only supports images and iframes. + if ( 'img' !== $tag_name && 'iframe' !== $tag_name ) { + return $loading_attrs; + } + + // For any resources, width and height must be provided, to avoid layout shifts. + if ( ! isset( $attr['width'], $attr['height'] ) ) { + return $loading_attrs; + } + + if ( isset( $attr['loading'] ) ) { + /* + * While any `loading` value could be set in `$loading_attrs`, for + * consistency we only do it for `loading="lazy"` since that is the + * only possible value that WordPress core would apply on its own. + */ + if ( 'lazy' === $attr['loading'] ) { + $loading_attrs['loading'] = 'lazy'; + if ( isset( $attr['fetchpriority'] ) && 'high' === $attr['fetchpriority'] ) { + _doing_it_wrong( + __FUNCTION__, + __( 'An image should not be lazy-loaded and marked as high priority at the same time.' ), + '6.3.0' + ); + } + } + + return $postprocess( $loading_attrs, true ); + } + + // An image with `fetchpriority="high"` cannot be assigned `loading="lazy"` at the same time. + if ( isset( $attr['fetchpriority'] ) && 'high' === $attr['fetchpriority'] ) { + return $postprocess( $loading_attrs, true ); + } + + /* + * Do not lazy-load images in the header block template part, as they are likely above the fold. + * For classic themes, this is handled in the condition below using the 'get_header' action. + */ $header_area = WP_TEMPLATE_PART_AREA_HEADER; if ( "template_part_{$header_area}" === $context ) { - return false; + return $postprocess( $loading_attrs, true ); } // Special handling for programmatically created image tags. - if ( ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context ) ) { + if ( 'the_post_thumbnail' === $context || 'wp_get_attachment_image' === $context ) { /* * Skip programmatically created images within post content as they need to be handled together with the other * images within the post content. @@ -5515,7 +5653,7 @@ function wp_get_loading_attr_default( $context ) { * post content image being lazy-loaded only because there are images elsewhere in the post content. */ if ( doing_filter( 'the_content' ) ) { - return false; + return $postprocess( $loading_attrs, true ); } // Conditionally skip lazy-loading on images before the loop. @@ -5529,7 +5667,7 @@ function wp_get_loading_attr_default( $context ) { */ && did_action( 'get_header' ) && ! did_action( 'get_footer' ) ) { - return false; + return $postprocess( $loading_attrs, true ); } } @@ -5540,23 +5678,23 @@ function wp_get_loading_attr_default( $context ) { if ( 'the_content' === $context || 'the_post_thumbnail' === $context ) { // Only elements within the main query loop have special handling. if ( is_admin() || ! in_the_loop() || ! is_main_query() ) { - return 'lazy'; + $loading_attrs['loading'] = 'lazy'; + return $postprocess( $loading_attrs, false ); } // 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 the count so far is below the threshold, `loading` attribute is omitted. if ( $content_media_count <= wp_omit_loading_attr_threshold() ) { - return false; + // The first largest image will still get `fetchpriority='high'`. + return $postprocess( $loading_attrs, true ); } - - // For elements after the threshold, lazy-load them as usual. - return 'lazy'; } // Lazy-load by default for any unknown context. - return 'lazy'; + $loading_attrs['loading'] = 'lazy'; + return $postprocess( $loading_attrs, false ); } /** @@ -5609,3 +5747,76 @@ function wp_increase_content_media_count( $amount = 1 ) { return $content_media_count; } + +/** + * Determines whether to add `fetchpriority='high'` to loading attributes. + * + * @since 6.3.0 + * @access private + * + * @param array $loading_attrs Array of the loading optimization attributes for the element. + * @param string $tag_name The tag name. + * @param array $attr Array of the attributes for the element. + * @return array Updated loading optimization attributes for the element. + */ +function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ) { + // For now, adding `fetchpriority="high"` is only supported for images. + if ( 'img' !== $tag_name ) { + return $loading_attrs; + } + + if ( isset( $attr['fetchpriority'] ) ) { + /* + * While any `fetchpriority` value could be set in `$loading_attrs`, + * for consistency we only do it for `fetchpriority="high"` since that + * is the only possible value that WordPress core would apply on its + * own. + */ + if ( 'high' === $attr['fetchpriority'] ) { + $loading_attrs['fetchpriority'] = 'high'; + wp_high_priority_element_flag( false ); + } + return $loading_attrs; + } + + // Lazy-loading and `fetchpriority="high"` are mutually exclusive. + if ( isset( $loading_attrs['loading'] ) && 'lazy' === $loading_attrs['loading'] ) { + return $loading_attrs; + } + + if ( ! wp_high_priority_element_flag() ) { + return $loading_attrs; + } + + /** + * Filters the minimum square-pixels threshold for an image to be eligible as the high-priority image. + * + * @since 6.3.0 + * + * @param int $threshold Minimum square-pixels threshold. Default 50000. + */ + $wp_min_priority_img_pixels = apply_filters( 'wp_min_priority_img_pixels', 50000 ); + if ( $wp_min_priority_img_pixels <= $attr['width'] * $attr['height'] ) { + $loading_attrs['fetchpriority'] = 'high'; + wp_high_priority_element_flag( false ); + } + return $loading_attrs; +} + +/** + * Accesses a flag that indicates if an element is a possible candidate for `fetchpriority='high'`. + * + * @since 6.3.0 + * @access private + * + * @param bool $value Optional. Used to change the static variable. Default null. + * @return bool Returns true if high-priority element was marked already, otherwise false. + */ +function wp_high_priority_element_flag( $value = null ) { + static $high_priority_element = true; + + if ( is_bool( $value ) ) { + $high_priority_element = $value; + } + return $high_priority_element; +} diff --git a/src/wp-includes/pluggable.php b/src/wp-includes/pluggable.php index 73d7d20748..45ee504fda 100644 --- a/src/wp-includes/pluggable.php +++ b/src/wp-includes/pluggable.php @@ -2815,14 +2815,11 @@ if ( ! function_exists( 'get_avatar' ) ) : 'class' => null, 'force_display' => false, 'loading' => null, + 'fetchpriority' => null, 'extra_attr' => '', 'decoding' => 'async', ); - if ( wp_lazy_loading_enabled( 'img', 'get_avatar' ) ) { - $defaults['loading'] = wp_get_loading_attr_default( 'get_avatar' ); - } - if ( empty( $args ) ) { $args = array(); } @@ -2840,6 +2837,11 @@ if ( ! function_exists( 'get_avatar' ) ) : $args['width'] = $args['size']; } + // Update args with loading optimized attributes. + $loading_optimization_attr = wp_get_loading_optimization_attributes( 'img', $args, 'get_avatar' ); + + $args = array_merge( $args, $loading_optimization_attr ); + if ( is_object( $id_or_email ) && isset( $id_or_email->comment_ID ) ) { $id_or_email = get_comment( $id_or_email ); } @@ -2892,7 +2894,7 @@ if ( ! function_exists( 'get_avatar' ) ) : } } - // Add `loading` and `decoding` attributes. + // Add `loading`, `fetchpriority` and `decoding` attributes. $extra_attr = $args['extra_attr']; if ( in_array( $args['loading'], array( 'lazy', 'eager' ), true ) @@ -2915,6 +2917,17 @@ if ( ! function_exists( 'get_avatar' ) ) : $extra_attr .= "decoding='{$args['decoding']}'"; } + // Add support for `fetchpriority`. + if ( in_array( $args['fetchpriority'], array( 'high', 'low', 'auto' ), true ) + && ! preg_match( '/\bfetchpriority\s*=/', $extra_attr ) + ) { + if ( ! empty( $extra_attr ) ) { + $extra_attr .= ' '; + } + + $extra_attr .= "fetchpriority='{$args['fetchpriority']}'"; + } + $avatar = sprintf( "%s", esc_attr( $args['alt'] ), diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 3c42f01290..ba3fe92fef 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -76,13 +76,14 @@ CAP; } /** - * Ensures that the static content media count and related filter are reset between tests. + * Ensures that the static content media count, fetchpriority element flag and related filter are reset between tests. */ public function set_up() { parent::set_up(); $this->reset_content_media_count(); $this->reset_omit_loading_attr_filter(); + $this->reset_high_priority_element_flag(); } public function test_img_caption_shortcode_added() { @@ -2289,7 +2290,7 @@ EOF; */ public function test_wp_filter_content_tags_srcset_sizes_wrong() { $img = get_image_tag( self::$large_id, '', '', '', 'medium' ); - $img = wp_img_tag_add_loading_attr( $img, 'test' ); + $img = wp_img_tag_add_loading_optimization_attrs( $img, 'test' ); $img = wp_img_tag_add_decoding_attr( $img, 'the_content' ); // Replace the src URL. @@ -2304,7 +2305,7 @@ EOF; public function test_wp_filter_content_tags_srcset_sizes_with_preexisting_srcset() { // Generate HTML and add a dummy srcset attribute. $img = get_image_tag( self::$large_id, '', '', '', 'medium' ); - $img = wp_img_tag_add_loading_attr( $img, 'test' ); + $img = wp_img_tag_add_loading_optimization_attrs( $img, 'test' ); $img = wp_img_tag_add_decoding_attr( $img, 'the_content' ); $img = preg_replace( '|]+) />|', '', $img ); @@ -2449,7 +2450,7 @@ EOF; // Build HTML for the editor. $img = get_image_tag( self::$large_id, '', '', '', 'medium' ); - $img = wp_img_tag_add_loading_attr( $img, 'test' ); + $img = wp_img_tag_add_loading_optimization_attrs( $img, 'test' ); $img_https = str_replace( 'http://', 'https://', $img ); $img_relative = str_replace( 'http://', '//', $img ); @@ -2990,6 +2991,7 @@ EOF; * @ticket 44427 * @ticket 50367 * @ticket 50756 + * @ticket 58235 * @requires function imagejpeg */ public function test_wp_filter_content_tags_loading_lazy() { @@ -3004,13 +3006,13 @@ EOF; $iframe = ''; $iframe_no_width_height = ''; - $lazy_img = wp_img_tag_add_loading_attr( $img, 'test' ); - $lazy_img_xhtml = wp_img_tag_add_loading_attr( $img_xhtml, 'test' ); - $lazy_img_html5 = wp_img_tag_add_loading_attr( $img_html5, 'test' ); + $lazy_img = wp_img_tag_add_loading_optimization_attrs( $img, 'test' ); + $lazy_img_xhtml = wp_img_tag_add_loading_optimization_attrs( $img_xhtml, 'test' ); + $lazy_img_html5 = wp_img_tag_add_loading_optimization_attrs( $img_html5, 'test' ); $lazy_iframe = wp_iframe_tag_add_loading_attr( $iframe, 'test' ); // The following should not be modified because there already is a 'loading' attribute. - $img_eager = str_replace( ' />', ' loading="eager" />', $img ); + $img_eager = str_replace( ' />', ' loading="eager" fetchpriority="high" />', $img ); $iframe_eager = str_replace( '">', '" loading="eager">', $iframe ); $content = ' @@ -3069,10 +3071,11 @@ EOF; /** * @ticket 44427 * @ticket 50756 + * @ticket 58235 */ public function test_wp_filter_content_tags_loading_lazy_opted_in() { $img = get_image_tag( self::$large_id, '', '', '', 'medium' ); - $lazy_img = wp_img_tag_add_loading_attr( $img, 'test' ); + $lazy_img = wp_img_tag_add_loading_optimization_attrs( $img, 'test' ); $lazy_img = wp_img_tag_add_decoding_attr( $lazy_img, 'the_content' ); $iframe = ''; $lazy_iframe = wp_iframe_tag_add_loading_attr( $iframe, 'test' ); @@ -3127,6 +3130,9 @@ EOF; /** * @ticket 44427 * @ticket 50367 + * + * @expectedDeprecated wp_img_tag_add_loading_attr + * @expectedDeprecated wp_get_loading_attr_default */ public function test_wp_img_tag_add_loading_attr() { $img = ' width='; @@ -3138,6 +3144,9 @@ EOF; /** * @ticket 44427 * @ticket 50367 + * + * @expectedDeprecated wp_img_tag_add_loading_attr + * @expectedDeprecated wp_get_loading_attr_default */ public function test_wp_img_tag_add_loading_attr_without_src() { $img = ' width='; @@ -3149,6 +3158,9 @@ EOF; /** * @ticket 44427 * @ticket 50367 + * + * @expectedDeprecated wp_img_tag_add_loading_attr + * @expectedDeprecated wp_get_loading_attr_default */ public function test_wp_img_tag_add_loading_attr_with_single_quotes() { $img = " width="; @@ -3288,6 +3300,93 @@ EOF; $this->assertStringNotContainsString( ' loading=', $img ); } + /** + * @ticket 58235 + * + * @covers ::wp_get_attachment_image + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_attachment_image_fetchpriority_not_present_by_default() { + $img = wp_get_attachment_image( self::$large_id ); + + $this->assertStringNotContainsString( ' fetchpriority="high"', $img ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_get_attachment_image + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_attachment_image_fetchpriority_high_when_not_lazy_loaded() { + $img = wp_get_attachment_image( self::$large_id, 'large', false, array( 'loading' => false ) ); + + $this->assertStringContainsString( ' fetchpriority="high"', $img ); + } + + /** + * @ticket 58235 + * + * @dataProvider data_provider_fetchpriority_values + * + * @covers ::wp_get_attachment_image + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_attachment_image_fetchpriority_original_value_respected( $value ) { + $img = wp_get_attachment_image( + self::$large_id, + 'large', + false, + array( + 'loading' => false, + 'fetchpriority' => $value, + ) + ); + + $this->assertStringContainsString( ' fetchpriority="' . $value . '"', $img ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_provider_fetchpriority_values() { + return self::text_array_to_dataprovider( array( 'high', 'low', 'auto' ) ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_get_attachment_image + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_attachment_image_fetchpriority_stripped_when_false() { + $img = wp_get_attachment_image( + self::$large_id, + 'large', + false, + array( + 'loading' => false, + 'fetchpriority' => false, + ) + ); + + $this->assertStringNotContainsString( ' fetchpriority=', $img ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_get_attachment_image + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_attachment_image_fetchpriority_high_prevents_lazy_loading() { + $img = wp_get_attachment_image( self::$large_id, 'large', false, array( 'fetchpriority' => 'high' ) ); + + $this->assertStringNotContainsString( ' loading="lazy"', $img ); + } + /** * @ticket 57086 * @@ -3564,6 +3663,8 @@ EOF; * * @covers ::wp_get_loading_attr_default * + * @expectedDeprecated wp_get_loading_attr_default + * * @dataProvider data_wp_get_loading_attr_default * * @param string $context @@ -3587,8 +3688,10 @@ EOF; // Set as main query. $this->set_main_query( $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. + /* + * 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` in the main query for first three element. @@ -3617,8 +3720,15 @@ EOF; /** * @ticket 53675 + * @ticket 58235 */ public function test_wp_omit_loading_attr_threshold_filter() { + // Using a smaller image here. + $attr = array( + 'width' => 100, + 'height' => 100, + ); + $query = $this->get_new_wp_query_for_published_post(); $this->set_main_query( $query ); @@ -3630,25 +3740,37 @@ EOF; // Due to the filter, now the first five elements should not be lazy-loaded, i.e. return `false`. for ( $i = 0; $i < 5; $i++ ) { - $this->assertFalse( wp_get_loading_attr_default( 'the_content' ) ); + $this->assertEmpty( + wp_get_loading_optimization_attributes( 'img', $attr, 'the_content' ), + 'Expected second image to not be lazy-loaded.' + ); } // For following elements, lazy-load them again. - $this->assertSame( 'lazy', wp_get_loading_attr_default( 'the_content' ) ); + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, 'the_content' ) + ); } } /** * @ticket 53675 + * @ticket 58235 + * + * @covers ::wp_filter_content_tags + * @covers ::wp_img_tag_add_loading_optimization_attrs + * @covers ::wp_get_loading_optimization_attributes */ - public function test_wp_filter_content_tags_with_wp_get_loading_attr_default() { + public function test_wp_filter_content_tags_with_loading_optimization_attrs() { $img1 = get_image_tag( self::$large_id, '', '', '', 'large' ); $iframe1 = ''; $img2 = get_image_tag( self::$large_id, '', '', '', 'medium' ); $img3 = get_image_tag( self::$large_id, '', '', '', 'thumbnail' ); $iframe2 = ''; - $lazy_img2 = wp_img_tag_add_loading_attr( $img2, 'the_content' ); - $lazy_img3 = wp_img_tag_add_loading_attr( $img3, 'the_content' ); + $prio_img1 = str_replace( ' src=', ' fetchpriority="high" src=', $img1 ); + $lazy_img2 = wp_img_tag_add_loading_optimization_attrs( $img2, 'the_content' ); + $lazy_img3 = wp_img_tag_add_loading_optimization_attrs( $img3, 'the_content' ); $lazy_iframe2 = wp_iframe_tag_add_loading_attr( $iframe2, 'the_content' ); // Use a threshold of 2. @@ -3656,7 +3778,7 @@ EOF; // 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; + $content_expected = $prio_img1 . $iframe1 . $lazy_img2 . $lazy_img3 . $lazy_iframe2; $content_expected = wp_img_tag_add_decoding_attr( $content_expected, 'the_content' ); $query = $this->get_new_wp_query_for_published_post(); @@ -3705,6 +3827,8 @@ EOF; * * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop * + * @expectedDeprecated wp_get_loading_attr_default + * * @param string $context Context for the element for which the `loading` attribute value is requested. */ public function test_wp_get_loading_attr_default_before_loop_if_not_main_query( $context ) { @@ -3727,6 +3851,8 @@ EOF; * * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop * + * @expectedDeprecated wp_get_loading_attr_default + * * @param string $context Context for the element for which the `loading` attribute value is requested. */ public function test_wp_get_loading_attr_default_before_loop_in_main_query_but_header_not_called( $context ) { @@ -3748,6 +3874,8 @@ EOF; * * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop * + * @expectedDeprecated wp_get_loading_attr_default + * * @param string $context Context for the element for which the `loading` attribute value is requested. */ public function test_wp_get_loading_attr_default_before_loop_if_main_query( $context ) { @@ -3769,6 +3897,8 @@ EOF; * * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop * + * @expectedDeprecated wp_get_loading_attr_default + * * @param string $context Context for the element for which the `loading` attribute value is requested. */ public function test_wp_get_loading_attr_default_after_loop( $context ) { @@ -3794,6 +3924,8 @@ EOF; * * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop * + * @expectedDeprecated wp_get_loading_attr_default + * * @param string $context Context for the element for which the `loading` attribute value is requested. */ public function test_wp_get_loading_attr_default_no_loop( $context ) { @@ -3828,9 +3960,11 @@ EOF; * * @ticket 56930 * @ticket 58548 + * @ticket 58235 * * @covers ::wp_filter_content_tags - * @covers ::wp_get_loading_attr_default + * @covers ::wp_img_tag_add_loading_optimization_attrs + * @covers ::wp_get_loading_optimization_attributes */ public function test_wp_filter_content_tags_does_not_lazy_load_first_image_in_block_theme() { global $_wp_current_template_content, $wp_query, $wp_the_query, $post; @@ -3842,11 +3976,12 @@ EOF; $img1 = get_image_tag( self::$large_id, '', '', '', 'large' ); $img2 = get_image_tag( self::$large_id, '', '', '', 'medium' ); - $lazy_img2 = wp_img_tag_add_loading_attr( $img2, 'the_content' ); + $prio_img1 = str_replace( ' src=', ' fetchpriority="high" src=', $img1 ); + $lazy_img2 = wp_img_tag_add_loading_optimization_attrs( $img2, 'the_content' ); // Only the second image should be lazy-loaded. $post_content = $img1 . $img2; - $expected_content = wpautop( $img1 . $lazy_img2 ); + $expected_content = wpautop( $prio_img1 . $lazy_img2 ); // Update the post to test with so that it has the above post content. wp_update_post( @@ -3873,9 +4008,11 @@ EOF; * * @ticket 56930 * @ticket 58548 + * @ticket 58235 * * @covers ::wp_filter_content_tags - * @covers ::wp_get_loading_attr_default + * @covers ::wp_img_tag_add_loading_optimization_attrs + * @covers ::wp_get_loading_optimization_attributes */ public function test_wp_filter_content_tags_does_not_lazy_load_first_featured_image_in_block_theme() { global $_wp_current_template_content, $wp_query, $wp_the_query, $post; @@ -3893,12 +4030,22 @@ EOF; $this->force_omit_loading_attr_threshold( 1 ); $content_img = get_image_tag( self::$large_id, '', '', '', 'large' ); - $lazy_content_img = wp_img_tag_add_loading_attr( $content_img, 'the_content' ); + $lazy_content_img = wp_img_tag_add_loading_optimization_attrs( $content_img, 'the_content' ); // The featured image should not be lazy-loaded as it is the first image. $featured_image_id = self::$large_id; update_post_meta( self::$post_ids['publish'], '_thumbnail_id', $featured_image_id ); - $expected_featured_image = '
' . get_the_post_thumbnail( self::$post_ids['publish'], 'post-thumbnail', array( 'loading' => false ) ) . '
'; + $expected_featured_image = '
' . get_the_post_thumbnail( + self::$post_ids['publish'], + 'post-thumbnail', + array( + 'loading' => false, + 'fetchpriority' => 'high', + ) + ) . '
'; + + // Reset high priority flag as the forced `fetchpriority="high"` above already modified it. + $this->reset_high_priority_element_flag(); // The post content image should be lazy-loaded since the featured image appears above. $post_content = $content_img; @@ -3912,7 +4059,6 @@ EOF; 'post_content_filtered' => $post_content, ) ); - $wp_query = new WP_Query( array( 'p' => self::$post_ids['publish'] ) ); $wp_the_query = $wp_query; $post = get_post( self::$post_ids['publish'] ); @@ -3928,9 +4074,11 @@ EOF; * in a "Header" template part. * * @ticket 56930 + * @ticket 58235 * * @covers ::wp_filter_content_tags - * @covers ::wp_get_loading_attr_default + * @covers ::wp_img_tag_add_loading_optimization_attrs + * @covers ::wp_get_loading_optimization_attributes */ public function test_wp_filter_content_tags_does_not_lazy_load_images_in_header() { global $_wp_current_template_content; @@ -3941,6 +4089,9 @@ EOF; // Use a single image for each header and footer template parts. $header_img = get_image_tag( self::$large_id, '', '', '', 'large' ); + // Since header_img is qualified candidate for LCP, fetchpriority high is applied to it. + $header_img = str_replace( ''; - $expected_template_content .= '
' . wp_img_tag_add_loading_attr( $footer_img, 'force-lazy' ) . '
'; + $expected_template_content .= '
' . wp_img_tag_add_loading_optimization_attrs( $footer_img, 'force-lazy' ) . '
'; $html = get_the_block_template_html(); $this->assertSame( '
' . $expected_template_content . '
', $html ); @@ -3977,15 +4128,29 @@ EOF; /** * @ticket 58089 + * @ticket 58235 * * @covers ::wp_filter_content_tags - * @covers ::wp_get_loading_attr_default + * @covers ::wp_get_loading_optimization_attributes */ public function test_wp_filter_content_tags_does_not_lazy_load_special_images_within_the_content() { global $wp_query, $wp_the_query; // Force no lazy-loading on the image tag expected in the content. - $expected_content = wpautop( wp_get_attachment_image( self::$large_id, 'large', false, array( 'loading' => false ) ) ); + $expected_content = wpautop( + wp_get_attachment_image( + self::$large_id, + 'large', + false, + array( + 'loading' => false, + 'fetchpriority' => 'high', + ) + ) + ); + + // Reset high priority flag as the forced `fetchpriority="high"` above already modified it. + $this->reset_high_priority_element_flag(); // Overwrite post content with an image. add_filter( @@ -4023,6 +4188,8 @@ EOF; * * @covers ::wp_get_loading_attr_default * + * @expectedDeprecated wp_get_loading_attr_default + * * @dataProvider data_special_contexts_for_the_content * * @param string $context Context for the element for which the `loading` attribute value is requested. @@ -4038,6 +4205,8 @@ EOF; * * @covers ::wp_get_loading_attr_default * + * @expectedDeprecated wp_get_loading_attr_default + * * @dataProvider data_special_contexts_for_the_content * * @param string $context Context for the element for which the `loading` attribute value is requested. @@ -4069,6 +4238,313 @@ EOF; ); } + /** + * Tests that wp_get_loading_attr_default() returns the expected loading attribute value. + * + * @ticket 53675 + * @ticket 56930 + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_attr_default + * + * @param string $context + */ + public function test_wp_get_loading_optimization_attributes( $context ) { + $attr = $this->get_width_height_for_high_priority(); + + // Return 'lazy' by default. + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, 'test' ) + ); + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, 'wp_get_attachment_image' ) + ); + + // Return 'lazy' if not in the loop or the main query. + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + $query = $this->get_new_wp_query_for_published_post(); + + while ( have_posts() ) { + the_post(); + + // Return 'lazy' if in the loop but not in the main query. + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + // Set as main query. + $this->set_main_query( $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( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, 'wp_get_attachment_image' ) + ); + + // First three element are not lazy loaded. However, first image is loaded with fetchpriority high. + $this->assertSame( + array( 'fetchpriority' => 'high' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + "Expected first image to not be lazy-loaded. First large image get's high fetchpriority." + ); + $this->assertEmpty( + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'Expected second image to not be lazy-loaded.' + ); + $this->assertEmpty( + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'Expected third image to not be lazy-loaded.' + ); + + // Return 'lazy' if in the loop and in the main query for any subsequent elements. + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + // Yes, for all subsequent elements. + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + } + } + + /** + * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value before loop but after get_header if not main query. + * + * @ticket 58211 + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_optimization_attributes_before_loop_if_not_main_query( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + + do_action( 'get_header' ); + + $attr = $this->get_width_height_for_high_priority(); + + // Lazy if not main query. + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + } + + /** + * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value before loop but after get_header in main query but header was not called. + * + * @ticket 58211 + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_optimization_attributes_before_loop_in_main_query_but_header_not_called( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + + $attr = $this->get_width_height_for_high_priority(); + + // Lazy if header not called. + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + } + + /** + * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value before loop but after get_header for main query. + * + * @ticket 58211 + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_optimization_attributes_before_loop_if_main_query( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + do_action( 'get_header' ); + + $attr = $this->get_width_height_for_high_priority(); + + // First image is loaded with high fetchpriority. + $this->assertSame( + array( 'fetchpriority' => 'high' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'Expected first image to not be lazy-loaded. First large image is loaded with high fetchpriority.' + ); + } + + /** + * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value after get_header and after loop. + * + * @ticket 58211 + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_optimization_attributes_after_loop( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + + do_action( 'get_header' ); + + while ( have_posts() ) { + the_post(); + } + + $attr = $this->get_width_height_for_high_priority(); + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + } + + /** + * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute if no loop. + * + * @ticket 58211 + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_attr_default_before_and_no_loop + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_optimization_attributes_no_loop( $context ) { + global $wp_query; + + $wp_query = $this->get_new_wp_query_for_published_post(); + $this->set_main_query( $wp_query ); + + // Ensure header and footer is called. + do_action( 'get_header' ); + do_action( 'get_footer' ); + + $attr = $this->get_width_height_for_high_priority(); + + // Load lazy if the there is no loop and footer was called. + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + } + + /** + * Tests that wp_get_loading_optimization_attributes() returns 'lazy' for special contexts when they're used outside of 'the_content' filter. + * + * @ticket 58089 + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_special_contexts_for_the_content + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_optimization_attributes_should_return_lazy_for_special_contexts_outside_of_the_content( $context ) { + $attr = $this->get_width_height_for_high_priority(); + $this->assertSame( + array( 'loading' => 'lazy' ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + } + + /** + * Tests that wp_get_loading_optimization_attributes() returns false for special contexts when they're used within 'the_content' filter. + * + * @ticket 58089 + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_special_contexts_for_the_content + * + * @param string $context Context for the element for which the `loading` attribute value is requested. + */ + public function test_wp_get_loading_optimization_attributes_should_return_false_for_special_contexts_within_the_content( $context ) { + remove_all_filters( 'the_content' ); + + $result = null; + add_filter( + 'the_content', + function( $content ) use ( &$result, $context ) { + $attr = $this->get_width_height_for_high_priority(); + $result = wp_get_loading_optimization_attributes( 'img', $attr, $context ); + return $content; + } + ); + apply_filters( 'the_content', '' ); + + $this->assertSame( + array( 'fetchpriority' => 'high' ), + $result, + 'First large image is loaded with high fetchpriority.' + ); + } + + /** + * @ticket 44427 + * @ticket 50367 + * @ticket 58235 + */ + public function test_wp_img_tag_add_loading_optimization_attrs() { + $img = ' width='; + $img = wp_img_tag_add_loading_optimization_attrs( $img, 'test' ); + + $this->assertStringContainsString( ' loading="lazy"', $img ); + } + + /** + * @ticket 44427 + * @ticket 50367 + * @ticket 58235 + */ + public function test_wp_img_tag_add_loading_optimization_attrs_without_src() { + $img = ''; + $img = wp_img_tag_add_loading_optimization_attrs( $img, 'test' ); + + $this->assertStringNotContainsString( ' loading=', $img ); + } + /** * Tests that the content media count is not affected by `the_excerpt()` calls for posts that contain images. * @@ -4120,6 +4596,7 @@ EOF; * that featured image not being lazy-loaded, since the images in the post content aren't displayed in the excerpt. * * @ticket 56588 + * @ticket 58235 * * @covers ::wp_trim_excerpt */ @@ -4131,6 +4608,7 @@ EOF; * then use a post that contains exactly 2 images. */ $this->force_omit_loading_attr_threshold( 2 ); + $post_content = ''; $post_content .= '

Some text.

'; $post_content .= ''; @@ -4144,7 +4622,17 @@ EOF; $featured_image_id = self::$large_id; update_post_meta( $post_id, '_thumbnail_id', $featured_image_id ); - $expected_image_tag = get_the_post_thumbnail( $post_id, 'post-thumbnail', array( 'loading' => false ) ); + $expected_image_tag = get_the_post_thumbnail( + $post_id, + 'post-thumbnail', + array( + 'loading' => false, + 'fetchpriority' => 'high', + ) + ); + + // Reset high priority flag as the forced `fetchpriority="high"` above already modified it. + $this->reset_high_priority_element_flag(); $wp_query = new WP_Query( array( 'post__in' => array( $post_id ) ) ); $wp_the_query = $wp_query; @@ -4180,6 +4668,10 @@ EOF; remove_filter( 'wp_omit_loading_attr_threshold', '__return_null', 100 ); } + private function reset_high_priority_element_flag() { + wp_high_priority_element_flag( true ); + } + /** * Test that generated files with the `image_editor_output_format` applied use the correct * quality level based on their mime type. @@ -4276,7 +4768,7 @@ EOF; * * @ticket 58212 * - * @covers ::wp_get_attachment_image() + * @covers ::wp_get_attachment_image */ public function test_wp_get_attachment_image_context_filter_default() { $last_context = ''; @@ -4291,7 +4783,7 @@ EOF; * * @ticket 58212 * - * @covers ::wp_get_attachment_image() + * @covers ::wp_get_attachment_image */ public function test_wp_get_attachment_image_context_filter_value_is_passed_correctly() { $last_context = ''; @@ -4309,6 +4801,298 @@ EOF; $this->assertSame( 'my_custom_context', $last_context ); } + /** + * Tests tag restriction for `wp_get_loading_optimization_attributes()`. + * + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_optimization_attributes_min_required_attrs + * + * @param string $tag_name The tag name. + * @param string $attr Element attributes. + * @param array $expected Expected return value. + * @param string $message Message to display if the test fails. + */ + public function test_wp_get_loading_optimization_attributes_min_required_attrs( $tag_name, $attr, $expected, $message ) { + $context = 'the_post_thumbnail'; + $this->assertSame( wp_get_loading_optimization_attributes( $tag_name, $attr, $context ), $expected, $message ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_get_loading_optimization_attributes_min_required_attrs() { + return array( + 'img_with_min_attrs' => array( + 'img', + array( + 'width' => 100, + 'height' => 100, + ), + array( 'loading' => 'lazy' ), + 'Expected default `loading="lazy"`.', + ), + 'img_without_height' => array( + 'img', + array( 'width' => 100 ), + array(), + 'Expected blank array as height is required.', + ), + 'img_without_width' => array( + 'img', + array( 'height' => 100 ), + array(), + 'Expected blank array as width is required.', + ), + ); + } + + /** + * Tests tag restriction for `wp_get_loading_optimization_attributes()`. + * + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_optimization_attributes_check_allowed_tags + * + * @param string $tag_name The tag name. + * @param array $expected Expected return value. + * @param string $message Message to display if the test fails. + */ + public function test_wp_get_loading_optimization_attributes_check_allowed_tags( $tag_name, $expected, $message ) { + $attr = $this->get_width_height_for_high_priority(); + $context = 'the_post_thumbnail'; + $this->assertSame( wp_get_loading_optimization_attributes( $tag_name, $attr, $context ), $expected, $message ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_get_loading_optimization_attributes_check_allowed_tags() { + return array( + 'img' => array( + 'img', + array( 'loading' => 'lazy' ), + 'Expected `loading="lazy"` for the img.', + ), + 'iframe' => array( + 'iframe', + array( + 'loading' => 'lazy', + ), + 'Expected `loading="lazy"` for the iframe.', + ), + 'video' => + array( + 'video', + array(), + 'Function should return empty array as video tag is not supported.', + ), + ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_loading_optimization_attributes_skip_for_block_template() { + $attr = $this->get_width_height_for_high_priority(); + + // Skip logic if context is `template`. + $this->assertSame( + array(), + wp_get_loading_optimization_attributes( 'img', $attr, 'template' ), + 'Skip logic and return blank array for block template.' + ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_loading_optimization_attributes_header_block_template() { + $attr = $this->get_width_height_for_high_priority(); + + // Skip logic if context is `template`. + $this->assertSame( + array( 'fetchpriority' => 'high' ), + wp_get_loading_optimization_attributes( 'img', $attr, 'template_part_' . WP_TEMPLATE_PART_AREA_HEADER ), + 'Images in the header block template part should not be lazy-loaded and first large image is set high fetchpriority.' + ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + * @expectedIncorrectUsage wp_get_loading_optimization_attributes + */ + public function test_wp_get_loading_optimization_attributes_incorrect_loading_attrs() { + $attr = $this->get_width_height_for_high_priority(); + $attr['loading'] = 'lazy'; + $attr['fetchpriority'] = 'high'; + + $this->assertSame( + array( + 'loading' => 'lazy', + 'fetchpriority' => 'high', + ), + wp_get_loading_optimization_attributes( 'img', $attr, 'test' ), + 'This should return both lazy-loading and high fetchpriority, but with doing_it_wrong message.' + ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_loading_optimization_attributes_if_loading_attr_present() { + $attr = $this->get_width_height_for_high_priority(); + $attr['loading'] = 'eager'; + + // Check fetchpriority high logic if loading attribute is present. + $this->assertSame( + array( + 'fetchpriority' => 'high', + ), + wp_get_loading_optimization_attributes( 'img', $attr, 'test' ), + 'fetchpriority should be set to high.' + ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_get_loading_optimization_attributes + */ + public function test_wp_get_loading_optimization_attributes_low_res_image() { + $attr = array( + 'width' => 100, + 'height' => 100, + 'loading' => 'eager', + ); + + // fetchpriority not set as image is of lower resolution. + $this->assertSame( + array(), + wp_get_loading_optimization_attributes( 'img', $attr, 'test' ), + 'loading optimization attr array should be empty.' + ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_maybe_add_fetchpriority_high_attr + * + * @dataProvider data_wp_maybe_add_fetchpriority_high_attr + */ + public function test_wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr, $expected_fetchpriority ) { + $loading_attrs = wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ); + + if ( $expected_fetchpriority ) { + $this->assertArrayHasKey( 'fetchpriority', $loading_attrs, 'fetchpriority attribute should be present' ); + $this->assertSame( $expected_fetchpriority, $loading_attrs['fetchpriority'], 'fetchpriority attribute has incorrect value' ); + } else { + $this->assertArrayNotHasKey( 'fetchpriority', $loading_attrs, 'fetchpriority attribute should not be present' ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_maybe_add_fetchpriority_high_attr() { + return array( + 'small image' => array( + array(), + 'img', + $this->get_insufficient_width_height_for_high_priority(), + false, + ), + 'large image' => array( + array(), + 'img', + $this->get_width_height_for_high_priority(), + 'high', + ), + 'image with loading=lazy' => array( + array( 'loading' => 'lazy' ), + 'img', + $this->get_width_height_for_high_priority(), + false, + ), + 'image with loading=eager' => array( + array( 'loading' => 'eager' ), + 'img', + $this->get_width_height_for_high_priority(), + 'high', + ), + 'image with fetchpriority=high' => array( + array(), + 'img', + array_merge( + $this->get_insufficient_width_height_for_high_priority(), + array( 'fetchpriority' => 'high' ) + ), + 'high', + ), + 'image with fetchpriority=low' => array( + array(), + 'img', + array_merge( + $this->get_insufficient_width_height_for_high_priority(), + array( 'fetchpriority' => 'low' ) + ), + false, + ), + 'non-image element' => array( + array(), + 'video', + $this->get_width_height_for_high_priority(), + false, + ), + ); + } + + /** + * @ticket 58235 + * + * @covers ::wp_maybe_add_fetchpriority_high_attr + */ + public function test_wp_maybe_add_fetchpriority_high_attr_min_priority_filter() { + $attr = array( + 'width' => 50, + 'height' => 50, + ); + + add_filter( + 'wp_min_priority_img_pixels', + static function( $res ) { + return 2500; // 50*50=2500 + } + ); + + // fetchpriority set to high as resolution is equal to (or greater than) 2500. + $this->assertSame( + array( + 'fetchpriority' => 'high', + ), + wp_maybe_add_fetchpriority_high_attr( array(), 'img', $attr ) + ); + } + /** * Helper method to keep track of the last context returned by the 'wp_get_attachment_image_context' filter. * @@ -4407,6 +5191,38 @@ EOF; global $wp_the_query; $wp_the_query = $query; } + + /** + * Returns an array with dimension attribute values eligible for a high priority image. + * + * @return array Associative array with 'width' and 'height' keys. + */ + private function get_width_height_for_high_priority() { + /* + * The product of width * height must be >50000 to qualify for high priority image. + * 300 * 200 = 60000 + */ + return array( + 'width' => 300, + 'height' => 200, + ); + } + + /** + * Returns an array with dimension attribute values ineligible for a high priority image. + * + * @return array Associative array with 'width' and 'height' keys. + */ + private function get_insufficient_width_height_for_high_priority() { + /* + * The product of width * height must be >50000 to qualify for high priority image. + * 200 * 100 = 20000 + */ + return array( + 'width' => 200, + 'height' => 100, + ); + } } /**