diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index 3b2636ee81..a1d02b98f1 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -81,6 +81,7 @@ private function is_embed_wrapper( OD_HTML_Tag_Processor $processor ): bool { * Otherwise, if the embed is not in any initial viewport, it will add lazy-loading logic. * * @since 0.2.0 + * @since n.e.x.t Adds preconnect links for each viewport group and skips if the element is not in the viewport for that group. * * @param OD_Tag_Visitor_Context $context Tag visitor context. * @return bool Whether the tag should be tracked in URL Metrics. @@ -109,7 +110,8 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { && $context->url_metric_group_collection->get_last_group()->count() > 0 ) { - $max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( self::get_embed_wrapper_xpath( $processor->get_xpath() ) ); + $embed_wrapper_xpath = self::get_embed_wrapper_xpath( $processor->get_xpath() ); + $max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( $embed_wrapper_xpath ); if ( $max_intersection_ratio > 0 ) { /* * The following embeds have been chosen for optimization due to their relative popularity among all embed types. @@ -171,12 +173,20 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { } foreach ( $preconnect_hrefs as $preconnect_href ) { - $context->link_collection->add_link( - array( - 'rel' => 'preconnect', - 'href' => $preconnect_href, - ) - ); + foreach ( $context->url_metric_group_collection as $group ) { + if ( ! ( $group->get_element_max_intersection_ratio( $embed_wrapper_xpath ) > 0.0 ) ) { + continue; + } + + $context->link_collection->add_link( + array( + 'rel' => 'preconnect', + 'href' => $preconnect_href, + ), + $group->get_minimum_viewport_width(), + $group->get_maximum_viewport_width() + ); + } } } elseif ( embed_optimizer_update_markup( $processor, false ) && ! $this->added_lazy_script ) { $processor->append_body_html( wp_get_inline_script_tag( embed_optimizer_get_lazy_load_script(), array( 'type' => 'module' ) ) ); diff --git a/plugins/embed-optimizer/hooks.php b/plugins/embed-optimizer/hooks.php index 452794d030..f61e439b45 100644 --- a/plugins/embed-optimizer/hooks.php +++ b/plugins/embed-optimizer/hooks.php @@ -42,7 +42,7 @@ function embed_optimizer_add_non_optimization_detective_hooks(): void { * @param string $optimization_detective_version Current version of the optimization detective plugin. */ function embed_optimizer_init_optimization_detective( string $optimization_detective_version ): void { - $required_od_version = '0.7.0'; + $required_od_version = '0.9.0'; if ( version_compare( (string) strtok( $optimization_detective_version, '-' ), $required_od_version, '<' ) ) { add_action( 'admin_notices', diff --git a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport-without-resized-data.php b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport-without-resized-data.php index 113950c837..37886051b9 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport-without-resized-data.php +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport-without-resized-data.php @@ -33,8 +33,8 @@ ... - +
diff --git a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport.php index 1e7d455393..04b94117d3 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport.php @@ -39,8 +39,8 @@ @media (min-width: 601px) and (max-width: 782px) { #embed-optimizer-a7659db28ecaa36ddee6ae66857dabd8 { min-height: 500px; } } @media (min-width: 783px) { #embed-optimizer-a7659db28ecaa36ddee6ae66857dabd8 { min-height: 500px; } } - +
diff --git a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport-on-mobile.php b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport-on-mobile.php new file mode 100644 index 0000000000..3c70222e49 --- /dev/null +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport-on-mobile.php @@ -0,0 +1,73 @@ + static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + foreach ( array_merge( od_get_breakpoint_max_widths(), array( 1000 ) ) as $i => $viewport_width ) { + $elements = array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => true, + 'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 + $i * 100 ) ), + ), + ); + + // Embed not visible on mobile. + if ( 480 === $viewport_width ) { + $elements[0]['intersectionRatio'] = 0; + $elements[0]['isLCP'] = false; + } + + $sample_size = od_get_url_metrics_breakpoint_sample_size(); + for ( $j = 0; $j < $sample_size; $j++ ) { + 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' => $viewport_width, + 'elements' => $elements, + ) + ) + ); + } + } + }, + 'buffer' => ' + + + + ... + + +
+
+ + +
+
+ + + ', + 'expected' => ' + + + + ... + + + + + +
+
+ + +
+
+ + + ', +); diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-inside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-inside-viewport.php index 1fd4ad7972..85a0e4e8e9 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-inside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-inside-viewport.php @@ -40,10 +40,10 @@ @media (min-width: 601px) and (max-width: 782px) { #embed-optimizer-a7659db28ecaa36ddee6ae66857dabd8 { min-height: 500px; } } @media (min-width: 783px) { #embed-optimizer-a7659db28ecaa36ddee6ae66857dabd8 { min-height: 500px; } } - - + +
diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport-on-mobile.php b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport-on-mobile.php new file mode 100644 index 0000000000..0755256069 --- /dev/null +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport-on-mobile.php @@ -0,0 +1,73 @@ + static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + foreach ( array_merge( od_get_breakpoint_max_widths(), array( 1000 ) ) as $i => $viewport_width ) { + $elements = array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => true, + 'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 + $i * 100 ) ), + ), + ); + + // Embed not visible on mobile. + if ( 480 === $viewport_width ) { + $elements[0]['intersectionRatio'] = 0; + $elements[0]['isLCP'] = false; + } + + 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' => $viewport_width, + 'elements' => $elements, + ) + ) + ); + } + }, + 'buffer' => ' + + + + ... + + +
+
+ + +
+
+ + + ', + 'expected' => ' + + + + ... + + + + + + + +
+
+ + +
+
+ + + + ', +); diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-inside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-inside-viewport.php index f0e62bba6d..5605d8c124 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-inside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-inside-viewport.php @@ -38,8 +38,8 @@ @media (min-width: 601px) and (max-width: 782px) { #embed-optimizer-a7659db28ecaa36ddee6ae66857dabd8 { min-height: 500px; } } @media (min-width: 783px) { #embed-optimizer-a7659db28ecaa36ddee6ae66857dabd8 { min-height: 500px; } } - +
diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport-on-mobile.php b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport-on-mobile.php new file mode 100644 index 0000000000..da1c4b8b47 --- /dev/null +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport-on-mobile.php @@ -0,0 +1,71 @@ + static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + foreach ( array_merge( od_get_breakpoint_max_widths(), array( 1000 ) ) as $viewport_width ) { + $elements = array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => true, + 'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 ) ), + ), + ); + + // Embed is ONLY visible on phablet and tablet. + if ( ! in_array( $viewport_width, array( 600, 782 ), true ) ) { + $elements[0]['intersectionRatio'] = 0; + $elements[0]['isLCP'] = false; + } + + $sample_size = od_get_url_metrics_breakpoint_sample_size(); + for ( $i = 0; $i < $sample_size; $i++ ) { + 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' => $viewport_width, + 'elements' => $elements, + ) + ) + ); + } + } + }, + 'buffer' => ' + + + + ... + + +
+
+ +
+
+ + + ', + 'expected' => ' + + + + ... + + + + + +
+
+ +
+
+ + + ', +); diff --git a/plugins/embed-optimizer/tests/test-hooks.php b/plugins/embed-optimizer/tests/test-hooks.php index b9b55d8e70..5c4f041b2e 100644 --- a/plugins/embed-optimizer/tests/test-hooks.php +++ b/plugins/embed-optimizer/tests/test-hooks.php @@ -63,7 +63,7 @@ public function data_provider_to_test_embed_optimizer_init_optimization_detectiv 'expected' => false, ), 'with_new_version' => array( - 'version' => '0.7.0', + 'version' => '99.0.0', 'expected' => true, ), ); diff --git a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group.php b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group.php index a651561a38..cb4c624413 100644 --- a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group.php +++ b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group.php @@ -62,8 +62,8 @@ ... - + Mobile Logo diff --git a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints.php b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints.php index 328d7d9160..1ca5817b2c 100644 --- a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints.php +++ b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints.php @@ -98,8 +98,8 @@ static function () { ... - + Mobile Logo diff --git a/plugins/image-prioritizer/tests/test-cases/responsive-background-images.php b/plugins/image-prioritizer/tests/test-cases/responsive-background-images.php index aabfab56ea..1f9d515776 100644 --- a/plugins/image-prioritizer/tests/test-cases/responsive-background-images.php +++ b/plugins/image-prioritizer/tests/test-cases/responsive-background-images.php @@ -56,9 +56,9 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array { ... + -
This is the desktop background!
diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet.php b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet.php index 3b4d59e85c..b8d3611277 100644 --- a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet.php +++ b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet.php @@ -88,8 +88,8 @@ static function () use ( $breakpoint_max_widths ) { ... - + Tablet header diff --git a/plugins/optimization-detective/class-od-html-tag-processor.php b/plugins/optimization-detective/class-od-html-tag-processor.php index 4b41d2a864..c4a74f44a3 100644 --- a/plugins/optimization-detective/class-od-html-tag-processor.php +++ b/plugins/optimization-detective/class-od-html-tag-processor.php @@ -273,7 +273,7 @@ public function next_token(): bool { $this->open_stack_tags = array(); $this->open_stack_indices = array(); - // Mark that the end of the document was reached, meaning that get_modified_html() can should now be able to append markup to the HEAD and the BODY. + // Mark that the end of the document was reached, meaning that get_modified_html() should now be able to append markup to the HEAD and the BODY. $this->reached_end_of_document = true; return false; } diff --git a/plugins/optimization-detective/class-od-link-collection.php b/plugins/optimization-detective/class-od-link-collection.php index 49b8669ed4..d6d7739fc3 100644 --- a/plugins/optimization-detective/class-od-link-collection.php +++ b/plugins/optimization-detective/class-od-link-collection.php @@ -130,18 +130,29 @@ function ( array $links ): array { */ private function merge_consecutive_links( array $links ): array { - // Ensure links are sorted by the minimum_viewport_width. usort( $links, /** * Comparator. * + * The links are sorted first by the 'href' attribute to group identical URLs together. + * If the 'href' attributes are the same, the links are then sorted by 'minimum_viewport_width'. + * * @param Link $a First link. * @param Link $b Second link. * @return int Comparison result. */ static function ( array $a, array $b ): int { - return $a['minimum_viewport_width'] <=> $b['minimum_viewport_width']; + // Get href values, defaulting to empty string if not present. + $href_a = $a['attributes']['href'] ?? ''; + $href_b = $b['attributes']['href'] ?? ''; + + $href_comparison = strcmp( $href_a, $href_b ); + if ( 0 === $href_comparison ) { + return $a['minimum_viewport_width'] <=> $b['minimum_viewport_width']; + } + + return $href_comparison; } ); 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 144f89a9c8..0832360cb9 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -461,9 +461,9 @@ public function get_xpath_elements_map(): array { $result = ( function () { $all_elements = array(); foreach ( $this->groups as $group ) { - foreach ( $group as $url_metric ) { - foreach ( $url_metric->get_elements() as $element ) { - $all_elements[ $element->get_xpath() ][] = $element; + foreach ( $group->get_xpath_elements_map() as $xpath => $elements ) { + foreach ( $elements as $element ) { + $all_elements[ $xpath ][] = $element; } } } @@ -488,12 +488,13 @@ public function get_all_element_max_intersection_ratios(): array { $result = ( function () { $elements_max_intersection_ratios = array(); - foreach ( $this->get_xpath_elements_map() as $xpath => $elements ) { - $element_intersection_ratios = array(); - foreach ( $elements as $element ) { - $element_intersection_ratios[] = $element->get_intersection_ratio(); + foreach ( $this->groups as $group ) { + foreach ( $group->get_all_element_max_intersection_ratios() as $xpath => $element_max_intersection_ratio ) { + $elements_max_intersection_ratios[ $xpath ] = (float) max( + $elements_max_intersection_ratios[ $xpath ] ?? 0, + $element_max_intersection_ratio + ); } - $elements_max_intersection_ratios[ $xpath ] = (float) max( $element_intersection_ratios ); } return $elements_max_intersection_ratios; } )(); diff --git a/plugins/optimization-detective/class-od-url-metric-group.php b/plugins/optimization-detective/class-od-url-metric-group.php index 0b2bbe444e..963f5b8840 100644 --- a/plugins/optimization-detective/class-od-url-metric-group.php +++ b/plugins/optimization-detective/class-od-url-metric-group.php @@ -72,7 +72,9 @@ final class OD_URL_Metric_Group implements IteratorAggregate, Countable, JsonSer * * @var array{ * get_lcp_element?: OD_Element|null, - * is_complete?: bool + * is_complete?: bool, + * get_xpath_elements_map?: array>, + * get_all_element_max_intersection_ratios?: array, * } */ private $result_cache = array(); @@ -320,6 +322,72 @@ public function get_lcp_element(): ?OD_Element { return $result; } + /** + * Gets all elements from all URL Metrics in the viewport group keyed by the elements' XPaths. + * + * @since n.e.x.t + * + * @return array> Keys are XPaths and values are the element instances. + */ + public function get_xpath_elements_map(): array { + if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) { + return $this->result_cache[ __FUNCTION__ ]; + } + + $result = ( function () { + $all_elements = array(); + foreach ( $this->url_metrics as $url_metric ) { + foreach ( $url_metric->get_elements() as $element ) { + $all_elements[ $element->get_xpath() ][] = $element; + } + } + return $all_elements; + } )(); + + $this->result_cache[ __FUNCTION__ ] = $result; + return $result; + } + + /** + * Gets the max intersection ratios of all elements in the viewport group and its captured URL Metrics. + * + * @since n.e.x.t + * + * @return array Keys are XPaths and values are the intersection ratios. + */ + public function get_all_element_max_intersection_ratios(): array { + if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) { + return $this->result_cache[ __FUNCTION__ ]; + } + + $result = ( function () { + $elements_max_intersection_ratios = array(); + foreach ( $this->get_xpath_elements_map() as $xpath => $elements ) { + $element_intersection_ratios = array(); + foreach ( $elements as $element ) { + $element_intersection_ratios[] = $element->get_intersection_ratio(); + } + $elements_max_intersection_ratios[ $xpath ] = (float) max( $element_intersection_ratios ); + } + return $elements_max_intersection_ratios; + } )(); + + $this->result_cache[ __FUNCTION__ ] = $result; + return $result; + } + + /** + * Gets the max intersection ratio of an element in the viewport group and its captured URL Metrics. + * + * @since n.e.x.t + * + * @param string $xpath XPath for the element. + * @return float|null Max intersection ratio of null if tag is unknown (not captured). + */ + public function get_element_max_intersection_ratio( string $xpath ): ?float { + return $this->get_all_element_max_intersection_ratios()[ $xpath ] ?? null; + } + /** * Returns an iterator for the URL Metrics in the group. * diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php index 3573d150fe..c2ccd3d0d3 100644 --- a/plugins/optimization-detective/helper.php +++ b/plugins/optimization-detective/helper.php @@ -37,7 +37,7 @@ function od_initialize_extensions(): void { */ function od_generate_media_query( ?int $minimum_viewport_width, ?int $maximum_viewport_width ): ?string { if ( is_int( $minimum_viewport_width ) && is_int( $maximum_viewport_width ) && $minimum_viewport_width > $maximum_viewport_width ) { - _doing_it_wrong( __FUNCTION__, esc_html__( 'The minimum width must be greater than the maximum width.', 'optimization-detective' ), 'Optimization Detective 0.7.0' ); + _doing_it_wrong( __FUNCTION__, esc_html__( 'The minimum width cannot be greater than the maximum width.', 'optimization-detective' ), 'Optimization Detective 0.7.0' ); return null; } $media_attributes = array(); diff --git a/plugins/optimization-detective/load.php b/plugins/optimization-detective/load.php index 602e4279c7..9753d24ed6 100644 --- a/plugins/optimization-detective/load.php +++ b/plugins/optimization-detective/load.php @@ -5,7 +5,7 @@ * Description: Provides an API for leveraging real user metrics to detect optimizations to apply on the frontend to improve page performance. * Requires at least: 6.5 * Requires PHP: 7.2 - * Version: 0.8.0 + * Version: 0.9.0-alpha * Author: WordPress Performance Team * Author URI: https://make.wordpress.org/performance/ * License: GPLv2 or later @@ -70,7 +70,7 @@ static function ( string $global_var_name, string $version, Closure $load ): voi } )( 'optimization_detective_pending_plugin', - '0.8.0', + '0.9.0-alpha', static function ( string $version ): void { if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) { return;