diff --git a/plugins/image-prioritizer/tests/test-cases/fetch-priority-high-on-lcp-image-common-on-mobile-and-desktop-with-url-metrics-missing-in-other-groups.php b/plugins/image-prioritizer/tests/test-cases/fetch-priority-high-on-lcp-image-common-on-mobile-and-desktop-with-url-metrics-missing-in-other-groups.php
new file mode 100644
index 0000000000..251a550fad
--- /dev/null
+++ b/plugins/image-prioritizer/tests/test-cases/fetch-priority-high-on-lcp-image-common-on-mobile-and-desktop-with-url-metrics-missing-in-other-groups.php
@@ -0,0 +1,68 @@
+ static function ( Test_Image_Prioritizer_Helper $test_case ): void {
+ $breakpoint_max_widths = array( 480, 600, 782 );
+
+ add_filter(
+ 'od_breakpoint_max_widths',
+ static function () use ( $breakpoint_max_widths ) {
+ return $breakpoint_max_widths;
+ }
+ );
+
+ OD_URL_Metrics_Post_Type::store_url_metric(
+ od_get_url_metrics_slug( od_get_normalized_query_vars() ),
+ $test_case->get_sample_url_metric(
+ array(
+ 'viewport_width' => 375,
+ 'elements' => array(
+ array(
+ 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
+ 'isLCP' => true,
+ ),
+ ),
+ )
+ )
+ );
+
+ OD_URL_Metrics_Post_Type::store_url_metric(
+ od_get_url_metrics_slug( od_get_normalized_query_vars() ),
+ $test_case->get_sample_url_metric(
+ array(
+ 'viewport_width' => 1000,
+ 'elements' => array(
+ array(
+ 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
+ 'isLCP' => true,
+ ),
+ ),
+ )
+ )
+ );
+ },
+ 'buffer' => '
+
+
+
+ ...
+
+
+
+
+
+ ',
+ 'expected' => '
+
+
+
+ ...
+
+
+
+
+
+
+
+
+ ',
+);
diff --git a/plugins/optimization-detective/class-od-url-metric-group-collection.php b/plugins/optimization-detective/class-od-url-metric-group-collection.php
index 9b50846eb6..ed7fe6928c 100644
--- a/plugins/optimization-detective/class-od-url-metric-group-collection.php
+++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php
@@ -427,6 +427,7 @@ public function get_groups_by_lcp_element( string $xpath ): array {
* Gets common LCP element.
*
* @since 0.3.0
+ * @since n.e.x.t An LCP element is also considered common if it is the same in the narrowest and widest viewport groups, and all intermediate groups are empty.
*
* @return OD_Element|null Common LCP element if it exists.
*/
@@ -437,38 +438,40 @@ public function get_common_lcp_element(): ?OD_Element {
$result = ( function () {
- // If every group isn't populated, then we can't say whether there is a common LCP element across every viewport group.
- if ( ! $this->is_every_group_populated() ) {
+ // Ensure both the narrowest (first) and widest (last) viewport groups are populated.
+ $first_group = $this->get_first_group();
+ $last_group = $this->get_last_group();
+ if ( $first_group->count() === 0 || $last_group->count() === 0 ) {
return null;
}
- // Look at the LCP elements across all the viewport groups.
- $groups_by_lcp_element_xpath = array();
- $lcp_elements_by_xpath = array();
- $group_has_unknown_lcp_element = false;
- foreach ( $this->groups as $group ) {
- $lcp_element = $group->get_lcp_element();
- if ( $lcp_element instanceof OD_Element ) {
- $groups_by_lcp_element_xpath[ $lcp_element->get_xpath() ][] = $group;
- $lcp_elements_by_xpath[ $lcp_element->get_xpath() ][] = $lcp_element;
- } else {
- $group_has_unknown_lcp_element = true;
- }
- }
+ $first_group_lcp_element = $first_group->get_lcp_element();
+ $last_group_lcp_element = $last_group->get_lcp_element();
+ // Validate LCP elements exist and have matching XPaths in the extreme viewport groups.
if (
- // All breakpoints share the same LCP element.
- 1 === count( $groups_by_lcp_element_xpath )
- &&
- // The breakpoints don't share a common lack of a detected LCP element.
- ! $group_has_unknown_lcp_element
+ ! $first_group_lcp_element instanceof OD_Element
+ ||
+ ! $last_group_lcp_element instanceof OD_Element
+ ||
+ $first_group_lcp_element->get_xpath() !== $last_group_lcp_element->get_xpath()
) {
- $xpath = key( $lcp_elements_by_xpath );
+ return null; // No common LCP element across the narrowest and widest viewports.
+ }
- return $lcp_elements_by_xpath[ $xpath ][0];
+ // Check intermediate viewport groups for conflicting LCP elements.
+ foreach ( array_slice( $this->groups, 1, -1 ) as $group ) {
+ $group_lcp_element = $group->get_lcp_element();
+ if (
+ $group_lcp_element instanceof OD_Element
+ &&
+ $group_lcp_element->get_xpath() !== $first_group_lcp_element->get_xpath()
+ ) {
+ return null; // Conflicting LCP element found in an intermediate group.
+ }
}
- return null;
+ return $first_group_lcp_element;
} )();
$this->result_cache[ __FUNCTION__ ] = $result;
diff --git a/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php b/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php
index 066f34f8b2..179d282c43 100644
--- a/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php
+++ b/plugins/optimization-detective/tests/test-class-od-url-metrics-group-collection.php
@@ -721,45 +721,119 @@ public function test_get_groups_by_lcp_element(): void {
$this->assertNull( $group_collection->get_common_lcp_element() );
}
+ /**
+ * Data provider.
+ *
+ * @return array
+ */
+ public function data_provider_test_get_common_lcp_element(): array {
+ $xpath1 = '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]/*[1]';
+ $xpath2 = '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]/*[2]';
+
+ $get_sample_url_metric = function ( int $viewport_width, string $lcp_element_xpath, bool $is_lcp = true ): OD_URL_Metric {
+ return $this->get_sample_url_metric(
+ array(
+ 'viewport_width' => $viewport_width,
+ 'element' => array(
+ 'isLCP' => $is_lcp,
+ 'xpath' => $lcp_element_xpath,
+ ),
+ )
+ );
+ };
+
+ return array(
+ 'all_groups_have_common_lcp' => array(
+ 'url_metrics' => array(
+ $get_sample_url_metric( 400, $xpath1 ),
+ $get_sample_url_metric( 600, $xpath1 ),
+ $get_sample_url_metric( 1000, $xpath1 ),
+ ),
+ 'expected' => $xpath1,
+ ),
+ 'no_url_metrics' => array(
+ 'url_metrics' => array(),
+ 'expected' => null,
+ ),
+ 'empty_first_group' => array(
+ 'url_metrics' => array(
+ $get_sample_url_metric( 600, $xpath1 ),
+ $get_sample_url_metric( 1000, $xpath1 ),
+ ),
+ 'expected' => null,
+ ),
+ 'empty_last_group' => array(
+ 'url_metrics' => array(
+ $get_sample_url_metric( 400, $xpath1 ),
+ $get_sample_url_metric( 600, $xpath1 ),
+ ),
+ 'expected' => null,
+ ),
+ 'first_and_last_common_lcp_others_empty' => array(
+ 'url_metrics' => array(
+ $get_sample_url_metric( 400, $xpath1 ),
+ $get_sample_url_metric( 1000, $xpath1 ),
+ ),
+ 'expected' => $xpath1,
+ ),
+ 'intermediate_groups_conflict' => array(
+ 'url_metrics' => array(
+ $get_sample_url_metric( 400, $xpath1 ),
+ $get_sample_url_metric( 600, $xpath2 ),
+ $get_sample_url_metric( 1000, $xpath1 ),
+ ),
+ 'expected' => null,
+ ),
+ 'first_and_last_lcp_mismatch' => array(
+ 'url_metrics' => array(
+ $get_sample_url_metric( 400, $xpath1 ),
+ $get_sample_url_metric( 600, $xpath1 ),
+ $get_sample_url_metric( 1000, $xpath2 ),
+ ),
+ 'expected' => null,
+ ),
+ 'no_lcp_metrics' => array(
+ 'url_metrics' => array(
+ $get_sample_url_metric( 400, $xpath1, false ),
+ $get_sample_url_metric( 600, $xpath1, false ),
+ $get_sample_url_metric( 1000, $xpath1, false ),
+ ),
+ 'expected' => null,
+ ),
+ );
+ }
+
/**
* Test get_common_lcp_element().
*
* @covers ::get_common_lcp_element
+ *
+ * @dataProvider data_provider_test_get_common_lcp_element
+ *
+ * @param OD_URL_Metric[] $url_metrics URL Metrics.
+ * @param string|null $expected Expected.
*/
- public function test_get_common_lcp_element(): void {
+ public function test_get_common_lcp_element( array $url_metrics, ?string $expected ): void {
$breakpoints = array( 480, 800 );
$sample_size = 3;
$current_etag = md5( '' );
$group_collection = new OD_URL_Metric_Group_Collection(
- array(),
+ $url_metrics,
$current_etag,
$breakpoints,
$sample_size,
HOUR_IN_SECONDS
);
- $lcp_element_xpath = '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]/*[1]';
-
- foreach ( array_merge( $breakpoints, array( 1000 ) ) as $viewport_width ) {
- for ( $i = 0; $i < $sample_size; $i++ ) {
- $group_collection->add_url_metric(
- $this->get_sample_url_metric(
- array(
- 'viewport_width' => $viewport_width,
- 'element' => array(
- 'isLCP' => true,
- 'xpath' => $lcp_element_xpath,
- ),
- )
- )
- );
- }
- }
-
$this->assertCount( 3, $group_collection );
+
$common_lcp_element = $group_collection->get_common_lcp_element();
- $this->assertInstanceOf( OD_Element::class, $common_lcp_element );
- $this->assertSame( $lcp_element_xpath, $common_lcp_element['xpath'] );
+ if ( is_string( $expected ) ) {
+ $this->assertInstanceOf( OD_Element::class, $common_lcp_element );
+ $this->assertSame( $expected, $common_lcp_element->get_xpath() );
+ } else {
+ $this->assertNull( $common_lcp_element );
+ }
}
/**
diff --git a/plugins/webp-uploads/helper.php b/plugins/webp-uploads/helper.php
index 312bd45d68..95fc15e18e 100644
--- a/plugins/webp-uploads/helper.php
+++ b/plugins/webp-uploads/helper.php
@@ -411,6 +411,17 @@ function webp_uploads_is_fallback_enabled(): bool {
return (bool) get_option( 'perflab_generate_webp_and_jpeg' );
}
+/**
+ * Checks if the `perflab_generate_all_fallback_sizes` option is enabled.
+ *
+ * @since n.e.x.t
+ *
+ * @return bool Whether the option is enabled. Default is false.
+ */
+function webp_uploads_should_generate_all_fallback_sizes(): bool {
+ return (bool) get_option( 'perflab_generate_all_fallback_sizes', 0 );
+}
+
/**
* Retrieves the image URL for a specified MIME type from the attachment metadata.
*
diff --git a/plugins/webp-uploads/hooks.php b/plugins/webp-uploads/hooks.php
index ce4c38a635..0e36bbca5a 100644
--- a/plugins/webp-uploads/hooks.php
+++ b/plugins/webp-uploads/hooks.php
@@ -806,3 +806,24 @@ function webp_uploads_opt_in_extra_image_sizes(): void {
}
}
add_action( 'plugins_loaded', 'webp_uploads_opt_in_extra_image_sizes' );
+
+/**
+ * Enables additional MIME type support for all image sizes based on the generate all fallback sizes settings.
+ *
+ * @since n.e.x.t
+ *
+ * @param array $allowed_sizes A map of image size names and whether they are allowed to have additional MIME types.
+ * @return array Modified map of image sizes with additional MIME type support.
+ */
+function webp_uploads_enable_additional_mime_type_support_for_all_sizes( array $allowed_sizes ): array {
+ if ( ! webp_uploads_should_generate_all_fallback_sizes() ) {
+ return $allowed_sizes;
+ }
+
+ foreach ( array_keys( $allowed_sizes ) as $size ) {
+ $allowed_sizes[ $size ] = true;
+ }
+
+ return $allowed_sizes;
+}
+add_filter( 'webp_uploads_image_sizes_with_additional_mime_type_support', 'webp_uploads_enable_additional_mime_type_support_for_all_sizes' );
diff --git a/plugins/webp-uploads/settings.php b/plugins/webp-uploads/settings.php
index c44d6c452a..cad0352e11 100644
--- a/plugins/webp-uploads/settings.php
+++ b/plugins/webp-uploads/settings.php
@@ -40,6 +40,18 @@ function webp_uploads_register_media_settings_field(): void {
'show_in_rest' => false,
)
);
+
+ // Add a setting to generate fallback images in all sizes including custom sizes.
+ register_setting(
+ 'media',
+ 'perflab_generate_all_fallback_sizes',
+ array(
+ 'type' => 'boolean',
+ 'default' => false,
+ 'show_in_rest' => false,
+ )
+ );
+
// Add a setting to use the picture element.
register_setting(
'media',
@@ -96,6 +108,16 @@ function webp_uploads_add_media_settings_fields(): void {
array( 'class' => 'perflab-generate-webp-and-jpeg' )
);
+ // Add setting field for generating fallback images in all sizes including custom sizes.
+ add_settings_field(
+ 'perflab_generate_all_fallback_sizes',
+ __( 'Generate all fallback image sizes', 'webp-uploads' ),
+ 'webp_uploads_generate_all_fallback_sizes_callback',
+ 'media',
+ 'perflab_modern_image_format_settings',
+ array( 'class' => 'perflab-generate-fallback-all-sizes' )
+ );
+
// Add picture element support settings field.
add_settings_field(
'webp_uploads_use_picture_element',
@@ -178,6 +200,94 @@ function webp_uploads_generate_webp_jpeg_setting_callback(): void {
+
+
+
+
+ markTestSkipped( 'Mime type ' . $mime_type . ' is not supported.' );
+ }
+
+ // Register custom image sizes.
+ add_image_size( 'custom_size_1', 200, 200, true );
+ add_image_size( 'custom_size_2', 400, 400, true );
+
+ // Generate image output type and fallback image.
+ update_option( 'perflab_generate_webp_and_jpeg', true );
+
+ // Generate fallback images for all sizes.
+ update_option( 'perflab_generate_all_fallback_sizes', true );
+
+ $this->set_image_output_type( $image_type );
+
+ $attachment_id = self::factory()->attachment->create_upload_object( TESTS_PLUGIN_DIR . '/tests/data/images/leaves.jpg' );
+
+ // Clean up custom sizes.
+ remove_image_size( 'custom_size_1' );
+ remove_image_size( 'custom_size_2' );
+
+ // Verify that fallback images are generated for custom sizes.
+ foreach ( array( 'custom_size_1', 'custom_size_2' ) as $size_name ) {
+ $this->assertImageHasSizeSource( $attachment_id, $size_name, 'image/jpeg' );
+ $this->assertImageHasSizeSource( $attachment_id, $size_name, $mime_type );
+ }
+
+ wp_delete_attachment( $attachment_id );
+ }
}
diff --git a/plugins/webp-uploads/uninstall.php b/plugins/webp-uploads/uninstall.php
index 46fba3c78d..8b3fae0aee 100644
--- a/plugins/webp-uploads/uninstall.php
+++ b/plugins/webp-uploads/uninstall.php
@@ -38,4 +38,5 @@
*/
function webp_uploads_delete_plugin_option(): void {
delete_option( 'perflab_generate_webp_and_jpeg' );
+ delete_option( 'perflab_generate_all_fallback_sizes' );
}