diff --git a/wp-includes/deprecated.php b/wp-includes/deprecated.php index 36be77efdd..1626995290 100644 --- a/wp-includes/deprecated.php +++ b/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/wp-includes/pluggable.php b/wp-includes/pluggable.php index 73d7d20748..45ee504fda 100644 --- a/wp-includes/pluggable.php +++ b/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/wp-includes/version.php b/wp-includes/version.php index 86bedf2c06..1346c728cc 100644 --- a/wp-includes/version.php +++ b/wp-includes/version.php @@ -16,7 +16,7 @@ * * @global string $wp_version */ -$wp_version = '6.3-alpha-56036'; +$wp_version = '6.3-alpha-56037'; /** * Holds the WordPress DB revision, increments when changes are made to the WordPress DB schema.