From e35dd286ce8cdedd91d10d223b399a5ea3f39289 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 15:24:30 -0800 Subject: [PATCH 01/30] Ensure IMG with fetchpriority=low does not get lazy-loaded or increase media count --- src/wp-includes/media.php | 27 +++++++++++++---- tests/phpunit/tests/media.php | 55 +++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index bfd2e58487429..0df1d0d9b5bf2 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -5967,6 +5967,7 @@ function wp_get_webp_info( $filename ) { * both attributes are present with those values. * * @since 6.3.0 + * @since 7.0.0 Support `fetchpriority=low` so that `loading=lazy` is not added and the media count is not increased. * * @global WP_Query $wp_query WordPress Query object. * @@ -6067,7 +6068,9 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { } // Logic to handle a `fetchpriority` attribute that is already provided. - if ( isset( $attr['fetchpriority'] ) && 'high' === $attr['fetchpriority'] ) { + $existing_fetchpriority = ( $attr['fetchpriority'] ?? null ); + $is_low_fetchpriority = ( 'low' === $existing_fetchpriority ); + if ( 'high' === $existing_fetchpriority ) { /* * If the image was already determined to not be in the viewport (e.g. * from an already provided `loading` attribute), trigger a warning. @@ -6090,6 +6093,13 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { } else { $maybe_in_viewport = true; } + } elseif ( $is_low_fetchpriority ) { + /* + * An IMG with fetchpriority=low is not initially displayed, as it may be a hidden in the Navigation Overlay, + * or it may be occluded in a non-initial carousel slide. Such images must not be lazy-loaded since the browser + * has no heuristic to know when to start loading them prior the user needing to see them. + */ + $maybe_in_viewport = false; } if ( null === $maybe_in_viewport ) { @@ -6140,7 +6150,7 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { * does not include any loop. */ && did_action( 'get_header' ) && ! did_action( 'get_footer' ) - ) { + ) { $maybe_in_viewport = true; $maybe_increase_count = true; } @@ -6149,18 +6159,25 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { /* * If the element is in the viewport (`true`), potentially add * `fetchpriority` with a value of "high". Otherwise, i.e. if the element - * is not not in the viewport (`false`) or it is unknown (`null`), add - * `loading` with a value of "lazy". + * is not in the viewport (`false`) or it is unknown (`null`), add + * `loading` with a value of "lazy" if the element is not already being + * de-prioritized with `fetchpriority=low` due to occlusion in + * Navigation Overlay, non-initial carousel slides, or a collapsed Details block. */ if ( $maybe_in_viewport ) { $loading_attrs = wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ); - } else { + } elseif ( ! $is_low_fetchpriority ) { // Only add `loading="lazy"` if the feature is enabled. if ( wp_lazy_loading_enabled( $tag_name, $context ) ) { $loading_attrs['loading'] = 'lazy'; } } + // Preserve fetchpriorit=low. + if ( $is_low_fetchpriority ) { + $loading_attrs['fetchpriority'] = 'low'; + } + /* * If flag was set based on contextual logic above, increase the content * media count, either unconditionally, or based on whether the image size diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 0822de252d1b7..5d19cfe6d12a4 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -4513,6 +4513,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { wp_get_loading_optimization_attributes( 'img', $attr, 'wp_get_attachment_image' ) ); + $this->assert_fetchpriority_low_loading_attrs( $attr, 'wp_get_attachment_image' ); + // Return 'lazy' if not in the loop or the main query. $this->assertSameSetsWithIndex( array( @@ -4527,6 +4529,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { while ( have_posts() ) { the_post(); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // Return 'lazy' if in the loop but not in the main query. $this->assertSameSetsWithIndex( array( @@ -4539,6 +4543,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { // Set as main query. $this->set_main_query( $query ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // First three element are not lazy loaded. However, first image is loaded with fetchpriority high. $this->assertSameSetsWithIndex( array( @@ -4548,6 +4554,9 @@ public function test_wp_get_loading_optimization_attributes( $context ) { wp_get_loading_optimization_attributes( 'img', $attr, $context ), "Expected first image to not be lazy-loaded. First large image get's high fetchpriority." ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + $this->assertSameSetsWithIndex( array( 'decoding' => 'async', @@ -4555,6 +4564,9 @@ public function test_wp_get_loading_optimization_attributes( $context ) { wp_get_loading_optimization_attributes( 'img', $attr, $context ), 'Expected second image to not be lazy-loaded.' ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + $this->assertSameSetsWithIndex( array( 'decoding' => 'async', @@ -4563,6 +4575,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { 'Expected third image to not be lazy-loaded.' ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // Return 'lazy' if in the loop and in the main query for any subsequent elements. $this->assertSameSetsWithIndex( array( @@ -4572,6 +4586,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { wp_get_loading_optimization_attributes( 'img', $attr, $context ) ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // Yes, for all subsequent elements. $this->assertSameSetsWithIndex( array( @@ -4580,6 +4596,8 @@ public function test_wp_get_loading_optimization_attributes( $context ) { ), wp_get_loading_optimization_attributes( 'img', $attr, $context ) ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); } } @@ -4606,12 +4624,17 @@ public function test_wp_get_loading_optimization_attributes_with_arbitrary_conte 'The "loading" attribute should be "lazy" when not in the loop or the main query.' ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + $query = $this->get_new_wp_query_for_published_post(); // Set as main query. $this->set_main_query( $query ); while ( have_posts() ) { + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + the_post(); $this->assertSameSetsWithIndex( @@ -4656,9 +4679,13 @@ public function test_wp_get_loading_optimization_attributes_with_arbitrary_conte 'The "loading" attribute should be "lazy" before the main query loop.' ); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + while ( have_posts() ) { the_post(); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + $this->assertSameSetsWithIndex( array( 'decoding' => 'async', @@ -4740,6 +4767,8 @@ public function test_wp_get_loading_optimization_attributes_header_contexts( $co wp_get_loading_optimization_attributes( 'img', $attr, $context ), 'Images in the header context should get lazy-loaded after the wp_loading_optimization_force_header_contexts filter.' ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); } /** @@ -4770,6 +4799,8 @@ function ( $context ) { } ); + $this->assert_fetchpriority_low_loading_attrs( $attr, 'something_completely_arbitrary' ); + $this->assertSameSetsWithIndex( array( 'decoding' => 'async', @@ -4809,6 +4840,8 @@ public function test_wp_get_loading_optimization_attributes_before_loop_if_not_m ), wp_get_loading_optimization_attributes( 'img', $attr, $context ) ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); } /** @@ -4840,6 +4873,8 @@ public function test_wp_get_loading_optimization_attributes_before_loop_in_main_ ), wp_get_loading_optimization_attributes( 'img', $attr, $context ) ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); } /** @@ -4863,6 +4898,8 @@ public function test_wp_get_loading_optimization_attributes_before_loop_if_main_ $attr = $this->get_width_height_for_high_priority(); + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + // First image is loaded with high fetchpriority. $this->assertSameSetsWithIndex( array( @@ -6309,6 +6346,24 @@ static function ( $loading_attrs ) { ); } + /** + * Asserts that loading attributes for IMG with fetchpriority=low. + * + * It must not get lazy-loaded or increase the counter since they may be in the Navigation Overlay. + */ + protected function assert_fetchpriority_low_loading_attrs( array $attr, string $context ): void { + $this->assertSameSetsWithIndex( + array( + 'fetchpriority' => 'low', + 'decoding' => 'async', + ), + wp_get_loading_optimization_attributes( + 'img', + array_merge( $attr, array( 'fetchpriority' => 'low' ) ), + $context + ) + ); + } /** * Test WebP lossless quality is handled correctly. From 1477ca6708e5518cad8bed9adf9a5050755585c1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 17:59:30 -0800 Subject: [PATCH 02/30] Improve comment grammar/clarity Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/media.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 0df1d0d9b5bf2..b2555e21261ca 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6095,9 +6095,9 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { } } elseif ( $is_low_fetchpriority ) { /* - * An IMG with fetchpriority=low is not initially displayed, as it may be a hidden in the Navigation Overlay, - * or it may be occluded in a non-initial carousel slide. Such images must not be lazy-loaded since the browser - * has no heuristic to know when to start loading them prior the user needing to see them. + * An IMG with fetchpriority=low is not initially displayed; it may be hidden in the Navigation Overlay, + * or it may be occluded in a non-initial carousel slide. Such images must not be lazy-loaded because the browser + * has no heuristic to know when to start loading them before the user needs to see them. */ $maybe_in_viewport = false; } From 36ebcb0523e610fa1126b5ce4d91428e812cd53c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 17:59:40 -0800 Subject: [PATCH 03/30] Fix typo in comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index b2555e21261ca..c3c767fc157a0 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6173,7 +6173,7 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { } } - // Preserve fetchpriorit=low. + // Preserve fetchpriority=low. if ( $is_low_fetchpriority ) { $loading_attrs['fetchpriority'] = 'low'; } From 1fa9a167a11258be0b0d14efd054b0364dc12229 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 18:00:05 -0800 Subject: [PATCH 04/30] Fix grammar typo Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpunit/tests/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 5d19cfe6d12a4..10e8284503da6 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -4552,7 +4552,7 @@ public function test_wp_get_loading_optimization_attributes( $context ) { '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." + "Expected first image to not be lazy-loaded. First large image gets high fetchpriority." ); $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); From 7f0c9e78cb21a98e2b2c73f72cc610a240b1f071 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 21:26:35 -0800 Subject: [PATCH 05/30] Use single quotes Co-authored-by: Mukesh Panchal --- tests/phpunit/tests/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 10e8284503da6..8d74262f09944 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -4552,7 +4552,7 @@ public function test_wp_get_loading_optimization_attributes( $context ) { 'fetchpriority' => 'high', ), wp_get_loading_optimization_attributes( 'img', $attr, $context ), - "Expected first image to not be lazy-loaded. First large image gets high fetchpriority." + 'Expected first image to not be lazy-loaded. First large image gets high fetchpriority.' ); $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); From 9b1c141bcee6752142d63fe5fea5bf67e0f22d0e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 6 Mar 2026 21:28:45 -0800 Subject: [PATCH 06/30] Re-use condition Co-authored-by: Mukesh Panchal --- src/wp-includes/media.php | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index c3c767fc157a0..4d66c5fcd9712 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6100,6 +6100,9 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { * has no heuristic to know when to start loading them before the user needs to see them. */ $maybe_in_viewport = false; + + // Preserve fetchpriority=low. + $loading_attrs['fetchpriority'] = 'low'; } if ( null === $maybe_in_viewport ) { @@ -6173,11 +6176,6 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { } } - // Preserve fetchpriority=low. - if ( $is_low_fetchpriority ) { - $loading_attrs['fetchpriority'] = 'low'; - } - /* * If flag was set based on contextual logic above, increase the content * media count, either unconditionally, or based on whether the image size From 2093108fd8bc2f2720ac0ec8b8331a4a1c596501 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 16:32:19 -0700 Subject: [PATCH 07/30] Add phpdoc params --- tests/phpunit/tests/media.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 8d74262f09944..374022e0fef99 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -6350,6 +6350,9 @@ static function ( $loading_attrs ) { * Asserts that loading attributes for IMG with fetchpriority=low. * * It must not get lazy-loaded or increase the counter since they may be in the Navigation Overlay. + * + * @param array $attr + * @param string $context */ protected function assert_fetchpriority_low_loading_attrs( array $attr, string $context ): void { $this->assertSameSetsWithIndex( From f5de5b276575a71dde9c19c054c4d2d7d3efd9b8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 16:35:24 -0700 Subject: [PATCH 08/30] Correct return description for wp_high_priority_element_flag() --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 4d66c5fcd9712..49dffde29c85f 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6321,7 +6321,7 @@ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr * @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. + * @return bool Returns true if the high-priority element was not already marked. */ function wp_high_priority_element_flag( $value = null ) { static $high_priority_element = true; From edf76ebeb4ac56ba7c0c9319d44ecb6c48837707 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 16:35:55 -0700 Subject: [PATCH 09/30] Add missing return type for wp_high_priority_element_flag() --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 49dffde29c85f..ad08d16f3a433 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6323,7 +6323,7 @@ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr * @param bool $value Optional. Used to change the static variable. Default null. * @return bool Returns true if the high-priority element was not already marked. */ -function wp_high_priority_element_flag( $value = null ) { +function wp_high_priority_element_flag( $value = null ): bool { static $high_priority_element = true; if ( is_bool( $value ) ) { From ec3c50806856118df857d924dfefd72b1ebca23c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 17:50:37 -0700 Subject: [PATCH 10/30] Update wp_maybe_add_fetchpriority_high_attr() to account for fetchpriority low/auto --- src/wp-includes/media.php | 26 ++++++++------ tests/phpunit/tests/media.php | 64 +++++++++++++++++++++++++++-------- 2 files changed, 65 insertions(+), 25 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index ad08d16f3a433..687ec6344b9ac 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6260,12 +6260,13 @@ function wp_increase_content_media_count( $amount = 1 ) { * Determines whether to add `fetchpriority='high'` to loading attributes. * * @since 6.3.0 + * @since 7.0.0 Support is added for IMG tags with `fetchpriority='low'` and `fetchpriority='auto'`. * @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. + * @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. @@ -6273,12 +6274,15 @@ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr return $loading_attrs; } - if ( isset( $attr['fetchpriority'] ) ) { + $existing_fetchpriority = $attr['fetchpriority'] ?? null; + if ( 'high' === $existing_fetchpriority || 'low' === $existing_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. + * When an IMG has been explicitly marked with `fetchpriority=high`, then honor that this is the element that + * should have the priority. In contrast, the Navigation block may add `fetchpriority=low` to an IMG which + * appears in the Navigation Overlay; such images should never be considered candidates for + * `fetchpriority=high`. Lastly, block visibility may add `fetchpriority=auto` to an IMG when the block is + * conditionally displayed based on viewport size. Such an image is considered an LCP element candidate if it + * exceeds the threshold for the minimum number of square pixels. */ if ( 'high' === $attr['fetchpriority'] ) { $loading_attrs['fetchpriority'] = 'high'; @@ -6307,7 +6311,9 @@ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr $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'; + if ( 'auto' !== $existing_fetchpriority ) { + $loading_attrs['fetchpriority'] = 'high'; + } wp_high_priority_element_flag( false ); } diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 374022e0fef99..478487f604c02 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -6086,52 +6086,83 @@ function ( $atts, $content = null ) { * * @dataProvider data_wp_maybe_add_fetchpriority_high_attr */ - public function test_wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr, $expected_fetchpriority ) { + public function test_wp_maybe_add_fetchpriority_high_attr( array $loading_attrs, string $tag_name, array $attr, ?string $expected_fetchpriority, bool $expected_high_priority_element_flag ): void { $loading_attrs = wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr ); - if ( $expected_fetchpriority ) { + if ( null !== $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' ); } + $this->assertSame( $expected_high_priority_element_flag, wp_high_priority_element_flag() ); } /** * Data provider. * - * @return array[] + * @return array, + * 1: string, + * 2: array, + * 3: string|null, + * 5: bool, + * }}> */ - public function data_wp_maybe_add_fetchpriority_high_attr() { + public function data_wp_maybe_add_fetchpriority_high_attr(): array { return array( - 'small image' => array( + 'small image' => array( array(), 'img', $this->get_insufficient_width_height_for_high_priority(), - false, + null, + true, ), - 'large image' => array( + 'small image with fetchpriority=auto' => array( + array(), + 'img', + array_merge( + $this->get_insufficient_width_height_for_high_priority(), + array( 'fetchpriority' => 'auto' ) + ), + null, + true, + ), + 'large image' => array( array(), 'img', $this->get_width_height_for_high_priority(), 'high', + false, ), - 'image with loading=lazy' => array( + 'large image with fetchpriority=auto' => array( + array(), + 'img', + array_merge( + $this->get_width_height_for_high_priority(), + array( 'fetchpriority' => 'auto' ) + ), + null, + false, + ), + 'image with loading=lazy' => array( array( 'loading' => 'lazy', 'decoding' => 'async', ), 'img', $this->get_width_height_for_high_priority(), - false, + null, + true, ), - 'image with loading=eager' => array( + 'image with loading=eager' => array( array( 'loading' => 'eager' ), 'img', $this->get_width_height_for_high_priority(), 'high', + false, ), - 'image with fetchpriority=high' => array( + 'image with fetchpriority=high' => array( array(), 'img', array_merge( @@ -6139,21 +6170,24 @@ public function data_wp_maybe_add_fetchpriority_high_attr() { array( 'fetchpriority' => 'high' ) ), 'high', + false, ), - 'image with fetchpriority=low' => array( + 'image with fetchpriority=low' => array( array(), 'img', array_merge( $this->get_insufficient_width_height_for_high_priority(), array( 'fetchpriority' => 'low' ) ), - false, + null, + true, ), - 'non-image element' => array( + 'non-image element' => array( array(), 'video', $this->get_width_height_for_high_priority(), - false, + null, + true, ), ); } From 1439f77dec0d1e8247df9feacafa14ee69addda1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 18:17:06 -0700 Subject: [PATCH 11/30] Update wp_get_loading_optimization_attributes() to account for fetchpriority=auto --- src/wp-includes/media.php | 16 ++++ tests/phpunit/tests/media.php | 134 +++++++++++++++++++++++++++++++++- 2 files changed, 147 insertions(+), 3 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 687ec6344b9ac..19c5c8791226c 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6103,6 +6103,22 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { // Preserve fetchpriority=low. $loading_attrs['fetchpriority'] = 'low'; + } elseif ( 'auto' === $existing_fetchpriority ) { + /* + * When a block's visibility support identifies that the block is conditionally displayed based on the viewport + * size, then it adds `fetchpriority=auto` to the block's IMG tags. These images must not be fetched with high + * priority because they could be erroneously loaded in viewports which do not even display them. Contrarily, + * they must not get `fetchpriority=low` because they may in fact be displayed in the current viewport. So as + * a signal to indicate that an IMG may be in the viewport, `fetchpriority=auto` is added. This has the effect + * here of preventing the media count from being increased, so that images hidden with block visibility do not + * effect whether a following IMG gets `loading=lazy`. In particular, `loading=lazy` should still be omitted + * on an IMG following any number of initial IMGs with `fetchpriority=auto` since those initial images may not + * be displayed. + */ + $maybe_in_viewport = true; + + // Preserve fetchpriority=auto. + $loading_attrs['fetchpriority'] = 'auto'; } if ( null === $maybe_in_viewport ) { diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 478487f604c02..6342d16e63eec 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -3916,7 +3916,10 @@ public function test_wp_get_loading_attr_default( $context ) { $this->assertFalse( wp_get_loading_attr_default( 'template_part_' . WP_TEMPLATE_PART_AREA_HEADER ), 'Images in the footer block template part should not be lazy-loaded.' ); } - public function data_wp_get_loading_attr_default() { + /** + * @return array + */ + public function data_wp_get_loading_attr_default(): array { return array( array( 'the_content' ), array( 'the_post_thumbnail' ), @@ -4481,7 +4484,7 @@ public function data_special_contexts_for_the_content_wp_get_loading_attr_defaul } /** - * Tests that wp_get_loading_attr_default() returns the expected loading attribute value. + * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value. * * @ticket 53675 * @ticket 56930 @@ -4494,7 +4497,7 @@ public function data_special_contexts_for_the_content_wp_get_loading_attr_defaul * * @param string $context */ - public function test_wp_get_loading_optimization_attributes( $context ) { + public function test_wp_get_loading_optimization_attributes( string $context ): void { $attr = $this->get_width_height_for_high_priority(); // Return 'lazy' by default. @@ -4601,6 +4604,131 @@ public function test_wp_get_loading_optimization_attributes( $context ) { } } + /** + * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value. + * + * This test is the same as {@see self::wp_get_loading_optimization_attributes()} except that the IMG which + * previously got `fetchpriority=high` now initially has `fetchpriority=auto`. This causes the initial lazy-loaded + * image to be bumped down one. + * + * @ticket 64823 + * + * @covers ::wp_get_loading_optimization_attributes + * + * @dataProvider data_wp_get_loading_attr_default + * + * @param string $context + */ + public function test_wp_get_loading_optimization_attributes_with_fetchpriority_auto_for_lcp_candidate( string $context ): void { + $attr = $this->get_width_height_for_high_priority(); + + // Return 'lazy' by default. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, 'test' ) + ); + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, 'wp_get_attachment_image' ) + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, 'wp_get_attachment_image' ); + + // Return 'lazy' if not in the loop or the main query. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + $query = $this->get_new_wp_query_for_published_post(); + + while ( have_posts() ) { + the_post(); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + // Return 'lazy' if in the loop but not in the main query. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + // Set as main query. + $this->set_main_query( $query ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + // First three element are not lazy loaded. However, first image initially has `fetchpriority=auto` which marks it as a possible LCP element. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'fetchpriority' => 'auto', + ), + wp_get_loading_optimization_attributes( + 'img', + array_merge( $attr, array( 'fetchpriority' => 'auto' ) ), + $context + ), + 'Expected first image to not be lazy-loaded.' + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'Expected second image to not be lazy-loaded.' + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ), + 'Expected third image to not be lazy-loaded.' + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + // This is the 4th subsequent image, and it still is not lazy-loaded because the first had fetchpriority=auto and so it may have been hidden with block visibility. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + // Yes, for all subsequent elements. + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( 'img', $attr, $context ) + ); + + $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + } + } + /** * Tests that wp_get_loading_optimization_attributes() returns fetchpriority=high and increases the count for arbitrary contexts in the main loop. * From e32f008a4c08544c3d25a773341e4cbd52172a18 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 18:29:42 -0700 Subject: [PATCH 12/30] Fix fetchpriority attr handling in wp_maybe_add_fetchpriority_high_attr() --- src/wp-includes/media.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 19c5c8791226c..57cbb63cbef9e 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6291,7 +6291,7 @@ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr } $existing_fetchpriority = $attr['fetchpriority'] ?? null; - if ( 'high' === $existing_fetchpriority || 'low' === $existing_fetchpriority ) { + if ( null !== $existing_fetchpriority && 'auto' !== $existing_fetchpriority ) { /* * When an IMG has been explicitly marked with `fetchpriority=high`, then honor that this is the element that * should have the priority. In contrast, the Navigation block may add `fetchpriority=low` to an IMG which @@ -6300,7 +6300,7 @@ function wp_maybe_add_fetchpriority_high_attr( $loading_attrs, $tag_name, $attr * conditionally displayed based on viewport size. Such an image is considered an LCP element candidate if it * exceeds the threshold for the minimum number of square pixels. */ - if ( 'high' === $attr['fetchpriority'] ) { + if ( 'high' === $existing_fetchpriority ) { $loading_attrs['fetchpriority'] = 'high'; wp_high_priority_element_flag( false ); } From 003806eb6d1ce0353a95671ce75e8e550cf75bef Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 19:20:31 -0700 Subject: [PATCH 13/30] Backport https://github.com/WordPress/gutenberg/pull/76302 --- src/wp-includes/block-supports/block-visibility.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 756e0500418f4..99da219c5a120 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -139,8 +139,16 @@ function wp_render_block_visibility_support( $block_content, $block ) { $processor = new WP_HTML_Tag_Processor( $block_content ); if ( $processor->next_tag() ) { $processor->add_class( implode( ' ', $class_names ) ); - $block_content = $processor->get_updated_html(); } + + /* + * Set all IMG tags to be `fetchpriority=auto` so that wp_get_loading_optimization_attributes() won't add + * `fetchpriority=high` or increment the media count to effect whether subsequent IMG tags get `loading=lazy`. + */ + while ( $processor->next_tag( 'IMG' ) ) { + $processor->set_attribute( 'fetchpriority', 'auto' ); + } + $block_content = $processor->get_updated_html(); } } From e489e94c4201d46977fbd45669ebfe3021dfa406 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 19:53:37 -0700 Subject: [PATCH 14/30] Improve since description Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 57cbb63cbef9e..6a851a867dc58 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -5967,7 +5967,7 @@ function wp_get_webp_info( $filename ) { * both attributes are present with those values. * * @since 6.3.0 - * @since 7.0.0 Support `fetchpriority=low` so that `loading=lazy` is not added and the media count is not increased. + * @since 7.0.0 Support `fetchpriority=low` and `fetchpriority=auto` so that `loading=lazy` is not added and the media count is not increased. * * @global WP_Query $wp_query WordPress Query object. * From 6e4e3bc24a52107a18e71dda48e68754ae27366d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 19:54:02 -0700 Subject: [PATCH 15/30] Fix typo in phpdoc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/block-supports/block-visibility.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 99da219c5a120..80009ca9a4ffd 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -143,7 +143,7 @@ function wp_render_block_visibility_support( $block_content, $block ) { /* * Set all IMG tags to be `fetchpriority=auto` so that wp_get_loading_optimization_attributes() won't add - * `fetchpriority=high` or increment the media count to effect whether subsequent IMG tags get `loading=lazy`. + * `fetchpriority=high` or increment the media count to affect whether subsequent IMG tags get `loading=lazy`. */ while ( $processor->next_tag( 'IMG' ) ) { $processor->set_attribute( 'fetchpriority', 'auto' ); From e3ec3043564407b9c4c2d1f7f06cd91e9485b2b7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 19:55:21 -0700 Subject: [PATCH 16/30] Account for IMG as root block element Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/block-supports/block-visibility.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 80009ca9a4ffd..7713bb2c80f4f 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -140,15 +140,17 @@ function wp_render_block_visibility_support( $block_content, $block ) { if ( $processor->next_tag() ) { $processor->add_class( implode( ' ', $class_names ) ); } + $block_content = $processor->get_updated_html(); /* * Set all IMG tags to be `fetchpriority=auto` so that wp_get_loading_optimization_attributes() won't add - * `fetchpriority=high` or increment the media count to affect whether subsequent IMG tags get `loading=lazy`. + * `fetchpriority=high` or increment the media count to effect whether subsequent IMG tags get `loading=lazy`. */ - while ( $processor->next_tag( 'IMG' ) ) { - $processor->set_attribute( 'fetchpriority', 'auto' ); + $img_processor = new WP_HTML_Tag_Processor( $block_content ); + while ( $img_processor->next_tag( 'IMG' ) ) { + $img_processor->set_attribute( 'fetchpriority', 'auto' ); } - $block_content = $processor->get_updated_html(); + $block_content = $img_processor->get_updated_html(); } } From 7657a5471cf3d2b516dc9d377c97be52f89752d5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 19:49:44 -0700 Subject: [PATCH 17/30] Improve test to ensure fetchpriority=auto is added to IMG --- .../tests/block-supports/block-visibility.php | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index dd116472ba1f4..e6d91edfb8793 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -236,8 +236,9 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo ); } - /* + /** * @ticket 64414 + * @ticket 64823 */ public function test_block_visibility_support_generated_css_with_two_viewport_sizes() { $this->register_visibility_block_with_support( @@ -259,13 +260,14 @@ public function test_block_visibility_support_generated_css_with_two_viewport_si ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertStringContainsString( - 'class="wp-block-hidden-desktop wp-block-hidden-mobile"', + $this->assertEqualHTML( + '
Test content
', $result, - 'Block should have both visibility classes in the class attribute' + '', + 'Block should have both visibility classes in the class attribute, and the IMG should have fetchpriority=auto.' ); $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); From a5a575b1cc27dac53b0c2f9c64807a87137b9bb2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 20:02:17 -0700 Subject: [PATCH 18/30] Reuse processor --- .../block-supports/block-visibility.php | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 7713bb2c80f4f..4c56482b5453b 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -139,18 +139,18 @@ function wp_render_block_visibility_support( $block_content, $block ) { $processor = new WP_HTML_Tag_Processor( $block_content ); if ( $processor->next_tag() ) { $processor->add_class( implode( ' ', $class_names ) ); - } - $block_content = $processor->get_updated_html(); - /* - * Set all IMG tags to be `fetchpriority=auto` so that wp_get_loading_optimization_attributes() won't add - * `fetchpriority=high` or increment the media count to effect whether subsequent IMG tags get `loading=lazy`. - */ - $img_processor = new WP_HTML_Tag_Processor( $block_content ); - while ( $img_processor->next_tag( 'IMG' ) ) { - $img_processor->set_attribute( 'fetchpriority', 'auto' ); + /* + * Set all IMG tags to be `fetchpriority=auto` so that wp_get_loading_optimization_attributes() won't add + * `fetchpriority=high` or increment the media count to effect whether subsequent IMG tags get `loading=lazy`. + */ + do { + if ( 'IMG' === $processor->get_tag() ) { + $processor->set_attribute( 'fetchpriority', 'auto' ); + } + } while ( $processor->next_tag() ); + $block_content = $processor->get_updated_html(); } - $block_content = $img_processor->get_updated_html(); } } From 0208a08a863345a54be299a5ba048b9feb76862f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 20:02:39 -0700 Subject: [PATCH 19/30] Mark WP_HTML_Tag_Processor::next_tag() as impure See https://phpstan.org/writing-php-code/phpdocs-basics#impure-functions --- src/wp-includes/html-api/class-wp-html-tag-processor.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wp-includes/html-api/class-wp-html-tag-processor.php b/src/wp-includes/html-api/class-wp-html-tag-processor.php index 9289d5d27f880..148bbc0a471a8 100644 --- a/src/wp-includes/html-api/class-wp-html-tag-processor.php +++ b/src/wp-includes/html-api/class-wp-html-tag-processor.php @@ -881,6 +881,8 @@ public function change_parsing_namespace( string $new_namespace ): bool { * @type string|null $tag_closers "visit" or "skip": whether to stop on tag closers, e.g. . * } * @return bool Whether a tag was matched. + * + * @phpstan-impure */ public function next_tag( $query = null ): bool { $this->parse_query( $query ); From 0b88b8b9f012c3c311e965929002057cbb07734d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 20:50:26 -0700 Subject: [PATCH 20/30] Leverage assertEqualHTML in tests and add IMG to more test cases --- .../tests/block-supports/block-visibility.php | 49 +++++++++++++------ 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index e6d91edfb8793..ff8831c1aa547 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -93,7 +93,7 @@ public function test_block_visibility_support_shows_block_when_support_not_opted array( 'visibility' => false ) ); - $block_content = '

This is a test block.

'; + $block_content = '
Test content
'; $block = array( 'blockName' => 'test/visibility-block', 'attrs' => array( @@ -122,7 +122,7 @@ public function test_block_visibility_support_no_visibility_attribute() { 'attrs' => array(), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertSame( $block_content, $result, 'Block content should remain unchanged when no visibility attribute is present.' ); @@ -150,7 +150,7 @@ public function test_block_visibility_support_generated_css_with_mobile_viewport ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertStringContainsString( 'wp-block-hidden-mobile', $result, 'Block should have the visibility class for the mobile breakpoint.' ); @@ -186,10 +186,15 @@ public function test_block_visibility_support_generated_css_with_tablet_viewport ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertStringContainsString( 'class="existing-class wp-block-hidden-tablet"', $result, 'Block should have the existing class and the visibility class for the tablet breakpoint in the class attribute.' ); + $this->assertEqualHTML( + '
Test content
', + $result, + '', + 'Block should have the existing class and the visibility class for the tablet breakpoint in the class attribute.' + ); $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); @@ -222,10 +227,15 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertStringContainsString( 'class="wp-block-hidden-desktop"', $result, 'Block should have the visibility class for the desktop breakpoint in the class attribute.' ); + $this->assertEqualHTML( + '
Test content
', + $result, + '', + 'Block should have the visibility class for the desktop breakpoint in the class attribute.' + ); $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); @@ -279,10 +289,11 @@ public function test_block_visibility_support_generated_css_with_two_viewport_si ); } - /* + /** * @ticket 64414 + * @ticket 64823 */ - public function test_block_visibility_support_generated_css_with_all_viewport_sizes_visible() { + public function test_block_visibility_support_generated_css_with_all_viewport_sizes_visible(): void { $this->register_visibility_block_with_support( 'test/viewport-all-visible', array( 'visibility' => true ) @@ -303,16 +314,17 @@ public function test_block_visibility_support_generated_css_with_all_viewport_si ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertSame( $block_content, $result, 'Block content should remain unchanged when all breakpoints are visible.' ); } - /* + /** * @ticket 64414 + * @ticket 64823 */ - public function test_block_visibility_support_generated_css_with_all_viewport_sizes_hidden() { + public function test_block_visibility_support_generated_css_with_all_viewport_sizes_hidden(): void { $this->register_visibility_block_with_support( 'test/viewport-all-hidden', array( 'visibility' => true ) @@ -333,10 +345,15 @@ public function test_block_visibility_support_generated_css_with_all_viewport_si ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); - $this->assertSame( '
Test content
', $result, 'Block content should have the visibility classes for all viewport sizes in the class attribute.' ); + $this->assertEqualHTML( + '
Test content
', + $result, + '', + 'Block content should have the visibility classes for all viewport sizes in the class attribute, and an IMG should get fetchpriority=auto.' + ); } /* @@ -357,7 +374,7 @@ public function test_block_visibility_support_generated_css_with_empty_object() ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertSame( $block_content, $result, 'Block content should remain unchanged when blockVisibility is an empty array.' ); @@ -387,7 +404,7 @@ public function test_block_visibility_support_generated_css_with_unknown_viewpor ), ); - $block_content = '
Test content
'; + $block_content = '
Test content
'; $result = wp_render_block_visibility_support( $block_content, $block ); $this->assertStringContainsString( From e19e2900e591c92e0f65de4ae2589d0ef01320e6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 20:53:49 -0700 Subject: [PATCH 21/30] Apply static analysis fixes to Tests_Block_Supports_Block_Visibility --- .../tests/block-supports/block-visibility.php | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/tests/phpunit/tests/block-supports/block-visibility.php b/tests/phpunit/tests/block-supports/block-visibility.php index ff8831c1aa547..4f24b931744f8 100644 --- a/tests/phpunit/tests/block-supports/block-visibility.php +++ b/tests/phpunit/tests/block-supports/block-visibility.php @@ -11,18 +11,18 @@ * @covers ::wp_render_block_visibility_support */ class Tests_Block_Supports_Block_Visibility extends WP_UnitTestCase { - /** - * @var string|null - */ - private $test_block_name; - public function set_up() { + private ?string $test_block_name; + + public function set_up(): void { parent::set_up(); $this->test_block_name = null; } - public function tear_down() { - unregister_block_type( $this->test_block_name ); + public function tear_down(): void { + if ( $this->test_block_name ) { + unregister_block_type( $this->test_block_name ); + } $this->test_block_name = null; parent::tear_down(); } @@ -30,12 +30,10 @@ public function tear_down() { /** * Registers a new block for testing block visibility support. * - * @param string $block_name Name for the test block. - * @param array $supports Array defining block support configuration. - * - * @return WP_Block_Type The block type for the newly registered test block. + * @param string $block_name Name for the test block. + * @param array $supports Array defining block support configuration. */ - private function register_visibility_block_with_support( $block_name, $supports = array() ) { + private function register_visibility_block_with_support( string $block_name, array $supports = array() ): void { $this->test_block_name = $block_name; register_block_type( $this->test_block_name, @@ -51,7 +49,7 @@ private function register_visibility_block_with_support( $block_name, $supports ); $registry = WP_Block_Type_Registry::get_instance(); - return $registry->get_registered( $this->test_block_name ); + $registry->get_registered( $this->test_block_name ); } /** @@ -60,7 +58,7 @@ private function register_visibility_block_with_support( $block_name, $supports * * @ticket 64061 */ - public function test_block_visibility_support_hides_block_when_visibility_false() { + public function test_block_visibility_support_hides_block_when_visibility_false(): void { $this->register_visibility_block_with_support( 'test/visibility-block', array( 'visibility' => true ) @@ -87,7 +85,7 @@ public function test_block_visibility_support_hides_block_when_visibility_false( * * @ticket 64061 */ - public function test_block_visibility_support_shows_block_when_support_not_opted_in() { + public function test_block_visibility_support_shows_block_when_support_not_opted_in(): void { $this->register_visibility_block_with_support( 'test/visibility-block', array( 'visibility' => false ) @@ -108,10 +106,10 @@ public function test_block_visibility_support_shows_block_when_support_not_opted $this->assertSame( $block_content, $result, 'Block content should remain unchanged when blockVisibility support is not opted in.' ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_no_visibility_attribute() { + public function test_block_visibility_support_no_visibility_attribute(): void { $this->register_visibility_block_with_support( 'test/block-visibility-none', array( 'visibility' => true ) @@ -128,10 +126,10 @@ public function test_block_visibility_support_no_visibility_attribute() { $this->assertSame( $block_content, $result, 'Block content should remain unchanged when no visibility attribute is present.' ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_mobile_viewport_size() { + public function test_block_visibility_support_generated_css_with_mobile_viewport_size(): void { $this->register_visibility_block_with_support( 'test/viewport-mobile', array( 'visibility' => true ) @@ -164,10 +162,10 @@ public function test_block_visibility_support_generated_css_with_mobile_viewport ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_tablet_viewport_size() { + public function test_block_visibility_support_generated_css_with_tablet_viewport_size(): void { $this->register_visibility_block_with_support( 'test/viewport-tablet', array( 'visibility' => true ) @@ -205,10 +203,10 @@ public function test_block_visibility_support_generated_css_with_tablet_viewport ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_desktop_breakpoint() { + public function test_block_visibility_support_generated_css_with_desktop_breakpoint(): void { $this->register_visibility_block_with_support( 'test/viewport-desktop', array( 'visibility' => true ) @@ -250,7 +248,7 @@ public function test_block_visibility_support_generated_css_with_desktop_breakpo * @ticket 64414 * @ticket 64823 */ - public function test_block_visibility_support_generated_css_with_two_viewport_sizes() { + public function test_block_visibility_support_generated_css_with_two_viewport_sizes(): void { $this->register_visibility_block_with_support( 'test/viewport-two', array( 'visibility' => true ) @@ -356,10 +354,10 @@ public function test_block_visibility_support_generated_css_with_all_viewport_si ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_empty_object() { + public function test_block_visibility_support_generated_css_with_empty_object(): void { $this->register_visibility_block_with_support( 'test/viewport-empty', array( 'visibility' => true ) @@ -380,10 +378,10 @@ public function test_block_visibility_support_generated_css_with_empty_object() $this->assertSame( $block_content, $result, 'Block content should remain unchanged when blockVisibility is an empty array.' ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_unknown_viewport_sizes_ignored() { + public function test_block_visibility_support_generated_css_with_unknown_viewport_sizes_ignored(): void { $this->register_visibility_block_with_support( 'test/viewport-unknown-viewport-sizes', array( 'visibility' => true ) @@ -414,10 +412,10 @@ public function test_block_visibility_support_generated_css_with_unknown_viewpor ); } - /* + /** * @ticket 64414 */ - public function test_block_visibility_support_generated_css_with_empty_content() { + public function test_block_visibility_support_generated_css_with_empty_content(): void { $this->register_visibility_block_with_support( 'test/viewport-empty-content', array( 'visibility' => true ) From d77176670e0697fa72689ba7b52f74b66e8d31ce Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 20:54:31 -0700 Subject: [PATCH 22/30] Fix typo in comment Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index 6a851a867dc58..fbb2cfb8a1c02 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6111,7 +6111,7 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { * they must not get `fetchpriority=low` because they may in fact be displayed in the current viewport. So as * a signal to indicate that an IMG may be in the viewport, `fetchpriority=auto` is added. This has the effect * here of preventing the media count from being increased, so that images hidden with block visibility do not - * effect whether a following IMG gets `loading=lazy`. In particular, `loading=lazy` should still be omitted + * affect whether a following IMG gets `loading=lazy`. In particular, `loading=lazy` should still be omitted * on an IMG following any number of initial IMGs with `fetchpriority=auto` since those initial images may not * be displayed. */ From 11c868392888f44902591cd91b3c4693dd3c4b63 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 20:55:09 -0700 Subject: [PATCH 23/30] Correct phpdoc Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpunit/tests/media.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 6342d16e63eec..28f7fa3baed84 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -6513,8 +6513,8 @@ static function ( $loading_attrs ) { * * It must not get lazy-loaded or increase the counter since they may be in the Navigation Overlay. * - * @param array $attr - * @param string $context + * @param array $attr + * @param string $context */ protected function assert_fetchpriority_low_loading_attrs( array $attr, string $context ): void { $this->assertSameSetsWithIndex( From fcbcd95287513505e7bd0d21d6a6fff02d4a31a3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 20:55:58 -0700 Subject: [PATCH 24/30] Fix method reference Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpunit/tests/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 28f7fa3baed84..38890b92976b5 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -4607,7 +4607,7 @@ public function test_wp_get_loading_optimization_attributes( string $context ): /** * Tests that wp_get_loading_optimization_attributes() returns the expected loading attribute value. * - * This test is the same as {@see self::wp_get_loading_optimization_attributes()} except that the IMG which + * This test is the same as {@see self::test_wp_get_loading_optimization_attributes()} except that the IMG which * previously got `fetchpriority=high` now initially has `fetchpriority=auto`. This causes the initial lazy-loaded * image to be bumped down one. * From dcf1ecb99aac8fda470ec949b3b169a294657a03 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 21:03:27 -0700 Subject: [PATCH 25/30] Refine phpdoc types Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpunit/tests/media.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 38890b92976b5..67d416437fb7e 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -6232,10 +6232,10 @@ public function test_wp_maybe_add_fetchpriority_high_attr( array $loading_attrs, * @return array, * 1: string, - * 2: array, + * 2: array, * 3: string|null, - * 5: bool, - * }}> + * 4: bool, + * }> */ public function data_wp_maybe_add_fetchpriority_high_attr(): array { return array( @@ -7227,9 +7227,9 @@ public function set_main_query( $query ) { /** * Returns an array with dimension attribute values eligible for a high priority image. * - * @return array Associative array with 'width' and 'height' keys. + * @return array{ width: int, height: int } Associative array with 'width' and 'height' keys. */ - private function get_width_height_for_high_priority() { + private function get_width_height_for_high_priority(): array { /* * The product of width * height must be >50000 to qualify for high priority image. * 300 * 200 = 60000 @@ -7243,9 +7243,9 @@ private function get_width_height_for_high_priority() { /** * Returns an array with dimension attribute values ineligible for a high priority image. * - * @return array Associative array with 'width' and 'height' keys. + * @return array{ width: int, height: int } Associative array with 'width' and 'height' keys. */ - private function get_insufficient_width_height_for_high_priority() { + private function get_insufficient_width_height_for_high_priority(): array { /* * The product of width * height must be >50000 to qualify for high priority image. * 200 * 100 = 20000 From 10c8c91c7edbd52303e601b6b7c517d1bc8ab23f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 21:06:33 -0700 Subject: [PATCH 26/30] Fix typo in test_wp_loading_optimization_force_header_contexts_filter Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpunit/tests/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 67d416437fb7e..e185de319dfee 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -4921,7 +4921,7 @@ public function test_wp_loading_optimization_force_header_contexts_filter() { add_filter( 'wp_loading_optimization_force_header_contexts', - function ( $context ) { + function ( $contexts ) { $contexts['something_completely_arbitrary'] = true; return $contexts; } From 0d3b2a008376e1412971d32c43feaa23a3d4629a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 8 Mar 2026 21:19:47 -0700 Subject: [PATCH 27/30] Use affect instead of effect Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/wp-includes/block-supports/block-visibility.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wp-includes/block-supports/block-visibility.php b/src/wp-includes/block-supports/block-visibility.php index 4c56482b5453b..1ba208e30c92e 100644 --- a/src/wp-includes/block-supports/block-visibility.php +++ b/src/wp-includes/block-supports/block-visibility.php @@ -142,7 +142,7 @@ function wp_render_block_visibility_support( $block_content, $block ) { /* * Set all IMG tags to be `fetchpriority=auto` so that wp_get_loading_optimization_attributes() won't add - * `fetchpriority=high` or increment the media count to effect whether subsequent IMG tags get `loading=lazy`. + * `fetchpriority=high` or increment the media count to affect whether subsequent IMG tags get `loading=lazy`. */ do { if ( 'IMG' === $processor->get_tag() ) { From 86909e11efd63df45e123518a5da03a3c8d10ee9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 9 Mar 2026 17:44:41 -0700 Subject: [PATCH 28/30] Avoid treating fetchpriority=auto as maybe_in_viewport and prevent incrementing media count for them --- src/wp-includes/media.php | 21 ++++++++++++--------- tests/phpunit/tests/media.php | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/src/wp-includes/media.php b/src/wp-includes/media.php index fbb2cfb8a1c02..f9f35f6dbb087 100644 --- a/src/wp-includes/media.php +++ b/src/wp-includes/media.php @@ -6115,7 +6115,6 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { * on an IMG following any number of initial IMGs with `fetchpriority=auto` since those initial images may not * be displayed. */ - $maybe_in_viewport = true; // Preserve fetchpriority=auto. $loading_attrs['fetchpriority'] = 'auto'; @@ -6195,16 +6194,20 @@ function wp_get_loading_optimization_attributes( $tag_name, $attr, $context ) { /* * If flag was set based on contextual logic above, increase the content * media count, either unconditionally, or based on whether the image size - * is larger than the threshold. + * is larger than the threshold. This does not apply when the IMG has + * fetchpriority=auto because it may be conditionally displayed by viewport + * size. */ - if ( $increase_count ) { - wp_increase_content_media_count(); - } elseif ( $maybe_increase_count ) { - /** This filter is documented in wp-includes/media.php */ - $wp_min_priority_img_pixels = apply_filters( 'wp_min_priority_img_pixels', 50000 ); - - if ( $wp_min_priority_img_pixels <= $attr['width'] * $attr['height'] ) { + if ( 'auto' !== $existing_fetchpriority ) { + if ( $increase_count ) { wp_increase_content_media_count(); + } elseif ( $maybe_increase_count ) { + /** This filter is documented in wp-includes/media.php */ + $wp_min_priority_img_pixels = apply_filters( 'wp_min_priority_img_pixels', 50000 ); + + if ( $wp_min_priority_img_pixels <= $attr['width'] * $attr['height'] ) { + wp_increase_content_media_count(); + } } } diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index e185de319dfee..78ad36402dfa9 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -4726,6 +4726,20 @@ public function test_wp_get_loading_optimization_attributes_with_fetchpriority_a ); $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'fetchpriority' => 'auto', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( + 'img', + array_merge( $attr, array( 'fetchpriority' => 'auto' ) ), + $context + ), + 'Expected a fetchpriority=auto IMG appearing after the media count threshold to till be lazy-loaded.' + ); } } From caaf92b34ab4c73ea50ce9bca8ecbb01e8e2aafd Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 9 Mar 2026 18:28:27 -0700 Subject: [PATCH 29/30] Fix typo in assertion message Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/phpunit/tests/media.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index 78ad36402dfa9..c569cbf1dcb43 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -4738,7 +4738,7 @@ public function test_wp_get_loading_optimization_attributes_with_fetchpriority_a array_merge( $attr, array( 'fetchpriority' => 'auto' ) ), $context ), - 'Expected a fetchpriority=auto IMG appearing after the media count threshold to till be lazy-loaded.' + 'Expected a fetchpriority=auto IMG appearing after the media count threshold to still be lazy-loaded.' ); } } From c8e469d03934fb66e33d0ec60b1d4afdd0ca27d0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 9 Mar 2026 18:28:50 -0700 Subject: [PATCH 30/30] Replicate assertion in companion test --- tests/phpunit/tests/media.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/phpunit/tests/media.php b/tests/phpunit/tests/media.php index c569cbf1dcb43..060a7295f6bb4 100644 --- a/tests/phpunit/tests/media.php +++ b/tests/phpunit/tests/media.php @@ -4601,6 +4601,20 @@ public function test_wp_get_loading_optimization_attributes( string $context ): ); $this->assert_fetchpriority_low_loading_attrs( $attr, $context ); + + $this->assertSameSetsWithIndex( + array( + 'decoding' => 'async', + 'fetchpriority' => 'auto', + 'loading' => 'lazy', + ), + wp_get_loading_optimization_attributes( + 'img', + array_merge( $attr, array( 'fetchpriority' => 'auto' ) ), + $context + ), + 'Expected a fetchpriority=auto IMG appearing after the media count threshold to still be lazy-loaded.' + ); } }