From 0a376c1b224a28eec1e401aac9441cc4a746599a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 17 Jul 2024 14:24:01 -0700 Subject: [PATCH 001/156] Introduce methods to get minumum height of element --- .../class-od-url-metrics-group-collection.php | 52 ++++++++++- ...-class-od-url-metrics-group-collection.php | 87 +++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) diff --git a/plugins/optimization-detective/class-od-url-metrics-group-collection.php b/plugins/optimization-detective/class-od-url-metrics-group-collection.php index 21bce9a0c5..752e66c8d7 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metrics-group-collection.php @@ -80,7 +80,8 @@ final class OD_URL_Metrics_Group_Collection implements Countable, IteratorAggreg * is_every_group_complete?: bool, * get_groups_by_lcp_element?: array, * get_common_lcp_element?: ElementData|null, - * get_all_element_max_intersection_ratios?: array + * get_all_element_max_intersection_ratios?: array, + * get_all_element_minimum_heights?: array * } */ private $result_cache = array(); @@ -430,6 +431,43 @@ public function get_all_element_max_intersection_ratios(): array { return $result; } + /** + * Gets the minimum heights of all elements across all groups and their captured URL metrics. + * + * @since n.e.x.t + * + * @return array Keys are XPaths and values are the minimum heights. + */ + public function get_all_element_minimum_heights(): array { + if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) { + return $this->result_cache[ __FUNCTION__ ]; + } + + $result = ( function () { + $element_min_heights = array(); + + /* + * O(n^3) my! Yes. This is why the result is cached. This being said, the number of groups should be 4 (one + * more than the default number of breakpoints) and the number of URL metrics for each group should be 3 + * (the default sample size). Therefore, given the number (n) of visited elements on the page this will only + * end up running n*4*3 times. + */ + foreach ( $this->groups as $group ) { + foreach ( $group as $url_metric ) { + foreach ( $url_metric->get_elements() as $element ) { + $element_min_heights[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_min_heights ) + ? min( $element_min_heights[ $element['xpath'] ], $element['intersectionRect']['height'] ) + : $element['intersectionRect']['height']; + } + } + } + return $element_min_heights; + } )(); + + $this->result_cache[ __FUNCTION__ ] = $result; + return $result; + } + /** * Gets the max intersection ratio of an element across all groups and their captured URL metrics. * @@ -440,6 +478,18 @@ public function get_element_max_intersection_ratio( string $xpath ): ?float { return $this->get_all_element_max_intersection_ratios()[ $xpath ] ?? null; } + /** + * Gets the minimum height of an element across all groups and their captured URL metrics. + * + * @since n.e.x.t + * + * @param string $xpath XPath for the element. + * @return float Minimum height in pixels. + */ + public function get_element_minimum_height( string $xpath ): ?float { + return $this->get_all_element_minimum_heights()[ $xpath ] ?? null; + } + /** * Gets URL metrics from all groups flattened into one list. * 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 3d97456a9a..de5ba3884d 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 @@ -720,6 +720,93 @@ public function test_get_all_element_max_intersection_ratios( array $url_metrics } } + /** + * Data provider. + * + * @return array + */ + public function data_provider_element_minimum_heights(): array { + $xpath1 = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[1]'; + $xpath2 = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[2]'; + $xpath3 = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[3]'; + + $get_sample_url_metric = function ( int $viewport_width, string $lcp_element_xpath, float $element_height ): OD_URL_Metric { + return $this->get_sample_url_metric( + array( + 'viewport_width' => $viewport_width, + 'element' => array( + 'isLCP' => true, + 'xpath' => $lcp_element_xpath, + 'intersectionRect' => array_merge( + $this->get_sample_dom_rect(), + array( 'height' => $element_height ) + ), + ), + ) + ); + }; + + return array( + 'one-element-sample-size-one' => array( + 'url_metrics' => array( + $get_sample_url_metric( 400, $xpath1, 480 ), + $get_sample_url_metric( 600, $xpath1, 240 ), + $get_sample_url_metric( 800, $xpath1, 768 ), + ), + 'expected' => array( + $xpath1 => 240.0, + ), + ), + 'three-elements-sample-size-two' => array( + 'url_metrics' => array( + // Group 1. + $get_sample_url_metric( 400, $xpath1, 400 ), + $get_sample_url_metric( 400, $xpath1, 600 ), + // Group 2. + $get_sample_url_metric( 600, $xpath2, 100.1 ), + $get_sample_url_metric( 600, $xpath2, 100.2 ), + $get_sample_url_metric( 600, $xpath2, 100.05 ), + // Group 3. + $get_sample_url_metric( 800, $xpath3, 500 ), + $get_sample_url_metric( 800, $xpath3, 500 ), + ), + 'expected' => array( + $xpath1 => 400.0, + $xpath2 => 100.05, + $xpath3 => 500.0, + ), + ), + 'no-url-metrics' => array( + 'url_metrics' => array(), + 'expected' => array(), + ), + + ); + } + + /** + * Test get_all_element_max_intersection_ratios() and get_element_max_intersection_ratio(). + * + * @covers ::get_all_element_minimum_heights + * @covers ::get_element_minimum_height + * + * @dataProvider data_provider_element_minimum_heights + * + * @param array $url_metrics URL metrics. + * @param array $expected Expected. + */ + public function test_get_all_element_minimum_heights( array $url_metrics, array $expected ): void { + $breakpoints = array( 480, 600, 782 ); + $sample_size = 3; + $group_collection = new OD_URL_Metrics_Group_Collection( $url_metrics, $breakpoints, $sample_size, 0 ); + $actual = $group_collection->get_all_element_minimum_heights(); + $this->assertSame( $actual, $group_collection->get_all_element_minimum_heights(), 'Cached result is identical.' ); + $this->assertSame( $expected, $actual ); + foreach ( $expected as $expected_xpath => $expected_max_ratio ) { + $this->assertSame( $expected_max_ratio, $group_collection->get_element_minimum_height( $expected_xpath ) ); + } + } + /** * Test get_flattened_url_metrics(). * From 8aa7e638368a8d13f063f6b3448cec07be01f753 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Jul 2024 16:14:37 -0700 Subject: [PATCH 002/156] Set the min-height of an embed prior to it loading --- .../class-embed-optimizer-tag-visitor.php | 12 ++++++++++++ .../tests/test-cases/nested-figure-embed.php | 4 ++-- ...embed-outside-viewport-with-subsequent-script.php | 2 +- .../single-twitter-embed-inside-viewport.php | 2 +- .../single-twitter-embed-outside-viewport.php | 2 +- .../single-wordpress-tv-embed-inside-viewport.php | 2 +- .../single-wordpress-tv-embed-outside-viewport.php | 2 +- .../single-youtube-embed-inside-viewport.php | 2 +- .../single-youtube-embed-outside-viewport.php | 2 +- 9 files changed, 21 insertions(+), 9 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index 4791b845cf..a1165b2d5f 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -44,6 +44,18 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { return false; } + $minimum_height = $context->url_metrics_group_collection->get_element_minimum_height( $processor->get_xpath() ); + if ( is_float( $minimum_height ) ) { + $style = $processor->get_attribute( 'style' ); + if ( is_string( $style ) ) { + $style = rtrim( trim( $style ), ';' ) . '; '; + } else { + $style = ''; + } + $style .= sprintf( 'min-height: %dpx;', $minimum_height ); + $processor->set_attribute( 'style', $style ); + } + $max_intersection_ratio = $context->url_metrics_group_collection->get_element_max_intersection_ratio( $processor->get_xpath() ); if ( $max_intersection_ratio > 0 ) { diff --git a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php index f460fc9030..42fe1c7887 100644 --- a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php +++ b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php @@ -101,12 +101,12 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool { -
+
-
+

So I heard you like FIGURE?

diff --git a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php index 2867ff1303..06c1172600 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php +++ b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php @@ -35,7 +35,7 @@ ... -
+
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 1336dcfcbd..5db811959f 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 @@ -36,7 +36,7 @@ -
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php index 3da5a29fac..c480e5b10c 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php @@ -34,7 +34,7 @@ ... -
+
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 5992973bf4..800f77cded 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 @@ -39,7 +39,7 @@ -
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php index a48ed16b11..4ec5a1dbb7 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php @@ -35,7 +35,7 @@ ... -
+
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 c4ed8b770c..ef9b0f0fa3 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 @@ -35,7 +35,7 @@ -
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php index 85c53e81bf..9641a15b00 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php @@ -33,7 +33,7 @@ ... -
+
From 11f98f45a302e6e51202ffdd2732bebae8a7de14 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Jul 2024 17:35:10 -0700 Subject: [PATCH 003/156] Set min-height on embed-wrapper instead of figure container --- .../class-embed-optimizer-tag-visitor.php | 64 ++++++++++++++++--- .../tests/test-cases/nested-figure-embed.php | 12 ++-- ...utside-viewport-with-subsequent-script.php | 6 +- .../single-twitter-embed-inside-viewport.php | 2 +- .../single-twitter-embed-outside-viewport.php | 2 +- ...gle-wordpress-tv-embed-inside-viewport.php | 6 +- ...le-wordpress-tv-embed-outside-viewport.php | 6 +- .../single-youtube-embed-inside-viewport.php | 2 +- .../single-youtube-embed-outside-viewport.php | 2 +- .../tests/test-cases/too-many-bookmarks.php | 4 +- 10 files changed, 75 insertions(+), 31 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index a1165b2d5f..7faff55504 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -26,25 +26,64 @@ final class Embed_Optimizer_Tag_Visitor { */ protected $added_lazy_script = false; + /** + * Determines whether the processor is currently at a figure.wp-block-embed tag. + * + * @since n.e.x.t + * + * @param OD_HTML_Tag_Processor $processor Processor. + * @return bool Whether at the tag. + */ + private function is_embed_figure( OD_HTML_Tag_Processor $processor ): bool { + return ( + 'FIGURE' === $processor->get_tag() + && + true === $processor->has_class( 'wp-block-embed' ) + ); + } + + /** + * Determines whether the processor is currently at a div.wp-block-embed__wrapper tag. + * + * @since n.e.x.t + * + * @param OD_HTML_Tag_Processor $processor Processor. + * @return bool Whether the tag should be measured and stored in URL metrics + */ + private function is_embed_wrapper( OD_HTML_Tag_Processor $processor ): bool { + return ( + 'DIV' === $processor->get_tag() + && + true === $processor->has_class( 'wp-block-embed__wrapper' ) + ); + } + /** * Visits a tag. * * @since 0.2.0 * * @param OD_Tag_Visitor_Context $context Tag visitor context. - * @return bool Whether the visit or visited the tag. + * @return bool Whether the tag should be measured and stored in URL metrics. */ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $processor = $context->processor; - if ( ! ( - 'FIGURE' === $processor->get_tag() - && - true === $processor->has_class( 'wp-block-embed' ) - ) ) { + + /* + * The only thing we need to do if it is a div.wp-block-embed__wrapper tag is return true so that the tag + * will get measured and stored in the URL Metrics. + */ + if ( $this->is_embed_wrapper( $processor ) ) { + return true; + } + + // Short-circuit if not a figure.wp-block-embed tag. + if ( ! $this->is_embed_figure( $processor ) ) { return false; } - $minimum_height = $context->url_metrics_group_collection->get_element_minimum_height( $processor->get_xpath() ); + $embed_wrapper_xpath = $processor->get_xpath() . '/*[1][self::DIV]'; + $minimum_height = $context->url_metrics_group_collection->get_element_minimum_height( $embed_wrapper_xpath ); if ( is_float( $minimum_height ) ) { $style = $processor->get_attribute( 'style' ); if ( is_string( $style ) ) { @@ -56,8 +95,7 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $processor->set_attribute( 'style', $style ); } - $max_intersection_ratio = $context->url_metrics_group_collection->get_element_max_intersection_ratio( $processor->get_xpath() ); - + $max_intersection_ratio = $context->url_metrics_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. @@ -131,6 +169,12 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $this->added_lazy_script = true; } - return true; + /* + * At this point the tag is a figure.wp-block-embed, and we can return false because this does not need to be + * measured and stored in URL Metrics. Only the child div.wp-block-embed__wrapper tag is measured and stored + * so that this visitor can look up the height to set as a min-height on the figure.wp-block-embed. For more + * information on what the return values mean for tag visitors, see . + */ + return false; } } diff --git a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php index 42fe1c7887..30bd54a190 100644 --- a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php +++ b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php @@ -4,7 +4,7 @@ $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 1, ), @@ -14,7 +14,7 @@ 'intersectionRatio' => 1, ), array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::FIGURE]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::FIGURE]/*[1][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0, ), @@ -101,13 +101,13 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool { -
-
+
+
-
-
+
+

So I heard you like FIGURE?

diff --git a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php index 06c1172600..b25fb71c53 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php +++ b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php @@ -4,7 +4,7 @@ $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0, ), @@ -35,8 +35,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 5db811959f..fb5b39df1c 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 @@ -4,7 +4,7 @@ $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', 'isLCP' => true, 'intersectionRatio' => 1, ), diff --git a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php index c480e5b10c..81bf2c2ae9 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php @@ -4,7 +4,7 @@ $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0, ), 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 800f77cded..b802cb8632 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 @@ -4,7 +4,7 @@ $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', 'isLCP' => true, 'intersectionRatio' => 1, ), @@ -39,8 +39,8 @@ -
-
+
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php index 4ec5a1dbb7..f931034b62 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php @@ -4,7 +4,7 @@ $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0, ), @@ -35,8 +35,8 @@ ... -
-
+
+
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 ef9b0f0fa3..df31dce205 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 @@ -4,7 +4,7 @@ $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', 'isLCP' => true, 'intersectionRatio' => 1, ), diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php index 9641a15b00..89151656d9 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php @@ -4,7 +4,7 @@ $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', 'isLCP' => false, 'intersectionRatio' => 0, ), diff --git a/plugins/embed-optimizer/tests/test-cases/too-many-bookmarks.php b/plugins/embed-optimizer/tests/test-cases/too-many-bookmarks.php index 76df5b30e6..696f16c603 100644 --- a/plugins/embed-optimizer/tests/test-cases/too-many-bookmarks.php +++ b/plugins/embed-optimizer/tests/test-cases/too-many-bookmarks.php @@ -59,8 +59,8 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool { ... -
-
+
+
From d7cc7cfc959715e64b16c5b55f535aecd27cd137 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Jul 2024 17:39:48 -0700 Subject: [PATCH 004/156] Use 500px as a better representation of an element that could be LCP --- .../embed-optimizer/tests/test-cases/nested-figure-embed.php | 4 ++-- ...-spotify-embed-outside-viewport-with-subsequent-script.php | 2 +- .../tests/test-cases/single-twitter-embed-inside-viewport.php | 2 +- .../test-cases/single-twitter-embed-outside-viewport.php | 2 +- .../test-cases/single-wordpress-tv-embed-inside-viewport.php | 2 +- .../test-cases/single-wordpress-tv-embed-outside-viewport.php | 2 +- .../tests/test-cases/single-youtube-embed-inside-viewport.php | 2 +- .../test-cases/single-youtube-embed-outside-viewport.php | 2 +- tests/class-optimization-detective-test-helpers.php | 4 ++-- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php index 30bd54a190..685a496e91 100644 --- a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php +++ b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php @@ -101,12 +101,12 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool { -
+
-
+

So I heard you like FIGURE?

diff --git a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php index b25fb71c53..523f85e6e7 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php +++ b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php @@ -35,7 +35,7 @@ ... -
+
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 fb5b39df1c..2279286a1a 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 @@ -36,7 +36,7 @@ -
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php index 81bf2c2ae9..62088e25f9 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php @@ -34,7 +34,7 @@ ... -
+
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 b802cb8632..9b6234d485 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 @@ -39,7 +39,7 @@ -
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php index f931034b62..f9087a8399 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php @@ -35,7 +35,7 @@ ... -
+
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 df31dce205..6c322bc701 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 @@ -35,7 +35,7 @@ -
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php index 89151656d9..729f25ef2c 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php @@ -33,7 +33,7 @@ ... -
+
diff --git a/tests/class-optimization-detective-test-helpers.php b/tests/class-optimization-detective-test-helpers.php index eaa9210270..ce04b2ac60 100644 --- a/tests/class-optimization-detective-test-helpers.php +++ b/tests/class-optimization-detective-test-helpers.php @@ -50,8 +50,8 @@ public function populate_url_metrics( array $elements, bool $complete = true ): */ public function get_sample_dom_rect(): array { return array( - 'width' => 100.1, - 'height' => 100.2, + 'width' => 500.1, + 'height' => 500.2, 'x' => 100.3, 'y' => 100.4, 'top' => 0.1, From 48e57e803dda71ff95587435bda7988a0037e2fc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 29 Jul 2024 17:45:51 -0700 Subject: [PATCH 005/156] Add test for existing style manipulation --- .../tests/test-cases/nested-figure-embed.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php index 685a496e91..2878eceacc 100644 --- a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php +++ b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php @@ -74,12 +74,12 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool { ... -
+
-
+

So I heard you like FIGURE?

@@ -101,12 +101,12 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool { -
+
-
+

So I heard you like FIGURE?

From 0d285f2297c60c337d1ebf407b8bb10035688770 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 30 Jul 2024 17:09:54 -0700 Subject: [PATCH 006/156] Add helper generator method to get all elements --- .../class-od-url-metrics-group-collection.php | 59 ++++++++++--------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metrics-group-collection.php b/plugins/optimization-detective/class-od-url-metrics-group-collection.php index 752e66c8d7..9f32810858 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metrics-group-collection.php @@ -396,6 +396,28 @@ public function get_common_lcp_element(): ?array { return $result; } + /** + * Gets all elements from all URL metrics from all groups. + * + * This is an O(n^3) function so its results must be cached. This being said, the number of groups should be 4 (one + * more than the default number of breakpoints) and the number of URL metrics for each group should be 3 + * (the default sample size). Therefore, given the number (n) of visited elements on the page this will only + * end up running n*4*3 times. + * + * @since n.e.x.t + * + * @return Generator + */ + protected function get_all_url_metrics_groups_elements(): Generator { + foreach ( $this->groups as $group ) { + foreach ( $group as $url_metric ) { + foreach ( $url_metric->get_elements() as $element ) { + yield $element; + } + } + } + } + /** * Gets the max intersection ratios of all elements across all groups and their captured URL metrics. * @@ -408,21 +430,10 @@ public function get_all_element_max_intersection_ratios(): array { $result = ( function () { $element_max_intersection_ratios = array(); - - /* - * O(n^3) my! Yes. This is why the result is cached. This being said, the number of groups should be 4 (one - * more than the default number of breakpoints) and the number of URL metrics for each group should be 3 - * (the default sample size). Therefore, given the number (n) of visited elements on the page this will only - * end up running n*4*3 times. - */ - foreach ( $this->groups as $group ) { - foreach ( $group as $url_metric ) { - foreach ( $url_metric->get_elements() as $element ) { - $element_max_intersection_ratios[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_max_intersection_ratios ) - ? max( $element_max_intersection_ratios[ $element['xpath'] ], $element['intersectionRatio'] ) - : $element['intersectionRatio']; - } - } + foreach ( $this->get_all_url_metrics_groups_elements() as $element ) { + $element_max_intersection_ratios[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_max_intersection_ratios ) + ? max( $element_max_intersection_ratios[ $element['xpath'] ], $element['intersectionRatio'] ) + : $element['intersectionRatio']; } return $element_max_intersection_ratios; } )(); @@ -446,20 +457,10 @@ public function get_all_element_minimum_heights(): array { $result = ( function () { $element_min_heights = array(); - /* - * O(n^3) my! Yes. This is why the result is cached. This being said, the number of groups should be 4 (one - * more than the default number of breakpoints) and the number of URL metrics for each group should be 3 - * (the default sample size). Therefore, given the number (n) of visited elements on the page this will only - * end up running n*4*3 times. - */ - foreach ( $this->groups as $group ) { - foreach ( $group as $url_metric ) { - foreach ( $url_metric->get_elements() as $element ) { - $element_min_heights[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_min_heights ) - ? min( $element_min_heights[ $element['xpath'] ], $element['intersectionRect']['height'] ) - : $element['intersectionRect']['height']; - } - } + foreach ( $this->get_all_url_metrics_groups_elements() as $element ) { + $element_min_heights[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_min_heights ) + ? min( $element_min_heights[ $element['xpath'] ], $element['intersectionRect']['height'] ) + : $element['intersectionRect']['height']; } return $element_min_heights; } )(); From 4778a3d7df6cbc9672ee82cf7308d532dcbf9f34 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 13 Aug 2024 17:48:43 -0700 Subject: [PATCH 007/156] Try using MutationObserve to watch for embed height changes --- plugins/optimization-detective/detect.js | 49 ++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 59b4657857..924611136c 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -199,6 +199,55 @@ export default async function detect( { } } ); + // TODO: Conditionally add this JS only when we know there are embeds on the page. + // TODO: This JS should be provided by Embed Optimizer and not bundled in Optimization Detective. It should update the pending url metrics before they are sent to the server when the page is hidden. + // TODO: This is only necessary for embeds that contain scripting. + /* + * Observe the loading of embeds on the page. We need to run this now before the resources on the page have fully + * loaded because we need to start observing the embed wrappers before the embeds have loaded. When we detect + * subtree modifications in an embed wrapper, we then need to measure the new height of the wrapper element. + * However, since there may be multiple subtree modifications performed as an embed is loaded, we need to wait until + * what is likely the last mutation. + */ + const EMBED_LOAD_WAIT_MS = 1000; + const embedWrappers = + /** @type NodeListOf */ document.querySelectorAll( + '.wp-block-embed > .wp-block-embed__wrapper[data-od-xpath]' + ); + + /** + * Monitors embed for mutations. + * + * @param {HTMLDivElement} embedWrapper Embed wrapper DIV. + */ + function monitorEmbedForMutations( embedWrapper ) { + // If the embed lacks any scripting, then short-circuit since it can't possibly be doing any mutations. + if ( ! embedWrapper.querySelector( 'script, [onload]' ) ) { + return; + } + if ( ! ( 'odXpath' in embedWrapper.dataset ) ) { + throw new Error( 'Embed wrapper missing data-od-xpath attribute.' ); + } + const xpath = embedWrapper.dataset.odXpath; + let timeoutId = 0; + const observer = new MutationObserver( () => { + if ( timeoutId > 0 ) { + clearTimeout( timeoutId ); + } + timeoutId = setTimeout( () => { + const rect = embedWrapper.getBoundingClientRect(); + log( + `[Embed Optimizer] Embed height of ${ rect.height }px for ${ xpath }` + ); + // TODO: Now amend URL metrics with this rect.height. + }, EMBED_LOAD_WAIT_MS ); + } ); + observer.observe( embedWrapper, { childList: true, subtree: true } ); + } + for ( const embedWrapper of embedWrappers ) { + monitorEmbedForMutations( embedWrapper ); + } + // Wait until the resources on the page have fully loaded. await new Promise( ( resolve ) => { if ( doc.readyState === 'complete' ) { From 1dbd4a1f0d557d2a7dd9d02fd752682b4a6d5908 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 13 Aug 2024 18:02:13 -0700 Subject: [PATCH 008/156] Use the more appropriate ResizeObserver instead of MutationObserver --- plugins/optimization-detective/detect.js | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 924611136c..edf59de516 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -216,11 +216,11 @@ export default async function detect( { ); /** - * Monitors embed for mutations. + * Monitors embed wrapper for resizes. * * @param {HTMLDivElement} embedWrapper Embed wrapper DIV. */ - function monitorEmbedForMutations( embedWrapper ) { + function monitorEmbedWrapperForResizes( embedWrapper ) { // If the embed lacks any scripting, then short-circuit since it can't possibly be doing any mutations. if ( ! embedWrapper.querySelector( 'script, [onload]' ) ) { return; @@ -230,22 +230,27 @@ export default async function detect( { } const xpath = embedWrapper.dataset.odXpath; let timeoutId = 0; - const observer = new MutationObserver( () => { + const observer = new ResizeObserver( ( entries ) => { + const [ entry ] = entries; if ( timeoutId > 0 ) { clearTimeout( timeoutId ); } + log( + `[Embed Optimizer] Pending embed height of ${ entry.contentRect.height }px for ${ xpath }` + ); + // TODO: Is the timeout really needed? We can just keep updating the height of the element until the URL metrics are sent when the page closes. timeoutId = setTimeout( () => { - const rect = embedWrapper.getBoundingClientRect(); log( - `[Embed Optimizer] Embed height of ${ rect.height }px for ${ xpath }` + `[Embed Optimizer] Final embed height of ${ entry.contentRect.height }px for ${ xpath }` ); + observer.disconnect(); // TODO: Now amend URL metrics with this rect.height. }, EMBED_LOAD_WAIT_MS ); } ); - observer.observe( embedWrapper, { childList: true, subtree: true } ); + observer.observe( embedWrapper, { box: 'content-box' } ); } for ( const embedWrapper of embedWrappers ) { - monitorEmbedForMutations( embedWrapper ); + monitorEmbedWrapperForResizes( embedWrapper ); } // Wait until the resources on the page have fully loaded. From a4bab7ea186943e18b9c61e344bb52daa635d36f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 13 Aug 2024 18:03:40 -0700 Subject: [PATCH 009/156] Remove condition that breaks monitoring resizes of post embeds --- plugins/optimization-detective/detect.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index edf59de516..317d940d59 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -221,10 +221,6 @@ export default async function detect( { * @param {HTMLDivElement} embedWrapper Embed wrapper DIV. */ function monitorEmbedWrapperForResizes( embedWrapper ) { - // If the embed lacks any scripting, then short-circuit since it can't possibly be doing any mutations. - if ( ! embedWrapper.querySelector( 'script, [onload]' ) ) { - return; - } if ( ! ( 'odXpath' in embedWrapper.dataset ) ) { throw new Error( 'Embed wrapper missing data-od-xpath attribute.' ); } From 5f0cdbed405834e74c1563828866975666441225 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 17 Aug 2024 16:24:26 -0700 Subject: [PATCH 010/156] Introduce client-side Optimization Detective extensions and move Embed Optimizer JS into one --- plugins/embed-optimizer/detect.js | 91 +++++++++++++++++++ plugins/embed-optimizer/hooks.php | 46 +++++++++- plugins/embed-optimizer/tests/test-hooks.php | 10 +-- plugins/optimization-detective/detect.js | 92 +++++++++----------- plugins/optimization-detective/detection.php | 10 +++ 5 files changed, 187 insertions(+), 62 deletions(-) create mode 100644 plugins/embed-optimizer/detect.js diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js new file mode 100644 index 0000000000..ca539ca8a9 --- /dev/null +++ b/plugins/embed-optimizer/detect.js @@ -0,0 +1,91 @@ +const consoleLogPrefix = '[Embed Optimizer]'; + +/** + * Log a message. + * + * @param {...*} message + */ +function log( ...message ) { + // eslint-disable-next-line no-console + console.log( consoleLogPrefix, ...message ); +} + +/* + * Observe the loading of embeds on the page. We need to run this now before the resources on the page have fully + * loaded because we need to start observing the embed wrappers before the embeds have loaded. When we detect + * subtree modifications in an embed wrapper, we then need to measure the new height of the wrapper element. + * However, since there may be multiple subtree modifications performed as an embed is loaded, we need to wait until + * what is likely the last mutation. + */ +const EMBED_LOAD_WAIT_MS = 1000; + +/** + * Initialize. + * + * @param {Object} args Args. + * @param {boolean} args.isDebug Whether to show debug messages. + */ +export async function initialize( { isDebug } ) { + const embedWrappers = + /** @type NodeListOf */ document.querySelectorAll( + '.wp-block-embed > .wp-block-embed__wrapper[data-od-xpath]' + ); + + for ( const embedWrapper of embedWrappers ) { + monitorEmbedWrapperForResizes( embedWrapper ); + } + + if ( isDebug ) { + log( embedWrappers ); + } + + // TODO: It should update the pending url metrics before they are sent to the server when the page is hidden. +} + +/** + * Initialize. + * + * @todo Add typing for args.urlMetric + * + * @param {Object} args Args. + * @param {boolean} args.isDebug Whether to show debug messages. + * @param {Object} args.urlMetric Pending URL metric. + */ +export async function finalize( { urlMetric, isDebug } ) { + if ( isDebug ) { + log( 'URL metric to be sent:', urlMetric ); + } + + // TODO: Finalize. +} + +/** + * Monitors embed wrapper for resizes. + * + * @param {HTMLDivElement} embedWrapper Embed wrapper DIV. + */ +function monitorEmbedWrapperForResizes( embedWrapper ) { + if ( ! ( 'odXpath' in embedWrapper.dataset ) ) { + throw new Error( 'Embed wrapper missing data-od-xpath attribute.' ); + } + const xpath = embedWrapper.dataset.odXpath; + let timeoutId = 0; + const observer = new ResizeObserver( ( entries ) => { + const [ entry ] = entries; + if ( timeoutId > 0 ) { + clearTimeout( timeoutId ); + } + log( + `Pending embed height of ${ entry.contentRect.height }px for ${ xpath }` + ); + // TODO: Is the timeout really needed? We can just keep updating the height of the element until the URL metrics are sent when the page closes. + timeoutId = setTimeout( () => { + log( + `Final embed height of ${ entry.contentRect.height }px for ${ xpath }` + ); + observer.disconnect(); + // TODO: Now amend URL metrics with this rect.height. + }, EMBED_LOAD_WAIT_MS ); + } ); + observer.observe( embedWrapper, { box: 'content-box' } ); +} diff --git a/plugins/embed-optimizer/hooks.php b/plugins/embed-optimizer/hooks.php index 0e4e9e39fe..fd59a89879 100644 --- a/plugins/embed-optimizer/hooks.php +++ b/plugins/embed-optimizer/hooks.php @@ -20,8 +20,9 @@ function embed_optimizer_add_hooks(): void { if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) { add_action( 'od_register_tag_visitors', 'embed_optimizer_register_tag_visitors' ); + add_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_detect_embed_presence' ); } else { - add_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html' ); + add_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_lazy_load' ); } } add_action( 'init', 'embed_optimizer_add_hooks' ); @@ -40,17 +41,54 @@ function embed_optimizer_register_tag_visitors( OD_Tag_Visitor_Registry $registr } /** - * Filter the oEmbed HTML. + * Filters the list of Optimization Detective extension module URLs to include the extension for Embed Optimizer. + * + * @since n.e.x.t + * + * @param string[]|mixed $extension_module_urls Extension module URLs. + * @return string[] Extension module URLs. + */ +function embed_optimizer_filter_extension_module_urls( $extension_module_urls ): array { + if ( ! is_array( $extension_module_urls ) ) { + $extension_module_urls = array(); + } + $extension_module_urls[] = add_query_arg( 'ver', EMBED_OPTIMIZER_VERSION, plugin_dir_url( __FILE__ ) . 'detect.js' ); + return $extension_module_urls; +} + +/** + * Filter the oEmbed HTML to detect when an embed is present so that the Optimization Detective extension module can be enqueued. + * + * This ensures that the module for handling embeds is only loaded when there is an embed on the page. + * + * @since n.e.x.t + * + * @param string|mixed $html The oEmbed HTML. + * @return string Unchanged oEmbed HTML. + */ +function embed_optimizer_filter_oembed_html_to_detect_embed_presence( $html ): string { + if ( ! is_string( $html ) ) { + $html = ''; + } + add_filter( 'od_extension_module_urls', 'embed_optimizer_filter_extension_module_urls' ); + return $html; +} + +/** + * Filter the oEmbed HTML to lazy load the embed. * * Add loading="lazy" to any iframe tags. * Lazy load any script tags. * * @since 0.1.0 * - * @param string $html The oEmbed HTML. + * @param string|mixed $html The oEmbed HTML. * @return string Filtered oEmbed HTML. */ -function embed_optimizer_filter_oembed_html( string $html ): string { +function embed_optimizer_filter_oembed_html_to_lazy_load( $html ): string { + if ( ! is_string( $html ) ) { + $html = ''; + } $html_processor = new WP_HTML_Tag_Processor( $html ); if ( embed_optimizer_update_markup( $html_processor, true ) ) { add_action( 'wp_footer', 'embed_optimizer_lazy_load_scripts' ); diff --git a/plugins/embed-optimizer/tests/test-hooks.php b/plugins/embed-optimizer/tests/test-hooks.php index 0e6b223d6c..9f0f397131 100644 --- a/plugins/embed-optimizer/tests/test-hooks.php +++ b/plugins/embed-optimizer/tests/test-hooks.php @@ -13,9 +13,9 @@ class Test_Embed_Optimizer_Hooks extends WP_UnitTestCase { public function test_hooks(): void { embed_optimizer_add_hooks(); if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) { - $this->assertFalse( has_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html' ) ); + $this->assertFalse( has_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_lazy_load' ) ); } else { - $this->assertSame( 10, has_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html' ) ); + $this->assertSame( 10, has_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_lazy_load' ) ); } $this->assertSame( 10, has_action( 'wp_head', 'embed_optimizer_render_generator' ) ); } @@ -23,15 +23,15 @@ public function test_hooks(): void { /** * Test that the oEmbed HTML is filtered. * - * @covers ::embed_optimizer_filter_oembed_html + * @covers ::embed_optimizer_filter_oembed_html_to_lazy_load * @covers ::embed_optimizer_update_markup * @dataProvider get_data_to_test_filter_oembed_html_data */ - public function test_embed_optimizer_filter_oembed_html( string $html, string $expected_html = null, bool $expected_lazy_script = false ): void { + public function test_embed_optimizer_filter_oembed_html_to_lazy_load( string $html, string $expected_html = null, bool $expected_lazy_script = false ): void { if ( null === $expected_html ) { $expected_html = $html; // No change. } - $this->assertEquals( $expected_html, embed_optimizer_filter_oembed_html( $html ) ); + $this->assertEquals( $expected_html, embed_optimizer_filter_oembed_html_to_lazy_load( $html ) ); $this->assertSame( $expected_lazy_script ? 10 : false, has_action( 'wp_footer', 'embed_optimizer_lazy_load_scripts' ) ); } diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 317d940d59..f4787dc51e 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -87,7 +87,7 @@ function error( ...message ) { */ /** - * @typedef {Object} URLMetrics + * @typedef {Object} URLMetric * @property {string} url - URL of the page. * @property {Object} viewport - Viewport. * @property {number} viewport.width - Viewport width. @@ -101,6 +101,12 @@ function error( ...message ) { * @property {boolean} complete - Whether viewport group is complete. */ +/** + * @typedef {Object} Extension + * @property {Function} [initialize] - Initialize the extension as soon as the DOM has loaded. + * @property {Function} [finalize] - Finalize the URL metric prior to it being sent to the server. + */ + /** * Checks whether the URL metric(s) for the provided viewport width is needed. * @@ -138,6 +144,7 @@ function getCurrentTime() { * @param {Object} args Args. * @param {number} args.serveTime The serve time of the page in milliseconds from PHP via `microtime( true ) * 1000`. * @param {number} args.detectionTimeWindow The number of milliseconds between now and when the page was first generated in which detection should proceed. + * @param {string[]} args.extensionModuleUrls URLs for extension script modules to import. * @param {boolean} args.isDebug Whether to show debug messages. * @param {string} args.restApiEndpoint URL for where to send the detection data. * @param {string} args.restApiNonce Nonce for writing to the REST API. @@ -153,6 +160,7 @@ export default async function detect( { serveTime, detectionTimeWindow, isDebug, + extensionModuleUrls, restApiEndpoint, restApiNonce, currentUrl, @@ -172,6 +180,8 @@ export default async function detect( { ); } + // TODO: Start of code which should be inlined in the module. + // Abort running detection logic if it was served in a cached page. if ( currentTime - serveTime > detectionTimeWindow ) { if ( isDebug ) { @@ -190,6 +200,8 @@ export default async function detect( { return; } + // TODO: End of code which should be inlined in the module. + // Ensure the DOM is loaded (although it surely already is since we're executing in a module). await new Promise( ( resolve ) => { if ( doc.readyState !== 'loading' ) { @@ -199,54 +211,15 @@ export default async function detect( { } } ); - // TODO: Conditionally add this JS only when we know there are embeds on the page. - // TODO: This JS should be provided by Embed Optimizer and not bundled in Optimization Detective. It should update the pending url metrics before they are sent to the server when the page is hidden. - // TODO: This is only necessary for embeds that contain scripting. - /* - * Observe the loading of embeds on the page. We need to run this now before the resources on the page have fully - * loaded because we need to start observing the embed wrappers before the embeds have loaded. When we detect - * subtree modifications in an embed wrapper, we then need to measure the new height of the wrapper element. - * However, since there may be multiple subtree modifications performed as an embed is loaded, we need to wait until - * what is likely the last mutation. - */ - const EMBED_LOAD_WAIT_MS = 1000; - const embedWrappers = - /** @type NodeListOf */ document.querySelectorAll( - '.wp-block-embed > .wp-block-embed__wrapper[data-od-xpath]' - ); - - /** - * Monitors embed wrapper for resizes. - * - * @param {HTMLDivElement} embedWrapper Embed wrapper DIV. - */ - function monitorEmbedWrapperForResizes( embedWrapper ) { - if ( ! ( 'odXpath' in embedWrapper.dataset ) ) { - throw new Error( 'Embed wrapper missing data-od-xpath attribute.' ); + /** @type {Extension[]} */ + const extensions = []; + for ( const extensionModuleUrl of extensionModuleUrls ) { + const extension = await import( extensionModuleUrl ); + extensions.push( extension ); + // TODO: There should to be a way to pass additional args into the module. Perhaps extensionModuleUrls should be a mapping of URLs to args. + if ( extension.initialize instanceof Function ) { + extension.initialize( { isDebug } ); } - const xpath = embedWrapper.dataset.odXpath; - let timeoutId = 0; - const observer = new ResizeObserver( ( entries ) => { - const [ entry ] = entries; - if ( timeoutId > 0 ) { - clearTimeout( timeoutId ); - } - log( - `[Embed Optimizer] Pending embed height of ${ entry.contentRect.height }px for ${ xpath }` - ); - // TODO: Is the timeout really needed? We can just keep updating the height of the element until the URL metrics are sent when the page closes. - timeoutId = setTimeout( () => { - log( - `[Embed Optimizer] Final embed height of ${ entry.contentRect.height }px for ${ xpath }` - ); - observer.disconnect(); - // TODO: Now amend URL metrics with this rect.height. - }, EMBED_LOAD_WAIT_MS ); - } ); - observer.observe( embedWrapper, { box: 'content-box' } ); - } - for ( const embedWrapper of embedWrappers ) { - monitorEmbedWrapperForResizes( embedWrapper ); } // Wait until the resources on the page have fully loaded. @@ -265,6 +238,7 @@ export default async function detect( { } ); } + // TODO: Does this make sense here? Should it be moved up above the isViewportNeeded condition? // As an alternative to this, the od_print_detection_script() function can short-circuit if the // od_is_url_metric_storage_locked() function returns true. However, the downside with that is page caching could // result in metrics missed from being gathered when a user navigates around a site and primes the page cache. @@ -275,6 +249,7 @@ export default async function detect( { return; } + // TODO: Does this make sense here? // Prevent detection when page is not scrolled to the initial viewport. if ( doc.documentElement.scrollTop > 0 ) { if ( isDebug ) { @@ -371,8 +346,8 @@ export default async function detect( { log( 'Detection is stopping.' ); } - /** @type {URLMetrics} */ - const urlMetrics = { + /** @type {URLMetric} */ + const urlMetric = { url: currentUrl, slug: urlMetricsSlug, nonce: urlMetricsNonce, @@ -411,11 +386,22 @@ export default async function detect( { boundingClientRect: elementIntersection.boundingClientRect, }; - urlMetrics.elements.push( elementMetrics ); + urlMetric.elements.push( elementMetrics ); + } + + // TODO: Wait until the page is unloading. + + for ( const extension of extensions ) { + if ( extension.finalize instanceof Function ) { + extension.finalize( { + isDebug, + urlMetric, + } ); + } } if ( isDebug ) { - log( 'Current URL metrics:', urlMetrics ); + log( 'Current URL metrics:', urlMetric ); } // Yield to main before sending data to server to further break up task. @@ -430,7 +416,7 @@ export default async function detect( { 'Content-Type': 'application/json', 'X-WP-Nonce': restApiNonce, }, - body: JSON.stringify( urlMetrics ), + body: JSON.stringify( urlMetric ), } ); if ( response.status === 200 ) { diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index 543b15c492..d02aace824 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -38,11 +38,21 @@ function od_get_detection_script( string $slug, OD_URL_Metrics_Group_Collection $web_vitals_lib_data = require __DIR__ . '/build/web-vitals.asset.php'; $web_vitals_lib_src = add_query_arg( 'ver', $web_vitals_lib_data['version'], plugin_dir_url( __FILE__ ) . 'build/web-vitals.js' ); + /** + * Filters the list of extension script module URLs to import when performing detection. + * + * @since n.e.x.t + * + * @param string[] $extension_module_urls Extension module URLs. + */ + $extension_module_urls = (array) apply_filters( 'od_extension_module_urls', array() ); + $current_url = od_get_current_url(); $detect_args = array( 'serveTime' => microtime( true ) * 1000, // In milliseconds for comparison with `Date.now()` in JavaScript. 'detectionTimeWindow' => $detection_time_window, 'isDebug' => WP_DEBUG, + 'extensionModuleUrls' => $extension_module_urls, 'restApiEndpoint' => rest_url( OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ), 'restApiNonce' => wp_create_nonce( 'wp_rest' ), 'currentUrl' => $current_url, From 0ba2d6e99bcbb1355e3d2227afa8d2f6761a5b4e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 17 Aug 2024 16:48:02 -0700 Subject: [PATCH 011/156] Override clientBoundingRect once embed has loaded --- plugins/embed-optimizer/detect.js | 61 +++++++++++++++++++----- plugins/optimization-detective/detect.js | 5 +- 2 files changed, 54 insertions(+), 12 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index ca539ca8a9..e2ec7f6696 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -1,5 +1,28 @@ const consoleLogPrefix = '[Embed Optimizer]'; +/** + * @todo This should be reused and not copied from ../optimization-detective/detect.js + * + * @typedef {Object} ElementMetrics + * @property {boolean} isLCP - Whether it is the LCP candidate. + * @property {boolean} isLCPCandidate - Whether it is among the LCP candidates. + * @property {string} xpath - XPath. + * @property {number} intersectionRatio - Intersection ratio. + * @property {DOMRectReadOnly} intersectionRect - Intersection rectangle. + * @property {DOMRectReadOnly} boundingClientRect - Bounding client rectangle. + */ + +/** + * @todo This should be reused and not copied from ../optimization-detective/detect.js + * + * @typedef {Object} URLMetric + * @property {string} url - URL of the page. + * @property {Object} viewport - Viewport. + * @property {number} viewport.width - Viewport width. + * @property {number} viewport.height - Viewport height. + * @property {ElementMetrics[]} elements - Metrics for the elements observed on the page. + */ + /** * Log a message. * @@ -19,6 +42,13 @@ function log( ...message ) { */ const EMBED_LOAD_WAIT_MS = 1000; +/** + * Embed element heights. + * + * @type {Map} + */ +const loadedElementContentRects = new Map(); + /** * Initialize. * @@ -36,10 +66,8 @@ export async function initialize( { isDebug } ) { } if ( isDebug ) { - log( embedWrappers ); + log( 'Loaded embed content rects:', loadedElementContentRects ); } - - // TODO: It should update the pending url metrics before they are sent to the server when the page is hidden. } /** @@ -47,16 +75,30 @@ export async function initialize( { isDebug } ) { * * @todo Add typing for args.urlMetric * - * @param {Object} args Args. - * @param {boolean} args.isDebug Whether to show debug messages. - * @param {Object} args.urlMetric Pending URL metric. + * @param {Object} args Args. + * @param {boolean} args.isDebug Whether to show debug messages. + * @param {URLMetric} args.urlMetric Pending URL metric. */ export async function finalize( { urlMetric, isDebug } ) { if ( isDebug ) { log( 'URL metric to be sent:', urlMetric ); } - // TODO: Finalize. + for ( const element of urlMetric.elements ) { + if ( loadedElementContentRects.has( element.xpath ) ) { + if ( isDebug ) { + log( + 'Overriding:', + element.boundingClientRect, + '=>', + loadedElementContentRects.get( element.xpath ) + ); + } + element.boundingClientRect = loadedElementContentRects.get( + element.xpath + ); + } + } } /** @@ -80,11 +122,8 @@ function monitorEmbedWrapperForResizes( embedWrapper ) { ); // TODO: Is the timeout really needed? We can just keep updating the height of the element until the URL metrics are sent when the page closes. timeoutId = setTimeout( () => { - log( - `Final embed height of ${ entry.contentRect.height }px for ${ xpath }` - ); + loadedElementContentRects.set( xpath, entry.contentRect ); observer.disconnect(); - // TODO: Now amend URL metrics with this rect.height. }, EMBED_LOAD_WAIT_MS ); } ); observer.observe( embedWrapper, { box: 'content-box' } ); diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index f4787dc51e..b1f1479e2a 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -389,7 +389,10 @@ export default async function detect( { urlMetric.elements.push( elementMetrics ); } - // TODO: Wait until the page is unloading. + // TODO: Wait until the page is actually unloading. + await new Promise( ( resolve ) => { + setTimeout( resolve, 2000 ); + } ); for ( const extension of extensions ) { if ( extension.finalize instanceof Function ) { From 5f4189d2a4b697a3eace1d33a303e5c8998d2ac6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 17 Aug 2024 17:50:30 -0700 Subject: [PATCH 012/156] Move jsdoc types to types.d.ts for reuse --- plugins/embed-optimizer/detect.js | 25 ++------------ plugins/optimization-detective/detect.js | 42 +++++------------------ plugins/optimization-detective/types.d.ts | 27 +++++++++++++++ 3 files changed, 39 insertions(+), 55 deletions(-) create mode 100644 plugins/optimization-detective/types.d.ts diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index e2ec7f6696..9422fd90b6 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -1,26 +1,8 @@ const consoleLogPrefix = '[Embed Optimizer]'; /** - * @todo This should be reused and not copied from ../optimization-detective/detect.js - * - * @typedef {Object} ElementMetrics - * @property {boolean} isLCP - Whether it is the LCP candidate. - * @property {boolean} isLCPCandidate - Whether it is among the LCP candidates. - * @property {string} xpath - XPath. - * @property {number} intersectionRatio - Intersection ratio. - * @property {DOMRectReadOnly} intersectionRect - Intersection rectangle. - * @property {DOMRectReadOnly} boundingClientRect - Bounding client rectangle. - */ - -/** - * @todo This should be reused and not copied from ../optimization-detective/detect.js - * - * @typedef {Object} URLMetric - * @property {string} url - URL of the page. - * @property {Object} viewport - Viewport. - * @property {number} viewport.width - Viewport width. - * @property {number} viewport.height - Viewport height. - * @property {ElementMetrics[]} elements - Metrics for the elements observed on the page. + * @typedef {import("../optimization-detective/types.d.ts").ElementMetrics} ElementMetrics + * @typedef {import("../optimization-detective/types.d.ts").URLMetric} URLMetric */ /** @@ -73,8 +55,6 @@ export async function initialize( { isDebug } ) { /** * Initialize. * - * @todo Add typing for args.urlMetric - * * @param {Object} args Args. * @param {boolean} args.isDebug Whether to show debug messages. * @param {URLMetric} args.urlMetric Pending URL metric. @@ -94,6 +74,7 @@ export async function finalize( { urlMetric, isDebug } ) { loadedElementContentRects.get( element.xpath ) ); } + // TODO: Maybe element.boundingClientRect should rather be element.initialBoundingClientRect and the schema is extended by Embed Optimizer to add an element.finalBoundingClientRect (same goes for intersectionRect and intersectionRatio). element.boundingClientRect = loadedElementContentRects.get( element.xpath ); diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index b1f1479e2a..4363fc3908 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -1,4 +1,10 @@ -/** @typedef {import("web-vitals").LCPMetric} LCPMetric */ +/** + * @typedef {import("web-vitals").LCPMetric} LCPMetric + * @typedef {import("types.d.ts").ElementMetrics} ElementMetrics + * @typedef {import("types.d.ts").URLMetric} URLMetric + * @typedef {import("types.d.ts").URLMetricsGroupStatus} URLMetricsGroupStatus + * @typedef {import("types.d.ts").Extension} Extension + */ const win = window; const doc = win.document; @@ -76,37 +82,6 @@ function error( ...message ) { console.error( consoleLogPrefix, ...message ); } -/** - * @typedef {Object} ElementMetrics - * @property {boolean} isLCP - Whether it is the LCP candidate. - * @property {boolean} isLCPCandidate - Whether it is among the LCP candidates. - * @property {string} xpath - XPath. - * @property {number} intersectionRatio - Intersection ratio. - * @property {DOMRectReadOnly} intersectionRect - Intersection rectangle. - * @property {DOMRectReadOnly} boundingClientRect - Bounding client rectangle. - */ - -/** - * @typedef {Object} URLMetric - * @property {string} url - URL of the page. - * @property {Object} viewport - Viewport. - * @property {number} viewport.width - Viewport width. - * @property {number} viewport.height - Viewport height. - * @property {ElementMetrics[]} elements - Metrics for the elements observed on the page. - */ - -/** - * @typedef {Object} URLMetricsGroupStatus - * @property {number} minimumViewportWidth - Minimum viewport width. - * @property {boolean} complete - Whether viewport group is complete. - */ - -/** - * @typedef {Object} Extension - * @property {Function} [initialize] - Initialize the extension as soon as the DOM has loaded. - * @property {Function} [finalize] - Finalize the URL metric prior to it being sent to the server. - */ - /** * Checks whether the URL metric(s) for the provided viewport width is needed. * @@ -216,7 +191,8 @@ export default async function detect( { for ( const extensionModuleUrl of extensionModuleUrls ) { const extension = await import( extensionModuleUrl ); extensions.push( extension ); - // TODO: There should to be a way to pass additional args into the module. Perhaps extensionModuleUrls should be a mapping of URLs to args. + // TODO: There should to be a way to pass additional args into the module. Perhaps extensionModuleUrls should be a mapping of URLs to args. It's important to pass webVitalsLibrarySrc to the extension so that onLCP, onCLS, or onINP can be obtained. + // TODO: Pass additional functions from this module into the extensions. if ( extension.initialize instanceof Function ) { extension.initialize( { isDebug } ); } diff --git a/plugins/optimization-detective/types.d.ts b/plugins/optimization-detective/types.d.ts new file mode 100644 index 0000000000..6d0400d6df --- /dev/null +++ b/plugins/optimization-detective/types.d.ts @@ -0,0 +1,27 @@ +interface ElementMetrics { + isLCP: boolean; + isLCPCandidate: boolean; + xpath: string; + intersectionRatio: number; + intersectionRect: DOMRectReadOnly; + boundingClientRect: DOMRectReadOnly; +} + +interface URLMetric { + url: string; + viewport: { + width: number; + height: number; + }; + elements: ElementMetrics[]; +} + +interface URLMetricsGroupStatus { + minimumViewportWidth: number; + complete: boolean; +} + +interface Extension { + initialize?: Function; + finalize?: Function; +} From bf2b3c5c3430c1d6eca3c4a3e9301e599ab81877 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 17 Aug 2024 22:11:05 -0700 Subject: [PATCH 013/156] Send URL metric when leaving the page This is an iteration on top of https://github.com/WordPress/performance/pull/1098 Co-authored-by: swissspidy --- plugins/optimization-detective/detect.js | 69 +++++++++++------------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 4363fc3908..7615454cb2 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -325,8 +325,6 @@ export default async function detect( { /** @type {URLMetric} */ const urlMetric = { url: currentUrl, - slug: urlMetricsSlug, - nonce: urlMetricsNonce, viewport: { width: win.innerWidth, height: win.innerHeight, @@ -365,9 +363,24 @@ export default async function detect( { urlMetric.elements.push( elementMetrics ); } - // TODO: Wait until the page is actually unloading. + if ( isDebug ) { + log( 'Current URL metrics:', urlMetric ); + } + + // Wait for the page to be hidden. await new Promise( ( resolve ) => { - setTimeout( resolve, 2000 ); + win.addEventListener( 'pagehide', resolve, { once: true } ); + win.addEventListener( 'pageswap', resolve, { once: true } ); + doc.addEventListener( + 'visibilitychange', + () => { + if ( document.visibilityState === 'hidden' ) { + // TODO: This will fire even when switching tabs. + resolve(); + } + }, + { once: true } + ); } ); for ( const extension of extensions ) { @@ -379,42 +392,22 @@ export default async function detect( { } } - if ( isDebug ) { - log( 'Current URL metrics:', urlMetric ); - } + // Even though the server may reject the REST API request, we still have to set the storage lock + // because we can't look at the response when sending a beacon. + setStorageLock( getCurrentTime() ); - // Yield to main before sending data to server to further break up task. - await new Promise( ( resolve ) => { - setTimeout( resolve, 0 ); + const body = Object.assign( {}, urlMetric, { + slug: urlMetricsSlug, + nonce: urlMetricsNonce, } ); - - try { - const response = await fetch( restApiEndpoint, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-WP-Nonce': restApiNonce, - }, - body: JSON.stringify( urlMetric ), - } ); - - if ( response.status === 200 ) { - setStorageLock( getCurrentTime() ); - } - - if ( isDebug ) { - const body = await response.json(); - if ( response.status === 200 ) { - log( 'Response:', body ); - } else { - error( 'Failure:', body ); - } - } - } catch ( err ) { - if ( isDebug ) { - error( err ); - } - } + const url = new URL( restApiEndpoint ); + url.searchParams.set( '_wpnonce', restApiNonce ); + navigator.sendBeacon( + url, + new Blob( [ JSON.stringify( body ) ], { + type: 'application/json', + } ) + ); // Clean up. breadcrumbedElementsMap.clear(); From b9bad0de1eacd9730b335d2f64a49b5885bee7b3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 17 Aug 2024 22:32:53 -0700 Subject: [PATCH 014/156] Use boundingClientRect instead of intersectionRect in get_all_element_minimum_heights --- plugins/embed-optimizer/detect.js | 5 +++-- .../class-od-url-metrics-group-collection.php | 4 ++-- plugins/optimization-detective/detect.js | 4 ++++ .../test-class-od-url-metrics-group-collection.php | 10 +++++++--- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index 9422fd90b6..0a0d2c9e3e 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -21,8 +21,9 @@ function log( ...message ) { * subtree modifications in an embed wrapper, we then need to measure the new height of the wrapper element. * However, since there may be multiple subtree modifications performed as an embed is loaded, we need to wait until * what is likely the last mutation. + * TODO: This is a magic number. Ideally we wouldn't need this. */ -const EMBED_LOAD_WAIT_MS = 1000; +const EMBED_LOAD_WAIT_MS = 5000; /** * Embed element heights. @@ -101,9 +102,9 @@ function monitorEmbedWrapperForResizes( embedWrapper ) { log( `Pending embed height of ${ entry.contentRect.height }px for ${ xpath }` ); + loadedElementContentRects.set( xpath, entry.contentRect ); // TODO: Is the timeout really needed? We can just keep updating the height of the element until the URL metrics are sent when the page closes. timeoutId = setTimeout( () => { - loadedElementContentRects.set( xpath, entry.contentRect ); observer.disconnect(); }, EMBED_LOAD_WAIT_MS ); } ); diff --git a/plugins/optimization-detective/class-od-url-metrics-group-collection.php b/plugins/optimization-detective/class-od-url-metrics-group-collection.php index 9f32810858..4aba737b03 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metrics-group-collection.php @@ -459,8 +459,8 @@ public function get_all_element_minimum_heights(): array { foreach ( $this->get_all_url_metrics_groups_elements() as $element ) { $element_min_heights[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_min_heights ) - ? min( $element_min_heights[ $element['xpath'] ], $element['intersectionRect']['height'] ) - : $element['intersectionRect']['height']; + ? min( $element_min_heights[ $element['xpath'] ], $element['boundingClientRect']['height'] ) + : $element['boundingClientRect']['height']; } return $element_min_heights; } )(); diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 7615454cb2..ba9be05742 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -396,6 +396,10 @@ export default async function detect( { // because we can't look at the response when sending a beacon. setStorageLock( getCurrentTime() ); + if ( isDebug ) { + log( 'Sending URL metric:', urlMetric ); + } + const body = Object.assign( {}, urlMetric, { slug: urlMetricsSlug, nonce: urlMetricsNonce, 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 de5ba3884d..144a9be0ce 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 @@ -735,9 +735,13 @@ public function data_provider_element_minimum_heights(): array { array( 'viewport_width' => $viewport_width, 'element' => array( - 'isLCP' => true, - 'xpath' => $lcp_element_xpath, - 'intersectionRect' => array_merge( + 'isLCP' => true, + 'xpath' => $lcp_element_xpath, + 'intersectionRect' => array_merge( + $this->get_sample_dom_rect(), + array( 'height' => $element_height ) + ), + 'boundingClientRect' => array_merge( $this->get_sample_dom_rect(), array( 'height' => $element_height ) ), From c6b02ecba4dafff7475d81e586a12a63b9592b6e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sat, 17 Aug 2024 22:38:59 -0700 Subject: [PATCH 015/156] Eliminate timeout for disconneccting ResizeObsever --- plugins/embed-optimizer/detect.js | 23 +---------------------- 1 file changed, 1 insertion(+), 22 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index 0a0d2c9e3e..677ec9f1c3 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -15,16 +15,6 @@ function log( ...message ) { console.log( consoleLogPrefix, ...message ); } -/* - * Observe the loading of embeds on the page. We need to run this now before the resources on the page have fully - * loaded because we need to start observing the embed wrappers before the embeds have loaded. When we detect - * subtree modifications in an embed wrapper, we then need to measure the new height of the wrapper element. - * However, since there may be multiple subtree modifications performed as an embed is loaded, we need to wait until - * what is likely the last mutation. - * TODO: This is a magic number. Ideally we wouldn't need this. - */ -const EMBED_LOAD_WAIT_MS = 5000; - /** * Embed element heights. * @@ -69,7 +59,7 @@ export async function finalize( { urlMetric, isDebug } ) { if ( loadedElementContentRects.has( element.xpath ) ) { if ( isDebug ) { log( - 'Overriding:', + `Overriding boundingClientRect for ${ element.xpath }:`, element.boundingClientRect, '=>', loadedElementContentRects.get( element.xpath ) @@ -93,20 +83,9 @@ function monitorEmbedWrapperForResizes( embedWrapper ) { throw new Error( 'Embed wrapper missing data-od-xpath attribute.' ); } const xpath = embedWrapper.dataset.odXpath; - let timeoutId = 0; const observer = new ResizeObserver( ( entries ) => { const [ entry ] = entries; - if ( timeoutId > 0 ) { - clearTimeout( timeoutId ); - } - log( - `Pending embed height of ${ entry.contentRect.height }px for ${ xpath }` - ); loadedElementContentRects.set( xpath, entry.contentRect ); - // TODO: Is the timeout really needed? We can just keep updating the height of the element until the URL metrics are sent when the page closes. - timeoutId = setTimeout( () => { - observer.disconnect(); - }, EMBED_LOAD_WAIT_MS ); } ); observer.observe( embedWrapper, { box: 'content-box' } ); } From 52a22607353ceeaa923aedc19c80f637beb08815 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 18 Aug 2024 14:44:55 -0700 Subject: [PATCH 016/156] Move extension initialization after idle callback --- plugins/optimization-detective/detect.js | 28 ++++++++++-------------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index ba9be05742..88f1e4cae8 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -155,8 +155,6 @@ export default async function detect( { ); } - // TODO: Start of code which should be inlined in the module. - // Abort running detection logic if it was served in a cached page. if ( currentTime - serveTime > detectionTimeWindow ) { if ( isDebug ) { @@ -175,8 +173,6 @@ export default async function detect( { return; } - // TODO: End of code which should be inlined in the module. - // Ensure the DOM is loaded (although it surely already is since we're executing in a module). await new Promise( ( resolve ) => { if ( doc.readyState !== 'loading' ) { @@ -186,18 +182,6 @@ export default async function detect( { } } ); - /** @type {Extension[]} */ - const extensions = []; - for ( const extensionModuleUrl of extensionModuleUrls ) { - const extension = await import( extensionModuleUrl ); - extensions.push( extension ); - // TODO: There should to be a way to pass additional args into the module. Perhaps extensionModuleUrls should be a mapping of URLs to args. It's important to pass webVitalsLibrarySrc to the extension so that onLCP, onCLS, or onINP can be obtained. - // TODO: Pass additional functions from this module into the extensions. - if ( extension.initialize instanceof Function ) { - extension.initialize( { isDebug } ); - } - } - // Wait until the resources on the page have fully loaded. await new Promise( ( resolve ) => { if ( doc.readyState === 'complete' ) { @@ -240,6 +224,18 @@ export default async function detect( { log( 'Proceeding with detection' ); } + /** @type {Extension[]} */ + const extensions = []; + for ( const extensionModuleUrl of extensionModuleUrls ) { + const extension = await import( extensionModuleUrl ); + extensions.push( extension ); + // TODO: There should to be a way to pass additional args into the module. Perhaps extensionModuleUrls should be a mapping of URLs to args. It's important to pass webVitalsLibrarySrc to the extension so that onLCP, onCLS, or onINP can be obtained. + // TODO: Pass additional functions from this module into the extensions. + if ( extension.initialize instanceof Function ) { + extension.initialize( { isDebug } ); + } + } + const breadcrumbedElements = doc.body.querySelectorAll( '[data-od-xpath]' ); /** @type {Map} */ From edc52fad0c127db5ea92e4a94dc2fdf9eaf2c062 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 18 Aug 2024 15:02:49 -0700 Subject: [PATCH 017/156] Fix warning when prematurely applying buffered text replacements, especially to end-of-body --- .../class-od-html-tag-processor.php | 11 ++- .../optimization-detective/optimization.php | 2 +- .../test-class-od-html-tag-processor.php | 87 +++---------------- 3 files changed, 22 insertions(+), 78 deletions(-) diff --git a/plugins/optimization-detective/class-od-html-tag-processor.php b/plugins/optimization-detective/class-od-html-tag-processor.php index 3a550b7b2b..d2125a62d4 100644 --- a/plugins/optimization-detective/class-od-html-tag-processor.php +++ b/plugins/optimization-detective/class-od-html-tag-processor.php @@ -565,11 +565,16 @@ public function append_body_html( string $html ): void { } /** - * Returns the string representation of the HTML Tag Processor. + * Gets the final updated HTML. * - * @return string The processed HTML. + * This should only be called after the closing HTML tag has been reached and just before + * calling {@see WP_HTML_Tag_Processor::get_updated_html()} to send the document back in the response. + * + * @since n.e.x.t + * + * @return string Final updated HTML. */ - public function get_updated_html(): string { + public function get_final_updated_html(): string { foreach ( array_keys( $this->buffered_text_replacements ) as $bookmark ) { $html_strings = $this->buffered_text_replacements[ $bookmark ]; if ( count( $html_strings ) === 0 ) { diff --git a/plugins/optimization-detective/optimization.php b/plugins/optimization-detective/optimization.php index 66a97591f2..b8e59075e1 100644 --- a/plugins/optimization-detective/optimization.php +++ b/plugins/optimization-detective/optimization.php @@ -251,5 +251,5 @@ function od_optimize_template_output_buffer( string $buffer ): string { $processor->append_body_html( od_get_detection_script( $slug, $group_collection ) ); } - return $processor->get_updated_html(); + return $processor->get_final_updated_html(); } diff --git a/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php b/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php index 744f0447c4..ed48f0d939 100644 --- a/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php +++ b/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php @@ -338,80 +338,15 @@ public function test_next_tag_with_query(): void { $p->next_tag( array( 'tag_name' => 'HTML' ) ); } - /** - * Test append_head_html(). - * - * @covers ::append_head_html - */ - public function test_append_head_html(): void { - $html = ' - - - - - - - -

Hello World

- - - '; - $processor = new OD_HTML_Tag_Processor( $html ); - $early_injected = ''; - $late_injected = ''; - $processor->append_head_html( $early_injected ); - - $saw_head = false; - while ( $processor->next_open_tag() ) { - $tag = $processor->get_tag(); - if ( 'HEAD' === $tag ) { - $saw_head = true; - } - } - $this->assertTrue( $saw_head ); - - $processor->append_head_html( $late_injected ); - $expected = " - - - - - {$early_injected}{$late_injected} - - -

Hello World

- - - "; - - $this->assertSame( $expected, $processor->get_updated_html() ); - - $later_injected = ''; - $processor->append_head_html( $later_injected ); - - $expected = " - - - - - {$early_injected}{$late_injected}{$later_injected} - - -

Hello World

- - - "; - $this->assertSame( $expected, $processor->get_updated_html() ); - } - /** * Test both append_head_html() and append_body_html(). * * @covers ::append_head_html * @covers ::append_body_html + * @covers ::get_final_updated_html */ public function test_append_head_and_body_html(): void { - $html = ' + $html = ' @@ -425,9 +360,13 @@ public function test_append_head_and_body_html(): void { '; - $head_injected = ''; - $body_injected = ''; - $processor = new OD_HTML_Tag_Processor( $html ); + $head_injected = ''; + $body_injected = ''; + $later_head_injected = ''; + $processor = new OD_HTML_Tag_Processor( $html ); + + $processor->append_head_html( $head_injected ); + $processor->append_body_html( $body_injected ); $saw_head = false; $saw_body = false; @@ -442,14 +381,14 @@ public function test_append_head_and_body_html(): void { $this->assertTrue( $saw_head ); $this->assertTrue( $saw_body ); - $processor->append_head_html( $head_injected ); - $processor->append_body_html( $body_injected ); + $processor->append_head_html( $later_head_injected ); + $expected = " - {$head_injected} + {$head_injected}{$later_head_injected}

Hello World

@@ -458,7 +397,7 @@ public function test_append_head_and_body_html(): void { "; - $this->assertSame( $expected, $processor->get_updated_html() ); + $this->assertSame( $expected, $processor->get_final_updated_html() ); } /** From 820d66d2fdc4354534cf6cdb418c4e3bea62cc47 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 18 Aug 2024 17:11:14 -0700 Subject: [PATCH 018/156] Prepend min-height to style attribute instead of appending --- .../embed-optimizer/class-embed-optimizer-tag-visitor.php | 8 ++++---- .../tests/test-cases/nested-figure-embed.php | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index 7faff55504..9cb714ab3a 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -85,13 +85,13 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $embed_wrapper_xpath = $processor->get_xpath() . '/*[1][self::DIV]'; $minimum_height = $context->url_metrics_group_collection->get_element_minimum_height( $embed_wrapper_xpath ); if ( is_float( $minimum_height ) ) { - $style = $processor->get_attribute( 'style' ); + $min_height_style = sprintf( 'min-height: %dpx;', $minimum_height ); + $style = $processor->get_attribute( 'style' ); if ( is_string( $style ) ) { - $style = rtrim( trim( $style ), ';' ) . '; '; + $style = $min_height_style . ' ' . $style; } else { - $style = ''; + $style = $min_height_style; } - $style .= sprintf( 'min-height: %dpx;', $minimum_height ); $processor->set_attribute( 'style', $style ); } diff --git a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php index 2878eceacc..983228afe5 100644 --- a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php +++ b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php @@ -101,12 +101,12 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool { -
+
-
+

So I heard you like FIGURE?

From 7864449a17c94f600cc20ee9ff21a33b71c469c9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 18 Aug 2024 18:12:19 -0700 Subject: [PATCH 019/156] Avoid setting meta attribute when value unchanged --- plugins/optimization-detective/class-od-html-tag-processor.php | 2 +- .../tests/test-class-od-html-tag-processor.php | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/optimization-detective/class-od-html-tag-processor.php b/plugins/optimization-detective/class-od-html-tag-processor.php index d2125a62d4..0da1c98adb 100644 --- a/plugins/optimization-detective/class-od-html-tag-processor.php +++ b/plugins/optimization-detective/class-od-html-tag-processor.php @@ -363,7 +363,7 @@ public function get_next_token_count(): int { public function set_attribute( $name, $value ): bool { // phpcs:ignore SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint $existing_value = $this->get_attribute( $name ); $result = parent::set_attribute( $name, $value ); - if ( $result ) { + if ( $result && $existing_value !== $value ) { if ( is_string( $existing_value ) ) { $this->set_meta_attribute( "replaced-{$name}", $existing_value ); } else { diff --git a/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php b/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php index ed48f0d939..75d1741399 100644 --- a/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php +++ b/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php @@ -413,6 +413,7 @@ public function test_html_tag_processor_wrapper_methods(): void { $open_tag = $processor->get_tag(); if ( 'HTML' === $open_tag ) { $processor->set_attribute( 'lang', 'es' ); + $processor->set_attribute( 'class', 'foo' ); // Unchanged. $processor->remove_attribute( 'dir' ); $processor->set_attribute( 'id', 'root' ); $processor->set_meta_attribute( 'foo', 'bar' ); From 7b01495fbd80158782978d32477a60dce3ab626b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 18 Aug 2024 21:36:48 -0700 Subject: [PATCH 020/156] Allow viewport_height to be specified in sample URL metric --- ...ass-optimization-detective-test-helpers.php | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/tests/class-optimization-detective-test-helpers.php b/tests/class-optimization-detective-test-helpers.php index ce04b2ac60..c69e8c0962 100644 --- a/tests/class-optimization-detective-test-helpers.php +++ b/tests/class-optimization-detective-test-helpers.php @@ -65,10 +65,11 @@ public function get_sample_dom_rect(): array { * Gets a sample URL metric. * * @phpstan-param array{ - * url?: string, - * viewport_width?: int, - * element?: ElementDataSubset, - * elements?: array + * url?: string, + * viewport_width?: int, + * viewport_height?: int, + * element?: ElementDataSubset, + * elements?: array * } $params Params. * * @return OD_URL_Metric URL metric. @@ -76,9 +77,10 @@ public function get_sample_dom_rect(): array { public function get_sample_url_metric( array $params ): OD_URL_Metric { $params = array_merge( array( - 'url' => home_url( '/' ), - 'viewport_width' => 480, - 'elements' => array(), + 'url' => home_url( '/' ), + 'viewport_width' => 480, + 'viewport_height' => 800, + 'elements' => array(), ), $params ); @@ -92,7 +94,7 @@ public function get_sample_url_metric( array $params ): OD_URL_Metric { 'url' => home_url( '/' ), 'viewport' => array( 'width' => $params['viewport_width'], - 'height' => 800, + 'height' => $params['viewport_height'], ), 'timestamp' => microtime( true ), 'elements' => array_map( From 9c8799b7abecb3e9b758374a12b6c8a0a111668f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 18 Aug 2024 21:53:34 -0700 Subject: [PATCH 021/156] Add get_all_elements_positioned_in_any_initial_viewport() and is_element_positioned_in_any_initial_viewport() methods to OD_URL_Metrics_Group_Collection --- .../class-od-url-metrics-group-collection.php | 56 +++++++++++- ...-class-od-url-metrics-group-collection.php | 90 +++++++++++++++++++ 2 files changed, 144 insertions(+), 2 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metrics-group-collection.php b/plugins/optimization-detective/class-od-url-metrics-group-collection.php index 4aba737b03..04c67ba2fb 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metrics-group-collection.php @@ -81,7 +81,8 @@ final class OD_URL_Metrics_Group_Collection implements Countable, IteratorAggreg * get_groups_by_lcp_element?: array, * get_common_lcp_element?: ElementData|null, * get_all_element_max_intersection_ratios?: array, - * get_all_element_minimum_heights?: array + * get_all_element_minimum_heights?: array, + * get_all_elements_positioned_in_any_initial_viewport?: array * } */ private $result_cache = array(); @@ -469,6 +470,45 @@ public function get_all_element_minimum_heights(): array { return $result; } + /** + * Gets all elements' status for whether they are positioned in any initial viewport. + * + * An element is positioned in the initial viewport if its `boundingClientRect.top` is less than the + * `viewport.height` for any of its recorded URL metrics. Note that even though the element may be positioned in the + * initial viewport, it may not actually be visible. It could be occluded as a latter slide in a carousel in which + * case it will have intersectionRatio of 0. Or the element may not be visible due to it or an ancestor having the + * `visibility:hidden` style, such as in the case of a dropdown navigation menu. When, for example, an IMG element + * is positioned in any initial viewport, it should not get `loading=lazy` but rather `fetchpriority=low`. + * + * @since n.e.x.t + * + * @return array Keys are XPaths and values whether the element is positioned in any initial viewport. + */ + public function get_all_elements_positioned_in_any_initial_viewport(): array { + if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) { + return $this->result_cache[ __FUNCTION__ ]; + } + + $result = ( function () { + $elements_positioned = array(); + foreach ( $this->groups as $group ) { + foreach ( $group as $url_metric ) { + foreach ( $url_metric->get_elements() as $element ) { + $elements_positioned[ $element['xpath'] ] = ( + ( $elements_positioned[ $element['xpath'] ] ?? false ) + || + $element['boundingClientRect']['top'] < $url_metric->get_viewport()['height'] + ); + } + } + } + return $elements_positioned; + } )(); + + $this->result_cache[ __FUNCTION__ ] = $result; + return $result; + } + /** * Gets the max intersection ratio of an element across all groups and their captured URL metrics. * @@ -485,12 +525,24 @@ public function get_element_max_intersection_ratio( string $xpath ): ?float { * @since n.e.x.t * * @param string $xpath XPath for the element. - * @return float Minimum height in pixels. + * @return float Minimum height in pixels or null if unknown. */ public function get_element_minimum_height( string $xpath ): ?float { return $this->get_all_element_minimum_heights()[ $xpath ] ?? null; } + /** + * Determines whether an element is positioned in any initial viewport. + * + * @since n.e.x.t + * + * @param string $xpath XPath for the element. + * @return bool Whether element is positioned in any initial viewport of null if unknown. + */ + public function is_element_positioned_in_any_initial_viewport( string $xpath ): ?bool { + return $this->get_all_elements_positioned_in_any_initial_viewport()[ $xpath ] ?? null; + } + /** * Gets URL metrics from all groups flattened into one list. * 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 144a9be0ce..b30aecb6c6 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 @@ -718,6 +718,7 @@ public function test_get_all_element_max_intersection_ratios( array $url_metrics foreach ( $expected as $expected_xpath => $expected_max_ratio ) { $this->assertSame( $expected_max_ratio, $group_collection->get_element_max_intersection_ratio( $expected_xpath ) ); } + $this->assertNull( $group_collection->get_element_max_intersection_ratio( '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::BLINK]/*[1]' ) ); } /** @@ -809,6 +810,95 @@ public function test_get_all_element_minimum_heights( array $url_metrics, array foreach ( $expected as $expected_xpath => $expected_max_ratio ) { $this->assertSame( $expected_max_ratio, $group_collection->get_element_minimum_height( $expected_xpath ) ); } + $this->assertNull( $group_collection->get_element_minimum_height( '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::BLINK]/*[1]' ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_provider_get_all_elements_positioned_in_any_initial_viewport(): array { + $xpath1 = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[1]'; + $xpath2 = '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::IMG]/*[2]'; + + $get_sample_url_metric = function ( int $viewport_width, int $viewport_height, string $xpath, float $intersection_ratio, float $top ): OD_URL_Metric { + return $this->get_sample_url_metric( + array( + 'viewport_width' => $viewport_width, + 'viewport_height' => $viewport_height, + 'element' => array( + 'isLCP' => false, + 'xpath' => $xpath, + 'intersectionRatio' => $intersection_ratio, + 'intersectionRect' => array_merge( + $this->get_sample_dom_rect(), + array( 'top' => $top ) + ), + 'boundingClientRect' => array_merge( + $this->get_sample_dom_rect(), + array( 'top' => $top ) + ), + ), + ) + ); + }; + + return array( + 'element-inside-viewport' => array( + 'url_metrics' => array( + $get_sample_url_metric( 360, 640, $xpath1, 1.0, 0 ), + $get_sample_url_metric( 360, 640, $xpath1, 1.0, 100 ), + $get_sample_url_metric( 360, 640, $xpath1, 1.0, 639 ), + ), + 'expected' => array( + $xpath1 => true, + ), + ), + 'element-outside-viewport' => array( + 'url_metrics' => array( + $get_sample_url_metric( 360, 640, $xpath1, 0.0, 640 ), + $get_sample_url_metric( 360, 640, $xpath1, 0.0, 641 ), + ), + 'expected' => array( + $xpath1 => false, + ), + ), + 'two-elements-inside-and-outside-viewport' => array( + 'url_metrics' => array( + $get_sample_url_metric( 360, 640, $xpath1, 1.0, 100 ), + $get_sample_url_metric( 360, 640, $xpath2, 0.0, 1000 ), + ), + 'expected' => array( + $xpath1 => true, + $xpath2 => false, + ), + ), + ); + } + + /** + * Test get_all_elements_positioned_in_any_initial_viewport() and is_element_positioned_in_any_initial_viewport(). + * + * @covers ::get_all_elements_positioned_in_any_initial_viewport + * @covers ::is_element_positioned_in_any_initial_viewport + * + * @dataProvider data_provider_get_all_elements_positioned_in_any_initial_viewport + * + * @param array $url_metrics URL metrics. + * @param array $expected Expected. + */ + public function test_get_all_elements_positioned_in_any_initial_viewport( array $url_metrics, array $expected ): void { + $breakpoints = array( 480, 600, 782 ); + $sample_size = 3; + $group_collection = new OD_URL_Metrics_Group_Collection( $url_metrics, $breakpoints, $sample_size, 0 ); + $actual = $group_collection->get_all_elements_positioned_in_any_initial_viewport(); + $this->assertSame( $actual, $group_collection->get_all_elements_positioned_in_any_initial_viewport(), 'Cached result is identical.' ); + $this->assertSame( $expected, $actual ); + foreach ( $expected as $expected_xpath => $expected_is_positioned ) { + $this->assertSame( $expected_is_positioned, $group_collection->is_element_positioned_in_any_initial_viewport( $expected_xpath ) ); + } + $this->assertNull( $group_collection->is_element_positioned_in_any_initial_viewport( '/*[0][self::HTML]/*[1][self::BODY]/*[0][self::BLINK]/*[1]' ) ); } /** From c573175bb243eabb22149ebd9abba71487d729b1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 18 Aug 2024 21:54:30 -0700 Subject: [PATCH 022/156] Add fetchpriority=low to occluded elements in initial viewport --- ...lass-image-prioritizer-img-tag-visitor.php | 57 ++++++++++++++----- ...wport-with-fully-populated-sample-data.php | 57 ++++++++++++++----- 2 files changed, 87 insertions(+), 27 deletions(-) diff --git a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php index 4395cdaf83..4967e28f62 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php @@ -40,6 +40,10 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $xpath = $processor->get_xpath(); + $current_fetchpriority = strtolower( trim( (string) $processor->get_attribute( 'fetchpriority' ), " \t\f\r\n" ) ); + $is_lazy_loaded = 'lazy' === strtolower( trim( (string) $processor->get_attribute( 'loading' ), " \t\f\r\n" ) ); + $updated_fetchpriority = null; + /* * When the same LCP element is common/shared among all viewport groups, make sure that the element has * fetchpriority=high, even though it won't really be needed because a preload link with fetchpriority=high @@ -47,13 +51,9 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { */ $common_lcp_element = $context->url_metrics_group_collection->get_common_lcp_element(); if ( ! is_null( $common_lcp_element ) && $xpath === $common_lcp_element['xpath'] ) { - if ( 'high' === $processor->get_attribute( 'fetchpriority' ) ) { - $processor->set_meta_attribute( 'fetchpriority-already-added', true ); - } else { - $processor->set_attribute( 'fetchpriority', 'high' ); - } + $updated_fetchpriority = 'high'; } elseif ( - is_string( $processor->get_attribute( 'fetchpriority' ) ) + 'high' === $current_fetchpriority && // Temporary condition in case someone updates Image Prioritizer without also updating Optimization Detective. method_exists( $context->url_metrics_group_collection, 'is_any_group_populated' ) @@ -70,26 +70,55 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { * fetchpriority=high in such case to prevent server-side heuristics from prioritizing loading the image * which isn't actually the LCP element for actual visitors. */ - $processor->remove_attribute( 'fetchpriority' ); + $updated_fetchpriority = false; // That is, remove it. } - $element_max_intersection_ratio = $context->url_metrics_group_collection->get_element_max_intersection_ratio( $xpath ); + $element_max_intersection_ratio = $context->url_metrics_group_collection->get_element_max_intersection_ratio( $xpath ); + $is_positioned_in_any_initial_viewport = null; + // Temporary condition in case Optimization Detective is not updated to the latest version yet. + if ( method_exists( $context->url_metrics_group_collection, 'is_element_positioned_in_any_initial_viewport' ) ) { + $is_positioned_in_any_initial_viewport = $context->url_metrics_group_collection->is_element_positioned_in_any_initial_viewport( $xpath ); + } // If the element was not found, we don't know if it was visible for not, so don't do anything. - if ( is_null( $element_max_intersection_ratio ) ) { + if ( is_null( $element_max_intersection_ratio ) || is_null( $is_positioned_in_any_initial_viewport ) ) { $processor->set_meta_attribute( 'unknown-tag', true ); // Mostly useful for debugging why an IMG isn't optimized. } else { - // Otherwise, make sure visible elements omit the loading attribute, and hidden elements include loading=lazy. + // TODO: Take into account whether the element has the computed style of visibility:hidden, in such case it should also get fetchpriority=low. $is_visible = $element_max_intersection_ratio > 0.0; - $loading = (string) $processor->get_attribute( 'loading' ); - if ( $is_visible && 'lazy' === $loading ) { - $processor->remove_attribute( 'loading' ); - } elseif ( ! $is_visible && 'lazy' !== $loading ) { + if ( $is_positioned_in_any_initial_viewport ) { + if ( ! $is_visible ) { + // If an element is positioned in the initial viewport and yet it is it not visible, it may be + // located in a subsequent carousel slide or inside a hidden navigation menu which could be + // displayed at any time. Therefore, it should get fetchpriority=low so that any images which are + // visible can be loaded with a higher priority. + $updated_fetchpriority = 'low'; + + // Also prevent the image from being lazy-loaded (or eager-loaded) since it may be revealed at any + // time without the browser having any signal (e.g. user scrolling toward it) to start downloading. + $processor->remove_attribute( 'loading' ); + } elseif ( $is_lazy_loaded ) { + // Otherwise, if the image is positioned inside any initial viewport then it should never get lazy-loaded. + $processor->remove_attribute( 'loading' ); + } + } elseif ( ! $is_lazy_loaded && ! $is_visible ) { + // Otherwise, the element is not positioned in any initial viewport, so it should always get lazy-loaded. + // The `! $is_visible` condition should always evaluate to true since the intersectionRatio of an + // element positioned below the initial viewport should by definition never be visible. $processor->set_attribute( 'loading', 'lazy' ); } } // TODO: If an image is visible in one breakpoint but not another, add loading=lazy AND add a regular-priority preload link with media queries (unless LCP in which case it should already have a fetchpriority=high link) so that the image won't be eagerly-loaded for viewports on which it is not shown. + // Set the fetchpriority attribute if needed. + if ( is_string( $updated_fetchpriority ) && $updated_fetchpriority !== $current_fetchpriority ) { + $processor->set_attribute( 'fetchpriority', $updated_fetchpriority ); + } elseif ( $updated_fetchpriority === $current_fetchpriority ) { + $processor->set_meta_attribute( 'fetchpriority-already-added', true ); + } elseif ( false === $updated_fetchpriority ) { + $processor->remove_attribute( 'fetchpriority' ); + } + // If this element is the LCP (for a breakpoint group), add a preload link for it. foreach ( $context->url_metrics_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) { $link_attributes = array_merge( diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data.php b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data.php index 356ddfb0d6..be12c24edb 100644 --- a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data.php +++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-and-lazy-loaded-image-outside-viewport-with-fully-populated-sample-data.php @@ -3,6 +3,12 @@ 'set_up' => static function ( Test_Image_Prioritizer_Helper $test_case ): void { $slug = od_get_url_metrics_slug( od_get_normalized_query_vars() ); $sample_size = od_get_url_metrics_breakpoint_sample_size(); + $outside_viewport_rect = array_merge( + $test_case->get_sample_dom_rect(), + array( + 'top' => 100000, + ) + ); foreach ( array_merge( od_get_breakpoint_max_widths(), array( 1000 ) ) as $viewport_width ) { for ( $i = 0; $i < $sample_size; $i++ ) { OD_URL_Metrics_Post_Type::store_url_metric( @@ -12,28 +18,45 @@ 'viewport_width' => $viewport_width, 'elements' => array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::DIV]/*[1][self::IMG]', 'isLCP' => true, ), array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[3][self::IMG]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::DIV]/*[2][self::IMG]', 'isLCP' => false, - 'intersectionRatio' => 0 === $i ? 0.5 : 0.0, // Make sure that the _max_ intersection ratio is considered. + 'intersectionRatio' => 0.0, // Subsequent carousel slide. ), array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[5][self::IMG]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::DIV]/*[3][self::IMG]', 'isLCP' => false, - 'intersectionRatio' => 0.0, + 'intersectionRatio' => 0.0, // Subsequent carousel slide. ), array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[6][self::IMG]', + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[3][self::IMG]', 'isLCP' => false, - 'intersectionRatio' => 0.0, + 'intersectionRatio' => 0 === $i ? 0.5 : 0.0, // Make sure that the _max_ intersection ratio is considered. ), + // All are outside all initial viewports. array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[7][self::IMG]', - 'isLCP' => false, - 'intersectionRatio' => 0.0, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[5][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[6][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[7][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, ), ), ) @@ -49,7 +72,11 @@ ... - Foo +

Pretend this is a super long paragraph that pushes the next image mostly out of the initial viewport.

Bar

Now the following image is definitely outside the initial viewport.

@@ -64,10 +91,14 @@ ... - + - Foo +

Pretend this is a super long paragraph that pushes the next image mostly out of the initial viewport.

Bar

Now the following image is definitely outside the initial viewport.

From 67c655726337d009d70391470669952c668e0c1d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 18 Aug 2024 22:24:43 -0700 Subject: [PATCH 023/156] Update auto-sizes tests to specify out-of-viewport rects --- .../tests/test-optimization-detective.php | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/plugins/auto-sizes/tests/test-optimization-detective.php b/plugins/auto-sizes/tests/test-optimization-detective.php index 0dd936c812..4bb84e8076 100644 --- a/plugins/auto-sizes/tests/test-optimization-detective.php +++ b/plugins/auto-sizes/tests/test-optimization-detective.php @@ -41,6 +41,13 @@ public function test_auto_sizes_register_tag_visitors(): void { * @return array Data. */ public function data_provider_test_od_optimize_template_output_buffer(): array { + $outside_viewport_rect = array_merge( + $this->get_sample_dom_rect(), + array( + 'top' => 1000, + ) + ); + return array( // Note: The Image Prioritizer plugin removes the loading attribute, and so then Auto Sizes does not then add sizes=auto. 'wrongly_lazy_responsive_img' => array( @@ -55,9 +62,11 @@ public function data_provider_test_od_optimize_template_output_buffer(): array { 'non_responsive_image' => array( 'element_metrics' => array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', - 'isLCP' => false, - 'intersectionRatio' => 0, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, ), 'buffer' => 'Quux', 'expected' => 'Quux', @@ -65,9 +74,11 @@ public function data_provider_test_od_optimize_template_output_buffer(): array { 'auto_sizes_added' => array( 'element_metrics' => array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', - 'isLCP' => false, - 'intersectionRatio' => 0, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, ), 'buffer' => 'Foo', 'expected' => 'Foo', @@ -75,9 +86,11 @@ public function data_provider_test_od_optimize_template_output_buffer(): array { 'auto_sizes_already_added' => array( 'element_metrics' => array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', - 'isLCP' => false, - 'intersectionRatio' => 0, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0, + 'intersectionRect' => $outside_viewport_rect, + 'boundingClientRect' => $outside_viewport_rect, ), 'buffer' => 'Foo', 'expected' => 'Foo', From def2aabd4f96a98f2ad3a65186a96e14110c1c06 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 22 Aug 2024 10:50:51 -0700 Subject: [PATCH 024/156] Use object spread Co-authored-by: swissspidy --- plugins/optimization-detective/detect.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 88f1e4cae8..3c5be96bcc 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -396,10 +396,11 @@ export default async function detect( { log( 'Sending URL metric:', urlMetric ); } - const body = Object.assign( {}, urlMetric, { + const body = { + ...urlMetric, slug: urlMetricsSlug, nonce: urlMetricsNonce, - } ); + }; const url = new URL( restApiEndpoint ); url.searchParams.set( '_wpnonce', restApiNonce ); navigator.sendBeacon( From fadc0fe46d986ced9cd0e18f9c9e520529c3ca1d Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Sun, 25 Aug 2024 21:31:44 +0530 Subject: [PATCH 025/156] Add Image_Prioritizer_Video_Tag_Visitor class --- ...ss-image-prioritizer-video-tag-visitor.php | 94 +++++++++++++++++++ 1 file changed, 94 insertions(+) create mode 100644 plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php new file mode 100644 index 0000000000..a51b512e14 --- /dev/null +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -0,0 +1,94 @@ +processor; + if ( self::VIDEO !== $processor->get_tag() ) { + return false; + } + + // Skip empty poster attributes and data: URLs. + $poster = trim( (string) $processor->get_attribute( self::POSTER ) ); + if ( '' === $poster || $this->is_data_url( $poster ) ) { + return false; + } + + $xpath = $processor->get_xpath(); + + // If this element is the LCP (for a breakpoint group), add a preload link for it. + foreach ( $context->url_metrics_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) { + $link_attributes = array_merge( + array( + 'rel' => 'preload', + 'fetchpriority' => 'high', + 'as' => 'image', + ), + array_filter( + array( + 'href' => (string) $processor->get_attribute( self::POSTER ), + ), + static function ( string $value ): bool { + return '' !== $value; + } + ) + ); + + $crossorigin = $processor->get_attribute( 'crossorigin' ); + if ( is_string( $crossorigin ) ) { + $link_attributes['crossorigin'] = 'use-credentials' === $crossorigin ? 'use-credentials' : 'anonymous'; + } + + $link_attributes['media'] = 'screen'; + + $context->link_collection->add_link( + $link_attributes, + $group->get_minimum_viewport_width(), + $group->get_maximum_viewport_width() + ); + } + + return true; + } +} From a6c9592fbfeb5b522ee9e0a23c37b3026cf6130e Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Sun, 25 Aug 2024 21:32:22 +0530 Subject: [PATCH 026/156] Add video-tag-visitor to be loaded --- plugins/image-prioritizer/load.php | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/image-prioritizer/load.php b/plugins/image-prioritizer/load.php index 550a26c8b8..7ff47aeae4 100644 --- a/plugins/image-prioritizer/load.php +++ b/plugins/image-prioritizer/load.php @@ -78,6 +78,7 @@ static function ( string $version ): void { require_once __DIR__ . '/class-image-prioritizer-tag-visitor.php'; require_once __DIR__ . '/class-image-prioritizer-img-tag-visitor.php'; + require_once __DIR__ . '/class-image-prioritizer-video-tag-visitor.php'; require_once __DIR__ . '/class-image-prioritizer-background-image-styled-tag-visitor.php'; require_once __DIR__ . '/helper.php'; require_once __DIR__ . '/hooks.php'; From 138b2b8f7366fcd4673ce82a4ce37ac37cd8e627 Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Sun, 25 Aug 2024 21:32:30 +0530 Subject: [PATCH 027/156] Add video tag visitor in the OD visitors --- plugins/image-prioritizer/helper.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/image-prioritizer/helper.php b/plugins/image-prioritizer/helper.php index e214b2c673..aed419afe1 100644 --- a/plugins/image-prioritizer/helper.php +++ b/plugins/image-prioritizer/helper.php @@ -36,4 +36,7 @@ function image_prioritizer_register_tag_visitors( OD_Tag_Visitor_Registry $regis $bg_image_visitor = new Image_Prioritizer_Background_Image_Styled_Tag_Visitor(); $registry->register( 'bg-image-tags', $bg_image_visitor ); + + $video_visitor = new Image_Prioritizer_Video_Tag_Visitor(); + $registry->register( 'video-tags', $video_visitor ); } From 03fc900183644dd16c7eab486d46f180f8303c4d Mon Sep 17 00:00:00 2001 From: thelovekesh Date: Sun, 25 Aug 2024 21:36:58 +0530 Subject: [PATCH 028/156] Update checking for crossorigin attrs on img and video tags --- .../class-image-prioritizer-img-tag-visitor.php | 2 +- .../class-image-prioritizer-video-tag-visitor.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php index 287e955f31..f3c6b8ee3b 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php @@ -111,7 +111,7 @@ static function ( string $value ): bool { ); $crossorigin = $processor->get_attribute( 'crossorigin' ); - if ( is_string( $crossorigin ) ) { + if ( null !== $crossorigin ) { $link_attributes['crossorigin'] = 'use-credentials' === $crossorigin ? 'use-credentials' : 'anonymous'; } diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index a51b512e14..06bafdae0c 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -76,7 +76,7 @@ static function ( string $value ): bool { ); $crossorigin = $processor->get_attribute( 'crossorigin' ); - if ( is_string( $crossorigin ) ) { + if ( null !== $crossorigin ) { $link_attributes['crossorigin'] = 'use-credentials' === $crossorigin ? 'use-credentials' : 'anonymous'; } From 1da219f394d546802d8b7b2c43f936152611cd59 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 17 Sep 2024 13:59:26 -0700 Subject: [PATCH 029/156] Use get_json_params() instead of get_params() so _wpnonce query param is excluded --- plugins/optimization-detective/storage/rest-api.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index 77b7fda2d0..c698bfd7d0 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -128,7 +128,7 @@ function od_handle_rest_request( WP_REST_Request $request ) { OD_Storage_Lock::set_lock(); try { - $data = $request->get_params(); + $data = $request->get_json_params(); // Remove params which are only used for the REST API request and which are not part of a URL Metric. unset( $data['slug'], From e34d9fe5a21f208d4d469e6e6fbdaebe1018b05d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 17 Sep 2024 14:42:18 -0700 Subject: [PATCH 030/156] Implement resizedBoundingClientRect extended property in schema --- .../class-embed-optimizer-tag-visitor.php | 14 +++++++- plugins/embed-optimizer/detect.js | 5 ++- plugins/embed-optimizer/hooks.php | 32 +++++++++++++++++++ .../tests/test-cases/nested-figure-embed.php | 27 ++++++++++++---- ...utside-viewport-with-subsequent-script.php | 19 +++++++++-- .../single-twitter-embed-inside-viewport.php | 17 ++++++++-- .../single-twitter-embed-outside-viewport.php | 17 ++++++++-- ...gle-wordpress-tv-embed-inside-viewport.php | 17 ++++++++-- ...le-wordpress-tv-embed-outside-viewport.php | 17 ++++++++-- .../single-youtube-embed-inside-viewport.php | 17 ++++++++-- .../single-youtube-embed-outside-viewport.php | 17 ++++++++-- .../class-od-url-metrics-group-collection.php | 2 +- 12 files changed, 168 insertions(+), 33 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index b4b9fe5367..edccb15cf3 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -83,7 +83,19 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { } $embed_wrapper_xpath = $processor->get_xpath() . '/*[1][self::DIV]'; - $minimum_height = $context->url_metrics_group_collection->get_element_minimum_height( $embed_wrapper_xpath ); + + // TODO: This should be cached. + $minimum_height = null; + foreach ( $context->url_metrics_group_collection->get_all_url_metrics_groups_elements() as $element ) { + if ( $embed_wrapper_xpath === $element['xpath'] && isset( $element['resizedBoundingClientRect'] ) ) { + if ( null === $minimum_height ) { + $minimum_height = $element['resizedBoundingClientRect']['height']; + } else { + $minimum_height = min( $minimum_height, $element['resizedBoundingClientRect']['height'] ); + } + } + } + if ( is_float( $minimum_height ) ) { $min_height_style = sprintf( 'min-height: %dpx;', $minimum_height ); $style = $processor->get_attribute( 'style' ); diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index 677ec9f1c3..65667ceb63 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -59,14 +59,13 @@ export async function finalize( { urlMetric, isDebug } ) { if ( loadedElementContentRects.has( element.xpath ) ) { if ( isDebug ) { log( - `Overriding boundingClientRect for ${ element.xpath }:`, + `boundingClientRect for ${ element.xpath } resized:`, element.boundingClientRect, '=>', loadedElementContentRects.get( element.xpath ) ); } - // TODO: Maybe element.boundingClientRect should rather be element.initialBoundingClientRect and the schema is extended by Embed Optimizer to add an element.finalBoundingClientRect (same goes for intersectionRect and intersectionRatio). - element.boundingClientRect = loadedElementContentRects.get( + element.resizedBoundingClientRect = loadedElementContentRects.get( element.xpath ); } diff --git a/plugins/embed-optimizer/hooks.php b/plugins/embed-optimizer/hooks.php index fd59a89879..e524f8243c 100644 --- a/plugins/embed-optimizer/hooks.php +++ b/plugins/embed-optimizer/hooks.php @@ -21,6 +21,7 @@ function embed_optimizer_add_hooks(): void { if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) { add_action( 'od_register_tag_visitors', 'embed_optimizer_register_tag_visitors' ); add_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_detect_embed_presence' ); + add_filter( 'od_url_metric_schema_element_item_additional_properties', 'embed_optimizer_add_element_item_schema_properties' ); } else { add_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_lazy_load' ); } @@ -40,6 +41,37 @@ function embed_optimizer_register_tag_visitors( OD_Tag_Visitor_Registry $registr $registry->register( 'embeds', new Embed_Optimizer_Tag_Visitor() ); } +/** + * Filters additional properties for the element item schema for Optimization Detective. + * + * @since n.e.x.t + * + * @param array $additional_properties Additional properties. + * @return array Additional properties. + */ +function embed_optimizer_add_element_item_schema_properties( array $additional_properties ): array { + $additional_properties['resizedBoundingClientRect'] = array( + 'type' => 'object', + 'properties' => array_fill_keys( + array( + 'width', + 'height', + 'x', + 'y', + 'top', + 'right', + 'bottom', + 'left', + ), + array( + 'type' => 'number', + 'required' => true, + ) + ), + ); + return $additional_properties; +} + /** * Filters the list of Optimization Detective extension module URLs to include the extension for Embed Optimizer. * diff --git a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php index 983228afe5..8a812d8749 100644 --- a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php +++ b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php @@ -1,12 +1,24 @@ static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + $rect = array( + 'width' => 500.1, + 'height' => 500.2, + 'x' => 100.3, + 'y' => 100.4, + 'top' => 0.1, + 'right' => 0.2, + 'bottom' => 0.3, + 'left' => 0.4, + ); + $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', - 'isLCP' => false, - 'intersectionRatio' => 1, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 1, + 'resizedBoundingClientRect' => array_merge( $rect, array( 'height' => 500 ) ), ), array( 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]/*[1][self::VIDEO]', @@ -14,9 +26,10 @@ 'intersectionRatio' => 1, ), array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::FIGURE]/*[1][self::DIV]', - 'isLCP' => false, - 'intersectionRatio' => 0, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0, + 'resizedBoundingClientRect' => array_merge( $rect, array( 'height' => 654 ) ), ), array( 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::FIGURE]/*[1][self::DIV]/*[1][self::FIGURE]/*[2][self::VIDEO]', @@ -106,7 +119,7 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool {
-
+

So I heard you like FIGURE?

diff --git a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php index 523f85e6e7..5ac269d4d1 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php +++ b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php @@ -1,12 +1,25 @@ static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + + $rect = array( + 'width' => 500.1, + 'height' => 500.2, + 'x' => 100.3, + 'y' => 100.4, + 'top' => 0.1, + 'right' => 0.2, + 'bottom' => 0.3, + 'left' => 0.4, + ); + $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', - 'isLCP' => false, - 'intersectionRatio' => 0, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0, + 'resizedBoundingClientRect' => array_merge( $rect, array( 'height' => 500 ) ), ), ), false 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 2279286a1a..5223ad9afa 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 @@ -1,12 +1,23 @@ static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + $rect = array( + 'width' => 500.1, + 'height' => 500.2, + 'x' => 100.3, + 'y' => 100.4, + 'top' => 0.1, + 'right' => 0.2, + 'bottom' => 0.3, + 'left' => 0.4, + ); $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', - 'isLCP' => true, - 'intersectionRatio' => 1, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => true, + 'intersectionRatio' => 1, + 'resizedBoundingClientRect' => array_merge( $rect, array( 'height' => 500 ) ), ), ) ); diff --git a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php index 62088e25f9..07a55a91e5 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php @@ -1,12 +1,23 @@ static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + $rect = array( + 'width' => 500.1, + 'height' => 500.2, + 'x' => 100.3, + 'y' => 100.4, + 'top' => 0.1, + 'right' => 0.2, + 'bottom' => 0.3, + 'left' => 0.4, + ); $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', - 'isLCP' => false, - 'intersectionRatio' => 0, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0, + 'resizedBoundingClientRect' => array_merge( $rect, array( 'height' => 500 ) ), ), ) ); 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 9b6234d485..fc045dced3 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 @@ -1,12 +1,23 @@ static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + $rect = array( + 'width' => 500.1, + 'height' => 500.2, + 'x' => 100.3, + 'y' => 100.4, + 'top' => 0.1, + 'right' => 0.2, + 'bottom' => 0.3, + 'left' => 0.4, + ); $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', - 'isLCP' => true, - 'intersectionRatio' => 1, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => true, + 'intersectionRatio' => 1, + 'resizedBoundingClientRect' => array_merge( $rect, array( 'height' => 500 ) ), ), ), false diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php index f9087a8399..e24f485221 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php @@ -1,12 +1,23 @@ static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + $rect = array( + 'width' => 500.1, + 'height' => 500.2, + 'x' => 100.3, + 'y' => 100.4, + 'top' => 0.1, + 'right' => 0.2, + 'bottom' => 0.3, + 'left' => 0.4, + ); $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', - 'isLCP' => false, - 'intersectionRatio' => 0, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0, + 'resizedBoundingClientRect' => array_merge( $rect, array( 'height' => 500 ) ), ), ), false 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 6c322bc701..ee60d7ccfe 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 @@ -1,12 +1,23 @@ static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + $rect = array( + 'width' => 500.1, + 'height' => 500.2, + 'x' => 100.3, + 'y' => 100.4, + 'top' => 0.1, + 'right' => 0.2, + 'bottom' => 0.3, + 'left' => 0.4, + ); $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', - 'isLCP' => true, - 'intersectionRatio' => 1, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => true, + 'intersectionRatio' => 1, + 'resizedBoundingClientRect' => array_merge( $rect, array( 'height' => 500 ) ), ), ) ); diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php index 729f25ef2c..c44cbafd0e 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php @@ -1,12 +1,23 @@ static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + $rect = array( + 'width' => 500.1, + 'height' => 500.2, + 'x' => 100.3, + 'y' => 100.4, + 'top' => 0.1, + 'right' => 0.2, + 'bottom' => 0.3, + 'left' => 0.4, + ); $test_case->populate_url_metrics( array( array( - 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', - 'isLCP' => false, - 'intersectionRatio' => 0, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => false, + 'intersectionRatio' => 0, + 'resizedBoundingClientRect' => array_merge( $rect, array( 'height' => 500 ) ), ), ) ); diff --git a/plugins/optimization-detective/class-od-url-metrics-group-collection.php b/plugins/optimization-detective/class-od-url-metrics-group-collection.php index 4aba737b03..a6f56115d7 100644 --- a/plugins/optimization-detective/class-od-url-metrics-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metrics-group-collection.php @@ -408,7 +408,7 @@ public function get_common_lcp_element(): ?array { * * @return Generator */ - protected function get_all_url_metrics_groups_elements(): Generator { + public function get_all_url_metrics_groups_elements(): Generator { foreach ( $this->groups as $group ) { foreach ( $group as $url_metric ) { foreach ( $url_metric->get_elements() as $element ) { From 5db6f54d9d1196501008fdbcfc7206b99b75e7b0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 18 Sep 2024 15:49:21 -0700 Subject: [PATCH 031/156] Fix testing JSON request --- .../tests/storage/test-rest-api.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/plugins/optimization-detective/tests/storage/test-rest-api.php b/plugins/optimization-detective/tests/storage/test-rest-api.php index 603d40cd98..4570463924 100644 --- a/plugins/optimization-detective/tests/storage/test-rest-api.php +++ b/plugins/optimization-detective/tests/storage/test-rest-api.php @@ -66,7 +66,8 @@ public function test_rest_request_good_params( Closure $set_up ): void { $valid_params = $set_up(); $request = new WP_REST_Request( 'POST', self::ROUTE ); $this->assertCount( 0, get_posts( array( 'post_type' => OD_URL_Metrics_Post_Type::SLUG ) ) ); - $request->set_body_params( $valid_params ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $valid_params ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status(), 'Response: ' . wp_json_encode( $response ) ); @@ -221,7 +222,8 @@ function ( $params ) { */ public function test_rest_request_bad_params( array $params ): void { $request = new WP_REST_Request( 'POST', self::ROUTE ); - $request->set_body_params( $params ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 400, $response->get_status(), 'Response: ' . wp_json_encode( $response ) ); $this->assertSame( 'rest_invalid_param', $response->get_data()['code'], 'Response: ' . wp_json_encode( $response ) ); @@ -246,7 +248,8 @@ public function test_rest_request_timestamp_ignored(): void { 'uuid' => wp_generate_uuid4(), ) ); - $request->set_body_params( $params ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status(), 'Response: ' . wp_json_encode( $response ) ); @@ -308,7 +311,8 @@ public function test_rest_request_breakpoint_not_needed_for_any_breakpoint(): vo // The next request will be rejected because all groups are fully populated with samples. $request = new WP_REST_Request( 'POST', self::ROUTE ); - $request->set_body_params( $this->get_valid_params() ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $this->get_valid_params() ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 403, $response->get_status() ); } @@ -438,7 +442,8 @@ static function () use ( $breakpoint_width ): array { private function populate_url_metrics( int $count, array $params ): void { for ( $i = 0; $i < $count; $i++ ) { $request = new WP_REST_Request( 'POST', self::ROUTE ); - $request->set_body_params( $params ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); } From 0fa263a75ef90c0f404c71621b1d7cb9dd5482a1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 18 Sep 2024 16:09:24 -0700 Subject: [PATCH 032/156] Go back to get_params() by ignoring _wpnonce This is needed because get_json_params() can return null. Also, no need to force the request body to be JSON. See 1da219f394d546802d8b7b2c43f936152611cd59 --- plugins/optimization-detective/storage/rest-api.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/optimization-detective/storage/rest-api.php b/plugins/optimization-detective/storage/rest-api.php index c698bfd7d0..73f3a36fc0 100644 --- a/plugins/optimization-detective/storage/rest-api.php +++ b/plugins/optimization-detective/storage/rest-api.php @@ -128,9 +128,10 @@ function od_handle_rest_request( WP_REST_Request $request ) { OD_Storage_Lock::set_lock(); try { - $data = $request->get_json_params(); + $data = $request->get_params(); // Remove params which are only used for the REST API request and which are not part of a URL Metric. unset( + $data['_wpnonce'], $data['slug'], $data['nonce'] ); From 2a723f74a856254ec03669476be4ed1e4cbe2826 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Sun, 29 Sep 2024 16:52:07 -0700 Subject: [PATCH 033/156] Fix jsdoc --- plugins/embed-optimizer/detect.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index 65667ceb63..b7d258ecbf 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -44,7 +44,7 @@ export async function initialize( { isDebug } ) { } /** - * Initialize. + * Finalize. * * @param {Object} args Args. * @param {boolean} args.isDebug Whether to show debug messages. From 29d4383a69d2664e15f7a5e155689302975ef4ad Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 4 Oct 2024 11:43:06 -0700 Subject: [PATCH 034/156] Eliminate use of deprecated property --- plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index c9656a8a70..7ada9a666a 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -86,7 +86,7 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { // TODO: This should be cached. $minimum_height = null; - foreach ( $context->url_metrics_group_collection->get_all_url_metrics_groups_elements() as $element ) { + foreach ( $context->url_metric_group_collection->get_all_url_metrics_groups_elements() as $element ) { if ( $embed_wrapper_xpath === $element['xpath'] && isset( $element['resizedBoundingClientRect'] ) ) { if ( null === $minimum_height ) { $minimum_height = $element['resizedBoundingClientRect']['height']; From a529218e33485e9cf26150ebe2e391bcb6012d71 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 4 Oct 2024 13:42:48 -0700 Subject: [PATCH 035/156] Add breakpoint-specific min-heights to account for responsive embeds --- .../class-embed-optimizer-tag-visitor.php | 54 +++++++++++++------ .../tests/test-cases/nested-figure-embed.php | 18 +++++-- ...utside-viewport-with-subsequent-script.php | 8 ++- .../single-twitter-embed-inside-viewport.php | 8 ++- .../single-twitter-embed-outside-viewport.php | 8 ++- ...gle-wordpress-tv-embed-inside-viewport.php | 8 ++- ...le-wordpress-tv-embed-outside-viewport.php | 8 ++- .../single-youtube-embed-inside-viewport.php | 8 ++- .../single-youtube-embed-outside-viewport.php | 8 ++- .../class-od-url-metric-group-collection.php | 13 +++-- .../class-od-url-metric-group.php | 2 + 11 files changed, 113 insertions(+), 30 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index 7ada9a666a..8f99e9faa1 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -84,27 +84,51 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $embed_wrapper_xpath = $processor->get_xpath() . '/*[1][self::DIV]'; - // TODO: This should be cached. - $minimum_height = null; - foreach ( $context->url_metric_group_collection->get_all_url_metrics_groups_elements() as $element ) { - if ( $embed_wrapper_xpath === $element['xpath'] && isset( $element['resizedBoundingClientRect'] ) ) { - if ( null === $minimum_height ) { - $minimum_height = $element['resizedBoundingClientRect']['height']; + /** + * Array of tuples of groups and their minimum heights keyed by the minimum viewport width. + * + * @var array $group_minimum_heights + */ + $group_minimum_heights = array(); + // TODO: This can be made more efficient if the get_all_url_metrics_groups_elements return value included an elements_by_xpath key. + foreach ( $context->url_metric_group_collection->get_all_url_metrics_groups_elements() as list( $group, $element ) ) { + if ( isset( $element['resizedBoundingClientRect'] ) && $embed_wrapper_xpath === $element['xpath'] ) { + $group_min_width = $group->get_minimum_viewport_width(); + if ( ! isset( $group_minimum_heights[ $group_min_width ] ) ) { + $group_minimum_heights[ $group_min_width ] = array( $group, $element['resizedBoundingClientRect']['height'] ); } else { - $minimum_height = min( $minimum_height, $element['resizedBoundingClientRect']['height'] ); + $group_minimum_heights[ $group_min_width ][1] = min( + $group_minimum_heights[ $group_min_width ][1], + $element['resizedBoundingClientRect']['height'] + ); } } } - if ( is_float( $minimum_height ) ) { - $min_height_style = sprintf( 'min-height: %dpx;', $minimum_height ); - $style = $processor->get_attribute( 'style' ); - if ( is_string( $style ) ) { - $style = $min_height_style . ' ' . $style; - } else { - $style = $min_height_style; + // Add style rules to set the min-height for each viewport group. + if ( count( $group_minimum_heights ) > 0 ) { + $element_id = $processor->get_attribute( 'id' ); + if ( ! is_string( $element_id ) ) { + $element_id = 'embed-optimizer-' . md5( $processor->get_xpath() ); + $processor->set_attribute( 'id', $element_id ); + } + + $style_rules = array(); + foreach ( $group_minimum_heights as list( $group, $minimum_height ) ) { + // TODO: The following media query logic can be added to a method on the group class. + $media_query = sprintf( 'screen and (min-width: %dpx)', $group->get_minimum_viewport_width() ); + if ( $group->get_maximum_viewport_width() !== PHP_INT_MAX ) { + $media_query .= sprintf( ' and (max-width: %dpx)', $group->get_maximum_viewport_width() ); + } + $style_rules[] = sprintf( + '@media %s { #%s { min-height: %dpx; } }', + $media_query, + $element_id, + $minimum_height + ); } - $processor->set_attribute( 'style', $style ); + + $processor->append_head_html( sprintf( "\n", join( "\n", $style_rules ) ) ); } $max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( $embed_wrapper_xpath ); diff --git a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php index 0259b116bc..05ba3f7bce 100644 --- a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php +++ b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php @@ -92,7 +92,7 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool {
-
+

So I heard you like FIGURE?

@@ -111,15 +111,27 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool { ... + + -
+
-
+

So I heard you like FIGURE?

diff --git a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php index 5ac269d4d1..c71aa62824 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php +++ b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php @@ -46,9 +46,15 @@ ... + -
+
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 5223ad9afa..f14b6f4eff 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 @@ -43,11 +43,17 @@ ... + -
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php index 07a55a91e5..0fdfa26c7c 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php @@ -43,9 +43,15 @@ ... + -
+
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 fc045dced3..f4c00d5121 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 @@ -44,13 +44,19 @@ ... + -
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php index e24f485221..3f6c047835 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php @@ -44,9 +44,15 @@ ... + -
+
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 ee60d7ccfe..ac62809de0 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 @@ -42,11 +42,17 @@ ... + -
+
diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php index c44cbafd0e..b2fb6fb02e 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php @@ -42,9 +42,15 @@ ... + -
+
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 997e073038..750d243cb2 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -404,18 +404,21 @@ public function get_common_lcp_element(): ?array { * (the default sample size). Therefore, given the number (n) of visited elements on the page this will only * end up running n*4*3 times. * + * @todo This should be cached. * @since n.e.x.t * - * @return Generator + * @return array */ - public function get_all_url_metrics_groups_elements(): Generator { + public function get_all_url_metrics_groups_elements(): array { + $elements_and_groups = array(); foreach ( $this->groups as $group ) { foreach ( $group as $url_metric ) { foreach ( $url_metric->get_elements() as $element ) { - yield $element; + $elements_and_groups[] = array( $group, $element ); } } } + return $elements_and_groups; } /** @@ -430,7 +433,7 @@ public function get_all_element_max_intersection_ratios(): array { $result = ( function () { $element_max_intersection_ratios = array(); - foreach ( $this->get_all_url_metrics_groups_elements() as $element ) { + foreach ( $this->get_all_url_metrics_groups_elements() as list( $group, $element ) ) { $element_max_intersection_ratios[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_max_intersection_ratios ) ? max( $element_max_intersection_ratios[ $element['xpath'] ], $element['intersectionRatio'] ) : $element['intersectionRatio']; @@ -457,7 +460,7 @@ public function get_all_element_minimum_heights(): array { $result = ( function () { $element_min_heights = array(); - foreach ( $this->get_all_url_metrics_groups_elements() as $element ) { + foreach ( $this->get_all_url_metrics_groups_elements() as list( $group, $element ) ) { $element_min_heights[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_min_heights ) ? min( $element_min_heights[ $element['xpath'] ], $element['boundingClientRect']['height'] ) : $element['boundingClientRect']['height']; diff --git a/plugins/optimization-detective/class-od-url-metric-group.php b/plugins/optimization-detective/class-od-url-metric-group.php index 091872cf98..1b9b51315c 100644 --- a/plugins/optimization-detective/class-od-url-metric-group.php +++ b/plugins/optimization-detective/class-od-url-metric-group.php @@ -145,6 +145,7 @@ public function __construct( array $url_metrics, int $minimum_viewport_width, in /** * Gets the minimum possible viewport width (inclusive). * + * @todo Eliminate in favor of readonly public property. * @return int<0, max> Minimum viewport width. */ public function get_minimum_viewport_width(): int { @@ -154,6 +155,7 @@ public function get_minimum_viewport_width(): int { /** * Gets the maximum possible viewport width (inclusive). * + * @todo Eliminate in favor of readonly public property. * @return int<1, max> Minimum viewport width. */ public function get_maximum_viewport_width(): int { From fa8a34eee4905ffdd5a797fc081029478db3c149 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 4 Oct 2024 14:06:32 -0700 Subject: [PATCH 036/156] Add od_generate_media_query() helper --- .../class-embed-optimizer-tag-visitor.php | 9 +-- .../tests/test-cases/nested-figure-embed.php | 16 +++--- ...utside-viewport-with-subsequent-script.php | 8 +-- .../single-twitter-embed-inside-viewport.php | 8 +-- .../single-twitter-embed-outside-viewport.php | 8 +-- ...gle-wordpress-tv-embed-inside-viewport.php | 8 +-- ...le-wordpress-tv-embed-outside-viewport.php | 8 +-- .../single-youtube-embed-inside-viewport.php | 8 +-- .../single-youtube-embed-outside-viewport.php | 8 +-- .../class-od-link-collection.php | 15 ++--- plugins/optimization-detective/helper.php | 27 +++++++++ .../tests/test-helper.php | 57 +++++++++++++++++++ 12 files changed, 126 insertions(+), 54 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index 8f99e9faa1..165d6703f7 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -106,7 +106,7 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { } // Add style rules to set the min-height for each viewport group. - if ( count( $group_minimum_heights ) > 0 ) { + if ( count( $group_minimum_heights ) > 0 && function_exists( 'od_generate_media_query' ) ) { // TODO: Remove the function_exists() check after a few releases. $element_id = $processor->get_attribute( 'id' ); if ( ! is_string( $element_id ) ) { $element_id = 'embed-optimizer-' . md5( $processor->get_xpath() ); @@ -115,14 +115,9 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $style_rules = array(); foreach ( $group_minimum_heights as list( $group, $minimum_height ) ) { - // TODO: The following media query logic can be added to a method on the group class. - $media_query = sprintf( 'screen and (min-width: %dpx)', $group->get_minimum_viewport_width() ); - if ( $group->get_maximum_viewport_width() !== PHP_INT_MAX ) { - $media_query .= sprintf( ' and (max-width: %dpx)', $group->get_maximum_viewport_width() ); - } $style_rules[] = sprintf( '@media %s { #%s { min-height: %dpx; } }', - $media_query, + od_generate_media_query( $group->get_minimum_viewport_width(), $group->get_maximum_viewport_width() ), $element_id, $minimum_height ); diff --git a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php index 05ba3f7bce..c9ae145167 100644 --- a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php +++ b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed.php @@ -112,16 +112,16 @@ static function ( OD_Tag_Visitor_Context $context ) use ( $test_case ): bool { ... diff --git a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php index c71aa62824..6e2e483cfe 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php +++ b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script.php @@ -47,10 +47,10 @@ ... 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 f14b6f4eff..63968e9758 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 @@ -44,10 +44,10 @@ ... diff --git a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php index 0fdfa26c7c..770c8de3e2 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-outside-viewport.php @@ -44,10 +44,10 @@ ... 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 f4c00d5121..24f56c8e4b 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 @@ -45,10 +45,10 @@ ... diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php index 3f6c047835..b7255a6b81 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport.php @@ -45,10 +45,10 @@ ... 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 ac62809de0..78dcf667a9 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 @@ -43,10 +43,10 @@ ... diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php index b2fb6fb02e..f95d503763 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport.php @@ -43,10 +43,10 @@ ... diff --git a/plugins/optimization-detective/class-od-link-collection.php b/plugins/optimization-detective/class-od-link-collection.php index b1fbfacb6c..49b8669ed4 100644 --- a/plugins/optimization-detective/class-od-link-collection.php +++ b/plugins/optimization-detective/class-od-link-collection.php @@ -192,20 +192,13 @@ static function ( array $carry, array $link ): array { // Add media attributes to the deduplicated links. return array_map( static function ( array $link ): array { - $media_attributes = array(); - if ( null !== $link['minimum_viewport_width'] && $link['minimum_viewport_width'] > 0 ) { - $media_attributes[] = sprintf( '(min-width: %dpx)', $link['minimum_viewport_width'] ); - } - if ( null !== $link['maximum_viewport_width'] && PHP_INT_MAX !== $link['maximum_viewport_width'] ) { - $media_attributes[] = sprintf( '(max-width: %dpx)', $link['maximum_viewport_width'] ); - } - if ( count( $media_attributes ) > 0 ) { + $media_query = od_generate_media_query( $link['minimum_viewport_width'], $link['maximum_viewport_width'] ); + if ( null !== $media_query ) { if ( ! isset( $link['attributes']['media'] ) ) { - $link['attributes']['media'] = ''; + $link['attributes']['media'] = $media_query; } else { - $link['attributes']['media'] .= ' and '; + $link['attributes']['media'] .= " and $media_query"; } - $link['attributes']['media'] .= implode( ' and ', $media_attributes ); } return $link['attributes']; }, diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php index 553d55cb11..d18ad80652 100644 --- a/plugins/optimization-detective/helper.php +++ b/plugins/optimization-detective/helper.php @@ -10,6 +10,33 @@ exit; // Exit if accessed directly. } +/** + * Generates a media query for the provided minimum and maximum viewport widths. + * + * @since n.e.x.t + * + * @param int|null $minimum_viewport_width Minimum viewport width. + * @param int|null $maximum_viewport_width Maximum viewport width. + * @return non-empty-string|null Media query, or null if the min/max were both unspecified or invalid. + */ +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 n.e.x.t' ); + return null; + } + $media_attributes = array(); + if ( null !== $minimum_viewport_width && $minimum_viewport_width > 0 ) { + $media_attributes[] = sprintf( '(min-width: %dpx)', $minimum_viewport_width ); + } + if ( null !== $maximum_viewport_width && PHP_INT_MAX !== $maximum_viewport_width ) { + $media_attributes[] = sprintf( '(max-width: %dpx)', $maximum_viewport_width ); + } + if ( count( $media_attributes ) === 0 ) { + return null; + } + return join( ' and ', $media_attributes ); +} + /** * Displays the HTML generator meta tag for the Optimization Detective plugin. * diff --git a/plugins/optimization-detective/tests/test-helper.php b/plugins/optimization-detective/tests/test-helper.php index af824a3631..77000f25de 100644 --- a/plugins/optimization-detective/tests/test-helper.php +++ b/plugins/optimization-detective/tests/test-helper.php @@ -7,6 +7,63 @@ class Test_OD_Helper extends WP_UnitTestCase { + /** + * @return array> + */ + public function data_to_test_od_generate_media_query(): array { + return array( + 'mobile' => array( + 'min_width' => 0, + 'max_width' => 320, + 'expected' => '(max-width: 320px)', + ), + 'mobile_alt' => array( + 'min_width' => null, + 'max_width' => 320, + 'expected' => '(max-width: 320px)', + ), + 'tablet' => array( + 'min_width' => 321, + 'max_width' => 600, + 'expected' => '(min-width: 321px) and (max-width: 600px)', + ), + 'desktop' => array( + 'min_width' => 601, + 'max_width' => PHP_INT_MAX, + 'expected' => '(min-width: 601px)', + ), + 'desktop_alt' => array( + 'min_width' => 601, + 'max_width' => null, + 'expected' => '(min-width: 601px)', + ), + 'no_widths' => array( + 'min_width' => null, + 'max_width' => null, + 'expected' => null, + ), + 'bad_widths' => array( + 'min_width' => 1000, + 'max_width' => 10, + 'expected' => null, + 'incorrect_usage' => 'od_generate_media_query', + ), + ); + } + + /** + * Test generating media query. + * + * @dataProvider data_to_test_od_generate_media_query + * @covers ::od_generate_media_query + */ + public function test_od_generate_media_query( ?int $min_width, ?int $max_width, ?string $expected, ?string $incorrect_usage = null ): void { + if ( null !== $incorrect_usage ) { + $this->setExpectedIncorrectUsage( $incorrect_usage ); + } + $this->assertSame( $expected, od_generate_media_query( $min_width, $max_width ) ); + } + /** * Test printing the meta generator tag. * From 1e40f848b88a5abd21f86efd7e3da862fed59ce3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 4 Oct 2024 14:19:04 -0700 Subject: [PATCH 037/156] Break up embed tag visitor into separate methods --- .../class-embed-optimizer-tag-visitor.php | 117 +++++++++++------- 1 file changed, 73 insertions(+), 44 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index 165d6703f7..e735db488d 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -82,51 +82,9 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { return false; } - $embed_wrapper_xpath = $processor->get_xpath() . '/*[1][self::DIV]'; + $this->reduce_layout_shifts( $context ); - /** - * Array of tuples of groups and their minimum heights keyed by the minimum viewport width. - * - * @var array $group_minimum_heights - */ - $group_minimum_heights = array(); - // TODO: This can be made more efficient if the get_all_url_metrics_groups_elements return value included an elements_by_xpath key. - foreach ( $context->url_metric_group_collection->get_all_url_metrics_groups_elements() as list( $group, $element ) ) { - if ( isset( $element['resizedBoundingClientRect'] ) && $embed_wrapper_xpath === $element['xpath'] ) { - $group_min_width = $group->get_minimum_viewport_width(); - if ( ! isset( $group_minimum_heights[ $group_min_width ] ) ) { - $group_minimum_heights[ $group_min_width ] = array( $group, $element['resizedBoundingClientRect']['height'] ); - } else { - $group_minimum_heights[ $group_min_width ][1] = min( - $group_minimum_heights[ $group_min_width ][1], - $element['resizedBoundingClientRect']['height'] - ); - } - } - } - - // Add style rules to set the min-height for each viewport group. - if ( count( $group_minimum_heights ) > 0 && function_exists( 'od_generate_media_query' ) ) { // TODO: Remove the function_exists() check after a few releases. - $element_id = $processor->get_attribute( 'id' ); - if ( ! is_string( $element_id ) ) { - $element_id = 'embed-optimizer-' . md5( $processor->get_xpath() ); - $processor->set_attribute( 'id', $element_id ); - } - - $style_rules = array(); - foreach ( $group_minimum_heights as list( $group, $minimum_height ) ) { - $style_rules[] = sprintf( - '@media %s { #%s { min-height: %dpx; } }', - od_generate_media_query( $group->get_minimum_viewport_width(), $group->get_maximum_viewport_width() ), - $element_id, - $minimum_height - ); - } - - $processor->append_head_html( sprintf( "\n", join( "\n", $style_rules ) ) ); - } - - $max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( $embed_wrapper_xpath ); + $max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( $this->get_embed_wrapper_xpath( $context ) ); if ( $max_intersection_ratio > 0 ) { /* * The following embeds have been chosen for optimization due to their relative popularity among all embed types. @@ -208,4 +166,75 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { */ return false; } + + /** + * Gets the XPath for the embed wrapper. + * + * @since n.e.x.t + * + * @param OD_Tag_Visitor_Context $context Tag visitor context. + * @return string XPath. + */ + private function get_embed_wrapper_xpath( OD_Tag_Visitor_Context $context ): string { + return $context->processor->get_xpath() . '/*[1][self::DIV]'; + } + + /** + * Reduces layout shifts. + * + * @since n.e.x.t + * + * @param OD_Tag_Visitor_Context $context Tag visitor context. + */ + private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { + // TODO: Remove this condition once no longer likely that Optimization Detective v0.6.0 is no longer in the wild. Or should the plugin short-circuit if OPTIMIZATION_DETECTIVE_VERSION is not 0.7.0 (for example) or higher? + if ( ! function_exists( 'od_generate_media_query' ) || ! method_exists( $context->url_metric_group_collection, 'get_all_url_metrics_groups_elements' ) ) { + return; + } + + $processor = $context->processor; + $embed_wrapper_xpath = $this->get_embed_wrapper_xpath( $context ); + + /** + * Array of tuples of groups and their minimum heights keyed by the minimum viewport width. + * + * @var array $group_minimum_heights + */ + $group_minimum_heights = array(); + // TODO: This can be made more efficient if the get_all_url_metrics_groups_elements return value included an elements_by_xpath key. + foreach ( $context->url_metric_group_collection->get_all_url_metrics_groups_elements() as list( $group, $element ) ) { + if ( isset( $element['resizedBoundingClientRect'] ) && $embed_wrapper_xpath === $element['xpath'] ) { + $group_min_width = $group->get_minimum_viewport_width(); + if ( ! isset( $group_minimum_heights[ $group_min_width ] ) ) { + $group_minimum_heights[ $group_min_width ] = array( $group, $element['resizedBoundingClientRect']['height'] ); + } else { + $group_minimum_heights[ $group_min_width ][1] = min( + $group_minimum_heights[ $group_min_width ][1], + $element['resizedBoundingClientRect']['height'] + ); + } + } + } + + // Add style rules to set the min-height for each viewport group. + if ( count( $group_minimum_heights ) > 0 ) { + $element_id = $processor->get_attribute( 'id' ); + if ( ! is_string( $element_id ) ) { + $element_id = 'embed-optimizer-' . md5( $processor->get_xpath() ); + $processor->set_attribute( 'id', $element_id ); + } + + $style_rules = array(); + foreach ( $group_minimum_heights as list( $group, $minimum_height ) ) { + $style_rules[] = sprintf( + '@media %s { #%s { min-height: %dpx; } }', + od_generate_media_query( $group->get_minimum_viewport_width(), $group->get_maximum_viewport_width() ), + $element_id, + $minimum_height + ); + } + + $processor->append_head_html( sprintf( "\n", join( "\n", $style_rules ) ) ); + } + } } From 5d4d5b2cc23d5902a309127ff5b07fa8d6d26ae2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 09:46:47 -0700 Subject: [PATCH 038/156] Bump alpha versions --- plugins/embed-optimizer/load.php | 4 ++-- plugins/image-prioritizer/load.php | 4 ++-- plugins/optimization-detective/load.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugins/embed-optimizer/load.php b/plugins/embed-optimizer/load.php index 1bca76100f..061367fd80 100644 --- a/plugins/embed-optimizer/load.php +++ b/plugins/embed-optimizer/load.php @@ -5,7 +5,7 @@ * Description: Optimizes the performance of embeds by lazy-loading iframes and scripts. * Requires at least: 6.5 * Requires PHP: 7.2 - * Version: 0.2.0 + * Version: 0.3.0-alpha * Author: WordPress Performance Team * Author URI: https://make.wordpress.org/performance/ * License: GPLv2 or later @@ -65,7 +65,7 @@ static function ( string $global_var_name, string $version, Closure $load ): voi } )( 'embed_optimizer_pending_plugin', - '0.2.0', + '0.3.0-alpha', static function ( string $version ): void { define( 'EMBED_OPTIMIZER_VERSION', $version ); diff --git a/plugins/image-prioritizer/load.php b/plugins/image-prioritizer/load.php index 97b9983ee2..6055f59099 100644 --- a/plugins/image-prioritizer/load.php +++ b/plugins/image-prioritizer/load.php @@ -6,7 +6,7 @@ * Requires at least: 6.5 * Requires PHP: 7.2 * Requires Plugins: optimization-detective - * Version: 0.1.4 + * Version: 0.1.5-alpha * Author: WordPress Performance Team * Author URI: https://make.wordpress.org/performance/ * License: GPLv2 or later @@ -66,7 +66,7 @@ static function ( string $global_var_name, string $version, Closure $load ): voi } )( 'image_prioritizer_pending_plugin', - '0.1.4', + '0.1.5-alpha', static function ( string $version ): void { // Define the constant. diff --git a/plugins/optimization-detective/load.php b/plugins/optimization-detective/load.php index a5f241f273..cf2ace22b4 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.6.0 + * Version: 0.7.0-alpha * Author: WordPress Performance Team * Author URI: https://make.wordpress.org/performance/ * License: GPLv2 or later @@ -65,7 +65,7 @@ static function ( string $global_var_name, string $version, Closure $load ): voi } )( 'optimization_detective_pending_plugin', - '0.6.0', + '0.7.0-alpha', static function ( string $version ): void { // Define the constant. From 5f1c2acf3f7967f20d8db051a373af78fa3bbab0 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 09:47:20 -0700 Subject: [PATCH 039/156] Add missing short-circuit in case EMBED_OPTIMIZER_VERSION is defined --- plugins/embed-optimizer/load.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/embed-optimizer/load.php b/plugins/embed-optimizer/load.php index 061367fd80..82ff88bab0 100644 --- a/plugins/embed-optimizer/load.php +++ b/plugins/embed-optimizer/load.php @@ -67,6 +67,9 @@ static function ( string $global_var_name, string $version, Closure $load ): voi 'embed_optimizer_pending_plugin', '0.3.0-alpha', static function ( string $version ): void { + if ( defined( 'EMBED_OPTIMIZER_VERSION' ) ) { + return; + } define( 'EMBED_OPTIMIZER_VERSION', $version ); From 915e1e7afa4971cc8c7ef34c237e4343a334fba5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 11:55:32 -0700 Subject: [PATCH 040/156] Rework bootstrap logic to wait until init priority 9 and add od_init action for extensions to hook into --- plugins/embed-optimizer/hooks.php | 49 ++++++++++++++++--- plugins/embed-optimizer/load.php | 11 +++-- ...lass-image-prioritizer-img-tag-visitor.php | 3 -- plugins/image-prioritizer/helper.php | 35 +++++++++++++ plugins/image-prioritizer/hooks.php | 4 +- plugins/image-prioritizer/load.php | 16 +++--- plugins/optimization-detective/helper.php | 16 ++++++ plugins/optimization-detective/hooks.php | 1 + plugins/optimization-detective/load.php | 13 +++-- 9 files changed, 120 insertions(+), 28 deletions(-) diff --git a/plugins/embed-optimizer/hooks.php b/plugins/embed-optimizer/hooks.php index e524f8243c..632dc286b8 100644 --- a/plugins/embed-optimizer/hooks.php +++ b/plugins/embed-optimizer/hooks.php @@ -18,15 +18,52 @@ function embed_optimizer_add_hooks(): void { add_action( 'wp_head', 'embed_optimizer_render_generator' ); - if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) { - add_action( 'od_register_tag_visitors', 'embed_optimizer_register_tag_visitors' ); - add_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_detect_embed_presence' ); - add_filter( 'od_url_metric_schema_element_item_additional_properties', 'embed_optimizer_add_element_item_schema_properties' ); - } else { + add_action( 'od_init', 'embed_optimizer_init_optimization_detective' ); + add_action( 'wp_loaded', 'embed_optimizer_add_non_optimization_detective_hooks' ); +} +add_action( 'init', 'embed_optimizer_add_hooks' ); + +/** + * Adds hooks for when the Optimization Detective logic is not running. + * + * @since n.e.x.t + */ +function embed_optimizer_add_non_optimization_detective_hooks(): void { + if ( false === has_action( 'od_register_tag_visitors', 'embed_optimizer_register_tag_visitors' ) ) { add_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_lazy_load' ); } } -add_action( 'init', 'embed_optimizer_add_hooks' ); + +/** + * Initializes Embed Optimizer when Optimization Detective has loaded. + * + * @since n.e.x.t + * + * @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'; + if ( ! version_compare( (string) strtok( $optimization_detective_version, '-' ), $required_od_version, '>=' ) ) { + add_action( + 'admin_notices', + static function (): void { + global $pagenow; + if ( ! in_array( $pagenow, array( 'index.php', 'plugins.php' ), true ) ) { + return; + } + wp_admin_notice( + esc_html__( 'The Embed Optimizer plugin requires a newer version of the Optimization Detective plugin. Please update your plugins.', 'embed-optimizer' ), + array( 'type' => 'warning' ) + ); + } + ); + return; + } + + add_action( 'od_register_tag_visitors', 'embed_optimizer_register_tag_visitors' ); + add_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_detect_embed_presence' ); + add_filter( 'od_url_metric_schema_element_item_additional_properties', 'embed_optimizer_add_element_item_schema_properties' ); +} /** * Registers the tag visitor for embeds. diff --git a/plugins/embed-optimizer/load.php b/plugins/embed-optimizer/load.php index 82ff88bab0..002cf97877 100644 --- a/plugins/embed-optimizer/load.php +++ b/plugins/embed-optimizer/load.php @@ -43,9 +43,14 @@ static function ( string $global_var_name, string $version, Closure $load ): voi } }; - // Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action is used - // because it is the first action that fires once the theme is loaded. - add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN ); + /* + * Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action could be + * used since it is the first action that fires once the theme is loaded. However, plugins may embed this + * logic inside a module which initializes even later at the init action. The earliest action that this + * plugin has hooks for is the init action at the default priority of 10 (which includes the rest_api_init + * action), so this is why it gets initialized at priority 9. + */ + add_action( 'init', $bootstrap, 9 ); } // Register this copy of the plugin. diff --git a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php index 7884de1b5b..80e88075f8 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php @@ -69,9 +69,6 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { } elseif ( is_string( $processor->get_attribute( 'fetchpriority' ) ) && - // Temporary condition in case someone updates Image Prioritizer without also updating Optimization Detective. - method_exists( $context->url_metric_group_collection, 'is_any_group_populated' ) - && $context->url_metric_group_collection->is_any_group_populated() ) { /* diff --git a/plugins/image-prioritizer/helper.php b/plugins/image-prioritizer/helper.php index e214b2c673..33b8983fc8 100644 --- a/plugins/image-prioritizer/helper.php +++ b/plugins/image-prioritizer/helper.php @@ -10,6 +10,41 @@ exit; // Exit if accessed directly. } +/** + * Initializes Image Prioritizer when Optimization Detective has loaded. + * + * @since n.e.x.t + * + * @param string $optimization_detective_version Current version of the optimization detective plugin. + */ +function image_prioritizer_init( string $optimization_detective_version ): void { + $required_od_version = '0.7.0'; + if ( ! version_compare( (string) strtok( $optimization_detective_version, '-' ), $required_od_version, '>=' ) ) { + add_action( + 'admin_notices', + static function (): void { + global $pagenow; + if ( ! in_array( $pagenow, array( 'index.php', 'plugins.php' ), true ) ) { + return; + } + wp_admin_notice( + esc_html__( 'The Image Prioritizer plugin requires a newer version of the Optimization Detective plugin. Please update your plugins.', 'image-prioritizer' ), + array( 'type' => 'warning' ) + ); + } + ); + return; + } + + // Classes are required here because only here do we know the expected version of Optimization Detective is active. + require_once __DIR__ . '/class-image-prioritizer-tag-visitor.php'; + require_once __DIR__ . '/class-image-prioritizer-img-tag-visitor.php'; + require_once __DIR__ . '/class-image-prioritizer-background-image-styled-tag-visitor.php'; + + add_action( 'wp_head', 'image_prioritizer_render_generator_meta_tag' ); + add_action( 'od_register_tag_visitors', 'image_prioritizer_register_tag_visitors' ); +} + /** * Displays the HTML generator meta tag for the Image Prioritizer plugin. * diff --git a/plugins/image-prioritizer/hooks.php b/plugins/image-prioritizer/hooks.php index 6ec73410c3..62d2fd3158 100644 --- a/plugins/image-prioritizer/hooks.php +++ b/plugins/image-prioritizer/hooks.php @@ -10,6 +10,4 @@ exit; // Exit if accessed directly. } -add_action( 'wp_head', 'image_prioritizer_render_generator_meta_tag' ); - -add_action( 'od_register_tag_visitors', 'image_prioritizer_register_tag_visitors' ); +add_action( 'od_init', 'image_prioritizer_init' ); diff --git a/plugins/image-prioritizer/load.php b/plugins/image-prioritizer/load.php index 6055f59099..8c1d4cfd06 100644 --- a/plugins/image-prioritizer/load.php +++ b/plugins/image-prioritizer/load.php @@ -44,9 +44,14 @@ static function ( string $global_var_name, string $version, Closure $load ): voi } }; - // Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action is used - // because it is the first action that fires once the theme is loaded. - add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN ); + /* + * Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action could be + * used since it is the first action that fires once the theme is loaded. However, plugins may embed this + * logic inside a module which initializes even later at the init action. The earliest action that this + * plugin has hooks for is the init action at the default priority of 10 (which includes the rest_api_init + * action), so this is why it gets initialized at priority 9. + */ + add_action( 'init', $bootstrap, 9 ); } // Register this copy of the plugin. @@ -68,17 +73,12 @@ static function ( string $global_var_name, string $version, Closure $load ): voi 'image_prioritizer_pending_plugin', '0.1.5-alpha', static function ( string $version ): void { - - // Define the constant. if ( defined( 'IMAGE_PRIORITIZER_VERSION' ) ) { return; } define( 'IMAGE_PRIORITIZER_VERSION', $version ); - require_once __DIR__ . '/class-image-prioritizer-tag-visitor.php'; - require_once __DIR__ . '/class-image-prioritizer-img-tag-visitor.php'; - require_once __DIR__ . '/class-image-prioritizer-background-image-styled-tag-visitor.php'; require_once __DIR__ . '/helper.php'; require_once __DIR__ . '/hooks.php'; } diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php index d18ad80652..c8b95f050e 100644 --- a/plugins/optimization-detective/helper.php +++ b/plugins/optimization-detective/helper.php @@ -10,6 +10,22 @@ exit; // Exit if accessed directly. } +/** + * Initializes extensions for Optimization Detective. + * + * @since n.e.x.t + */ +function od_initialize_extensions(): void { + /** + * Fires when extensions to Optimization Detective can be loaded and initialized. + * + * @since n.e.x.t + * + * @param string $version Optimization Detective version. + */ + do_action( 'od_init', OPTIMIZATION_DETECTIVE_VERSION ); +} + /** * Generates a media query for the provided minimum and maximum viewport widths. * diff --git a/plugins/optimization-detective/hooks.php b/plugins/optimization-detective/hooks.php index 6b6feb924d..c0f94d148c 100644 --- a/plugins/optimization-detective/hooks.php +++ b/plugins/optimization-detective/hooks.php @@ -10,6 +10,7 @@ exit; // Exit if accessed directly. } +add_action( 'init', 'od_initialize_extensions', PHP_INT_MAX ); add_filter( 'template_include', 'od_buffer_output', PHP_INT_MAX ); OD_URL_Metrics_Post_Type::add_hooks(); add_action( 'wp', 'od_maybe_add_template_output_buffer_filter' ); diff --git a/plugins/optimization-detective/load.php b/plugins/optimization-detective/load.php index cf2ace22b4..fbffedf72c 100644 --- a/plugins/optimization-detective/load.php +++ b/plugins/optimization-detective/load.php @@ -43,9 +43,14 @@ static function ( string $global_var_name, string $version, Closure $load ): voi } }; - // Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action is used - // because it is the first action that fires once the theme is loaded. - add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN ); + /* + * Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action could be + * used since it is the first action that fires once the theme is loaded. However, plugins may embed this + * logic inside a module which initializes even later at the init action. The earliest action that this + * plugin has hooks for is the init action at the default priority of 10 (which includes the rest_api_init + * action), so this is why it gets initialized at priority 9. + */ + add_action( 'init', $bootstrap, 9 ); } // Register this copy of the plugin. @@ -67,8 +72,6 @@ static function ( string $global_var_name, string $version, Closure $load ): voi 'optimization_detective_pending_plugin', '0.7.0-alpha', static function ( string $version ): void { - - // Define the constant. if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) { return; } From 26ae3960705fea0d8d5608bc47e78cd0407cbe49 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 13:26:01 -0700 Subject: [PATCH 041/156] Add test for when resizedBoundingClientRect data not available --- ...d-inside-viewport-without-resized-data.php | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport-without-resized-data.php 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 new file mode 100644 index 0000000000..1cd9bb40fb --- /dev/null +++ b/plugins/embed-optimizer/tests/test-cases/single-twitter-embed-inside-viewport-without-resized-data.php @@ -0,0 +1,59 @@ + static function ( Test_Embed_Optimizer_Optimization_Detective $test_case ): void { + $rect = array( + 'width' => 500.1, + 'height' => 500.2, + 'x' => 100.3, + 'y' => 100.4, + 'top' => 0.1, + 'right' => 0.2, + 'bottom' => 0.3, + 'left' => 0.4, + ); + $test_case->populate_url_metrics( + array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]', + 'isLCP' => true, + 'intersectionRatio' => 1, + // Intentionally omitting resizedBoundingClientRect here to test behavior when data isn't supplied. + ), + ) + ); + }, + 'buffer' => ' + + + + ... + + +
+
+ + +
+
+ + + ', + 'expected' => ' + + + + ... + + + + +
+
+ + +
+
+ + + ', +); From cd80ed1102394f3deb4736f4d201e0a59981f0a5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 13:27:30 -0700 Subject: [PATCH 042/156] Remove obsolete short-circuiting now that OD dependency version is checked --- .../embed-optimizer/class-embed-optimizer-tag-visitor.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index e735db488d..fe412e90c1 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -187,11 +187,6 @@ private function get_embed_wrapper_xpath( OD_Tag_Visitor_Context $context ): str * @param OD_Tag_Visitor_Context $context Tag visitor context. */ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { - // TODO: Remove this condition once no longer likely that Optimization Detective v0.6.0 is no longer in the wild. Or should the plugin short-circuit if OPTIMIZATION_DETECTIVE_VERSION is not 0.7.0 (for example) or higher? - if ( ! function_exists( 'od_generate_media_query' ) || ! method_exists( $context->url_metric_group_collection, 'get_all_url_metrics_groups_elements' ) ) { - return; - } - $processor = $context->processor; $embed_wrapper_xpath = $this->get_embed_wrapper_xpath( $context ); From bd008c5b69b2cee382a6532da1432ffb80da2965 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 14:34:25 -0700 Subject: [PATCH 043/156] Evolve get_all_url_metrics_groups_elements into get_all_denormalized_elements --- .../class-embed-optimizer-tag-visitor.php | 26 ++++---- .../class-od-url-metric-group-collection.php | 63 +++++++++++-------- 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index fe412e90c1..8d07f416bd 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -196,18 +196,20 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { * @var array $group_minimum_heights */ $group_minimum_heights = array(); - // TODO: This can be made more efficient if the get_all_url_metrics_groups_elements return value included an elements_by_xpath key. - foreach ( $context->url_metric_group_collection->get_all_url_metrics_groups_elements() as list( $group, $element ) ) { - if ( isset( $element['resizedBoundingClientRect'] ) && $embed_wrapper_xpath === $element['xpath'] ) { - $group_min_width = $group->get_minimum_viewport_width(); - if ( ! isset( $group_minimum_heights[ $group_min_width ] ) ) { - $group_minimum_heights[ $group_min_width ] = array( $group, $element['resizedBoundingClientRect']['height'] ); - } else { - $group_minimum_heights[ $group_min_width ][1] = min( - $group_minimum_heights[ $group_min_width ][1], - $element['resizedBoundingClientRect']['height'] - ); - } + + $denormalized_elements = $context->url_metric_group_collection->get_all_denormalized_elements()[ $embed_wrapper_xpath ] ?? array(); + foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { + if ( ! isset( $element['resizedBoundingClientRect'] ) ) { + continue; + } + $group_min_width = $group->get_minimum_viewport_width(); + if ( ! isset( $group_minimum_heights[ $group_min_width ] ) ) { + $group_minimum_heights[ $group_min_width ] = array( $group, $element['resizedBoundingClientRect']['height'] ); + } else { + $group_minimum_heights[ $group_min_width ][1] = min( + $group_minimum_heights[ $group_min_width ][1], + $element['resizedBoundingClientRect']['height'] + ); } } 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 750d243cb2..9fb4f79a81 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -81,7 +81,8 @@ final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggrega * get_groups_by_lcp_element?: array, * get_common_lcp_element?: ElementData|null, * get_all_element_max_intersection_ratios?: array, - * get_all_element_minimum_heights?: array + * get_all_element_minimum_heights?: array, + * get_all_denormalized_elements?: array>, * } */ private $result_cache = array(); @@ -397,28 +398,37 @@ public function get_common_lcp_element(): ?array { } /** - * Gets all elements from all URL metrics from all groups. + * Gets all elements from all URL metrics from all groups keyed by the elements' XPaths. * * This is an O(n^3) function so its results must be cached. This being said, the number of groups should be 4 (one * more than the default number of breakpoints) and the number of URL metrics for each group should be 3 * (the default sample size). Therefore, given the number (n) of visited elements on the page this will only * end up running n*4*3 times. * - * @todo This should be cached. + * @todo Should there be an OD_Element class which has a $url_metric property which then in turn has a $group property. Then this would only need to return array. * @since n.e.x.t * - * @return array + * @return array> Keys are XPaths and values are arrays of tuples consisting of the group, URL metric, and element data. */ - public function get_all_url_metrics_groups_elements(): array { - $elements_and_groups = array(); - foreach ( $this->groups as $group ) { - foreach ( $group as $url_metric ) { - foreach ( $url_metric->get_elements() as $element ) { - $elements_and_groups[] = array( $group, $element ); + public function get_all_denormalized_elements(): array { + if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) { + return $this->result_cache[ __FUNCTION__ ]; + } + + $result = ( function () { + $all_denormalized_elements = array(); + foreach ( $this->groups as $group ) { + foreach ( $group as $url_metric ) { + foreach ( $url_metric->get_elements() as $element ) { + $all_denormalized_elements[ $element['xpath'] ][] = array( $group, $url_metric, $element ); + } } } - } - return $elements_and_groups; + return $all_denormalized_elements; + } )(); + + $this->result_cache[ __FUNCTION__ ] = $result; + return $result; } /** @@ -432,13 +442,15 @@ public function get_all_element_max_intersection_ratios(): array { } $result = ( function () { - $element_max_intersection_ratios = array(); - foreach ( $this->get_all_url_metrics_groups_elements() as list( $group, $element ) ) { - $element_max_intersection_ratios[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_max_intersection_ratios ) - ? max( $element_max_intersection_ratios[ $element['xpath'] ], $element['intersectionRatio'] ) - : $element['intersectionRatio']; + $elements_max_intersection_ratios = array(); + foreach ( $this->get_all_denormalized_elements() as $xpath => $denormalized_elements ) { + $element_min_heights = array(); + foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { + $element_min_heights[] = $element['intersectionRatio']; + } + $elements_max_intersection_ratios[ $xpath ] = (float) max( ...$element_min_heights ); } - return $element_max_intersection_ratios; + return $elements_max_intersection_ratios; } )(); $this->result_cache[ __FUNCTION__ ] = $result; @@ -458,14 +470,15 @@ public function get_all_element_minimum_heights(): array { } $result = ( function () { - $element_min_heights = array(); - - foreach ( $this->get_all_url_metrics_groups_elements() as list( $group, $element ) ) { - $element_min_heights[ $element['xpath'] ] = array_key_exists( $element['xpath'], $element_min_heights ) - ? min( $element_min_heights[ $element['xpath'] ], $element['boundingClientRect']['height'] ) - : $element['boundingClientRect']['height']; + $elements_min_heights = array(); + foreach ( $this->get_all_denormalized_elements() as $xpath => $denormalized_elements ) { + $element_min_heights = array(); + foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { + $element_min_heights[] = $element['boundingClientRect']['height']; + } + $elements_min_heights[ $xpath ] = (float) min( ...$element_min_heights ); } - return $element_min_heights; + return $elements_min_heights; } )(); $this->result_cache[ __FUNCTION__ ] = $result; From 1b5cf1350e79ebd7839a4cdf655f43cf45cc0552 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 15:08:26 -0700 Subject: [PATCH 044/156] Add Embed Optimizer tests --- plugins/embed-optimizer/hooks.php | 2 +- plugins/embed-optimizer/tests/test-hooks.php | 79 +++++++++++++++++-- .../tests/test-optimization-detective.php | 40 ++++++++++ 3 files changed, 114 insertions(+), 7 deletions(-) diff --git a/plugins/embed-optimizer/hooks.php b/plugins/embed-optimizer/hooks.php index 632dc286b8..9f9624a105 100644 --- a/plugins/embed-optimizer/hooks.php +++ b/plugins/embed-optimizer/hooks.php @@ -43,7 +43,7 @@ function embed_optimizer_add_non_optimization_detective_hooks(): void { */ function embed_optimizer_init_optimization_detective( string $optimization_detective_version ): void { $required_od_version = '0.7.0'; - if ( ! version_compare( (string) strtok( $optimization_detective_version, '-' ), $required_od_version, '>=' ) ) { + if ( version_compare( (string) strtok( $optimization_detective_version, '-' ), $required_od_version, '<' ) ) { add_action( 'admin_notices', static function (): void { diff --git a/plugins/embed-optimizer/tests/test-hooks.php b/plugins/embed-optimizer/tests/test-hooks.php index 9f0f397131..b9b55d8e70 100644 --- a/plugins/embed-optimizer/tests/test-hooks.php +++ b/plugins/embed-optimizer/tests/test-hooks.php @@ -10,14 +10,81 @@ class Test_Embed_Optimizer_Hooks extends WP_UnitTestCase { /** * @covers ::embed_optimizer_add_hooks */ - public function test_hooks(): void { + public function test_embed_optimizer_add_hooks(): void { + remove_all_actions( 'od_init' ); + remove_all_actions( 'wp_head' ); + remove_all_actions( 'wp_loaded' ); embed_optimizer_add_hooks(); - if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) { - $this->assertFalse( has_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_lazy_load' ) ); - } else { - $this->assertSame( 10, has_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_lazy_load' ) ); - } + $this->assertSame( 10, has_action( 'od_init', 'embed_optimizer_init_optimization_detective' ) ); $this->assertSame( 10, has_action( 'wp_head', 'embed_optimizer_render_generator' ) ); + $this->assertSame( 10, has_action( 'wp_loaded', 'embed_optimizer_add_non_optimization_detective_hooks' ) ); + } + + /** + * @return array> + */ + public function data_provider_to_test_embed_optimizer_add_non_optimization_detective_hooks(): array { + return array( + 'without_optimization_detective' => array( + 'set_up' => static function (): void {}, + 'expected' => 10, + ), + 'with_optimization_detective' => array( + 'set_up' => static function (): void { + add_action( 'od_register_tag_visitors', 'embed_optimizer_register_tag_visitors' ); + }, + 'expected' => false, + ), + ); + } + + /** + * @dataProvider data_provider_to_test_embed_optimizer_add_non_optimization_detective_hooks + * @covers ::embed_optimizer_add_non_optimization_detective_hooks + * + * @param Closure $set_up Set up. + * @param int|false $expected Expected. + */ + public function test_embed_optimizer_add_non_optimization_detective_hooks( Closure $set_up, $expected ): void { + remove_all_filters( 'embed_oembed_html' ); + remove_all_actions( 'od_register_tag_visitors' ); + $set_up(); + embed_optimizer_add_non_optimization_detective_hooks(); + $this->assertSame( $expected, has_filter( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_lazy_load' ) ); + } + + /** + * @return array> + */ + public function data_provider_to_test_embed_optimizer_init_optimization_detective(): array { + return array( + 'with_old_version' => array( + 'version' => '0.5.0', + 'expected' => false, + ), + 'with_new_version' => array( + 'version' => '0.7.0', + 'expected' => true, + ), + ); + } + + /** + * @covers ::embed_optimizer_init_optimization_detective + * @dataProvider data_provider_to_test_embed_optimizer_init_optimization_detective + */ + public function test_embed_optimizer_init_optimization_detective( string $version, bool $expected ): void { + remove_all_actions( 'admin_notices' ); + remove_all_actions( 'od_register_tag_visitors' ); + remove_all_filters( 'embed_oembed_html' ); + remove_all_filters( 'od_url_metric_schema_element_item_additional_properties' ); + + embed_optimizer_init_optimization_detective( $version ); + + $this->assertSame( ! $expected, has_action( 'admin_notices' ) ); + $this->assertSame( $expected ? 10 : false, has_action( 'od_register_tag_visitors', 'embed_optimizer_register_tag_visitors' ) ); + $this->assertSame( $expected ? 10 : false, has_action( 'embed_oembed_html', 'embed_optimizer_filter_oembed_html_to_detect_embed_presence' ) ); + $this->assertSame( $expected ? 10 : false, has_filter( 'od_url_metric_schema_element_item_additional_properties', 'embed_optimizer_add_element_item_schema_properties' ) ); } /** diff --git a/plugins/embed-optimizer/tests/test-optimization-detective.php b/plugins/embed-optimizer/tests/test-optimization-detective.php index eed257784c..0355864f66 100644 --- a/plugins/embed-optimizer/tests/test-optimization-detective.php +++ b/plugins/embed-optimizer/tests/test-optimization-detective.php @@ -32,6 +32,46 @@ public function test_embed_optimizer_register_tag_visitors(): void { $this->assertInstanceOf( Embed_Optimizer_Tag_Visitor::class, $registry->get_registered( 'embeds' ) ); } + + /** + * Tests embed_optimizer_add_element_item_schema_properties(). + * + * @covers ::embed_optimizer_add_element_item_schema_properties + */ + public function test_embed_optimizer_add_element_item_schema_properties(): void { + $props = embed_optimizer_add_element_item_schema_properties( array( 'foo' => array() ) ); + $this->assertArrayHasKey( 'foo', $props ); + $this->assertArrayHasKey( 'resizedBoundingClientRect', $props ); + $this->assertArrayHasKey( 'properties', $props['resizedBoundingClientRect'] ); + } + + /** + * Tests embed_optimizer_filter_extension_module_urls(). + * + * @covers ::embed_optimizer_filter_extension_module_urls + */ + public function test_embed_optimizer_filter_extension_module_urls(): void { + $urls = embed_optimizer_filter_extension_module_urls( null ); + $this->assertCount( 1, $urls ); + $this->assertStringContainsString( 'detect', $urls[0] ); + + $urls = embed_optimizer_filter_extension_module_urls( array( 'foo.js' ) ); + $this->assertCount( 2, $urls ); + $this->assertStringContainsString( 'foo.js', $urls[0] ); + $this->assertStringContainsString( 'detect', $urls[1] ); + } + + /** + * Tests embed_optimizer_filter_oembed_html_to_detect_embed_presence(). + * + * @covers ::embed_optimizer_filter_oembed_html_to_detect_embed_presence + */ + public function test_embed_optimizer_filter_oembed_html_to_detect_embed_presence(): void { + $this->assertFalse( has_filter( 'od_extension_module_urls', 'embed_optimizer_filter_extension_module_urls' ) ); + $this->assertSame( '...', embed_optimizer_filter_oembed_html_to_detect_embed_presence( '...' ) ); + $this->assertSame( 10, has_filter( 'od_extension_module_urls', 'embed_optimizer_filter_extension_module_urls' ) ); + } + /** * Data provider. * From a70df2868177d0b4d28e439fc3c3f5c89f41c3a3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 15:53:10 -0700 Subject: [PATCH 045/156] Account for error when passing single-item array to min() or max() --- .../class-od-url-metric-group-collection.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) 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 9fb4f79a81..7c4d042d14 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -82,7 +82,7 @@ final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggrega * get_common_lcp_element?: ElementData|null, * get_all_element_max_intersection_ratios?: array, * get_all_element_minimum_heights?: array, - * get_all_denormalized_elements?: array>, + * get_all_denormalized_elements?: array>, * } */ private $result_cache = array(); @@ -408,7 +408,7 @@ public function get_common_lcp_element(): ?array { * @todo Should there be an OD_Element class which has a $url_metric property which then in turn has a $group property. Then this would only need to return array. * @since n.e.x.t * - * @return array> Keys are XPaths and values are arrays of tuples consisting of the group, URL metric, and element data. + * @return array> Keys are XPaths and values are arrays of tuples consisting of the group, URL metric, and element data. */ public function get_all_denormalized_elements(): array { if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) { @@ -444,11 +444,13 @@ public function get_all_element_max_intersection_ratios(): array { $result = ( function () { $elements_max_intersection_ratios = array(); foreach ( $this->get_all_denormalized_elements() as $xpath => $denormalized_elements ) { - $element_min_heights = array(); + $element_intersection_ratios = array(); foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { - $element_min_heights[] = $element['intersectionRatio']; + $element_intersection_ratios[] = $element['intersectionRatio']; } - $elements_max_intersection_ratios[ $xpath ] = (float) max( ...$element_min_heights ); + $elements_max_intersection_ratios[ $xpath ] = count( $element_intersection_ratios ) > 1 + ? (float) max( ...$element_intersection_ratios ) + : $element_intersection_ratios[0]; } return $elements_max_intersection_ratios; } )(); @@ -476,7 +478,9 @@ public function get_all_element_minimum_heights(): array { foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { $element_min_heights[] = $element['boundingClientRect']['height']; } - $elements_min_heights[ $xpath ] = (float) min( ...$element_min_heights ); + $elements_min_heights[ $xpath ] = count( $element_min_heights ) > 1 + ? (float) min( ...$element_min_heights ) + : $element_min_heights[0]; } return $elements_min_heights; } )(); From 4e48d3d94edac1087d4f40e50f982ee6ffee296c Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 15:57:57 -0700 Subject: [PATCH 046/156] Add test for Image Prioritizer --- .../image-prioritizer/tests/test-helper.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/plugins/image-prioritizer/tests/test-helper.php b/plugins/image-prioritizer/tests/test-helper.php index 708c062a53..c58bdaff07 100644 --- a/plugins/image-prioritizer/tests/test-helper.php +++ b/plugins/image-prioritizer/tests/test-helper.php @@ -10,6 +10,38 @@ class Test_Image_Prioritizer_Helper extends WP_UnitTestCase { use Optimization_Detective_Test_Helpers; + /** + * @return array> + */ + public function data_provider_to_test_image_prioritizer_init(): array { + return array( + 'with_old_version' => array( + 'version' => '0.5.0', + 'expected' => false, + ), + 'with_new_version' => array( + 'version' => '0.7.0', + 'expected' => true, + ), + ); + } + + /** + * @covers ::image_prioritizer_init + * @dataProvider data_provider_to_test_image_prioritizer_init + */ + public function test_image_prioritizer_init( string $version, bool $expected ): void { + remove_all_actions( 'admin_notices' ); + remove_all_actions( 'wp_head' ); + remove_all_actions( 'od_register_tag_visitors' ); + + image_prioritizer_init( $version ); + + $this->assertSame( ! $expected, has_action( 'admin_notices' ) ); + $this->assertSame( $expected ? 10 : false, has_action( 'wp_head', 'image_prioritizer_render_generator_meta_tag' ) ); + $this->assertSame( $expected ? 10 : false, has_action( 'od_register_tag_visitors', 'image_prioritizer_register_tag_visitors' ) ); + } + /** * Test printing the meta generator tag. * From d17cacea56114ef5053711f9fc04c893a2eaffee Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 16:00:13 -0700 Subject: [PATCH 047/156] Remove now-unused method to get element minimum hights --- .../class-od-url-metric-group-collection.php | 43 ------------------- ...-class-od-url-metrics-group-collection.php | 23 ---------- 2 files changed, 66 deletions(-) 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 7c4d042d14..aec416e441 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -81,7 +81,6 @@ final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggrega * get_groups_by_lcp_element?: array, * get_common_lcp_element?: ElementData|null, * get_all_element_max_intersection_ratios?: array, - * get_all_element_minimum_heights?: array, * get_all_denormalized_elements?: array>, * } */ @@ -459,36 +458,6 @@ public function get_all_element_max_intersection_ratios(): array { return $result; } - /** - * Gets the minimum heights of all elements across all groups and their captured URL metrics. - * - * @since n.e.x.t - * - * @return array Keys are XPaths and values are the minimum heights. - */ - public function get_all_element_minimum_heights(): array { - if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) { - return $this->result_cache[ __FUNCTION__ ]; - } - - $result = ( function () { - $elements_min_heights = array(); - foreach ( $this->get_all_denormalized_elements() as $xpath => $denormalized_elements ) { - $element_min_heights = array(); - foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { - $element_min_heights[] = $element['boundingClientRect']['height']; - } - $elements_min_heights[ $xpath ] = count( $element_min_heights ) > 1 - ? (float) min( ...$element_min_heights ) - : $element_min_heights[0]; - } - return $elements_min_heights; - } )(); - - $this->result_cache[ __FUNCTION__ ] = $result; - return $result; - } - /** * Gets the max intersection ratio of an element across all groups and their captured URL metrics. * @@ -499,18 +468,6 @@ public function get_element_max_intersection_ratio( string $xpath ): ?float { return $this->get_all_element_max_intersection_ratios()[ $xpath ] ?? null; } - /** - * Gets the minimum height of an element across all groups and their captured URL metrics. - * - * @since n.e.x.t - * - * @param string $xpath XPath for the element. - * @return float Minimum height in pixels. - */ - public function get_element_minimum_height( string $xpath ): ?float { - return $this->get_all_element_minimum_heights()[ $xpath ] ?? null; - } - /** * Gets URL metrics from all groups flattened into one list. * 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 8c2bac8df4..c405a590c9 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 @@ -788,29 +788,6 @@ public function data_provider_element_minimum_heights(): array { ); } - /** - * Test get_all_element_max_intersection_ratios() and get_element_max_intersection_ratio(). - * - * @covers ::get_all_element_minimum_heights - * @covers ::get_element_minimum_height - * - * @dataProvider data_provider_element_minimum_heights - * - * @param array $url_metrics URL metrics. - * @param array $expected Expected. - */ - public function test_get_all_element_minimum_heights( array $url_metrics, array $expected ): void { - $breakpoints = array( 480, 600, 782 ); - $sample_size = 3; - $group_collection = new OD_URL_Metric_Group_Collection( $url_metrics, $breakpoints, $sample_size, 0 ); - $actual = $group_collection->get_all_element_minimum_heights(); - $this->assertSame( $actual, $group_collection->get_all_element_minimum_heights(), 'Cached result is identical.' ); - $this->assertSame( $expected, $actual ); - foreach ( $expected as $expected_xpath => $expected_max_ratio ) { - $this->assertSame( $expected_max_ratio, $group_collection->get_element_minimum_height( $expected_xpath ) ); - } - } - /** * Test get_flattened_url_metrics(). * From 55740818d1735e369a4962de38f6377611f1c65f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 16:50:23 -0700 Subject: [PATCH 048/156] Improve handling of get_updated_html --- .../class-od-html-tag-processor.php | 35 +++++++++++++++---- .../optimization-detective/optimization.php | 2 +- .../test-class-od-html-tag-processor.php | 21 ++++++++--- 3 files changed, 47 insertions(+), 11 deletions(-) diff --git a/plugins/optimization-detective/class-od-html-tag-processor.php b/plugins/optimization-detective/class-od-html-tag-processor.php index b3289cf100..c15c3fba0f 100644 --- a/plugins/optimization-detective/class-od-html-tag-processor.php +++ b/plugins/optimization-detective/class-od-html-tag-processor.php @@ -178,6 +178,15 @@ final class OD_HTML_Tag_Processor extends WP_HTML_Tag_Processor { */ private $buffered_text_replacements = array(); + /** + * Whether the end of the document was reached. + * + * @since n.e.x.t + * @see self::next_token() + * @var bool + */ + private $reached_end_of_document = false; + /** * Count for the number of times that the cursor was moved. * @@ -263,6 +272,9 @@ public function next_token(): bool { if ( ! parent::next_token() ) { $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. + $this->reached_end_of_document = true; return false; } @@ -557,16 +569,27 @@ public function append_body_html( string $html ): void { } /** - * Gets the final updated HTML. + * Returns the string representation of the HTML Tag Processor. * - * This should only be called after the closing HTML tag has been reached and just before - * calling {@see WP_HTML_Tag_Processor::get_updated_html()} to send the document back in the response. + * This can only be called once the end of the document has been reached. It is responsible for adding the pending + * markup to append to the HEAD and the BODY. Originally this was done in an overridden get_updated_html() method + * before calling the parent method. However, every time that seek() is called it the HTML Processor will flush any + * pending updates to the document. This means that if there is any pending markup to append to the end of the BODY + * then the insertion will fail because the closing tag for the BODY has not been encountered yet. Additionally, by + * not processing the buffered text replacements in get_updated_html() then we avoid trying to insert them every + * time that seek() is called which is wasteful as they are only needed once finishing iterating over the document. * - * @since n.e.x.t + * @since 0.4.0 + * @see WP_HTML_Tag_Processor::get_updated_html() + * @see WP_HTML_Tag_Processor::seek() * - * @return string Final updated HTML. + * @return string The processed HTML. */ - public function get_final_updated_html(): string { + public function get_updated_html(): string { + if ( ! $this->reached_end_of_document ) { + return parent::get_updated_html(); + } + foreach ( array_keys( $this->buffered_text_replacements ) as $bookmark ) { $html_strings = $this->buffered_text_replacements[ $bookmark ]; if ( count( $html_strings ) === 0 ) { diff --git a/plugins/optimization-detective/optimization.php b/plugins/optimization-detective/optimization.php index 22c58e7277..ffb97c8a41 100644 --- a/plugins/optimization-detective/optimization.php +++ b/plugins/optimization-detective/optimization.php @@ -250,5 +250,5 @@ function od_optimize_template_output_buffer( string $buffer ): string { $processor->append_body_html( od_get_detection_script( $slug, $group_collection ) ); } - return $processor->get_final_updated_html(); + return $processor->get_updated_html(); } diff --git a/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php b/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php index 59f788e655..1f8870a31a 100644 --- a/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php +++ b/plugins/optimization-detective/tests/test-class-od-html-tag-processor.php @@ -343,7 +343,7 @@ public function test_next_tag_with_query(): void { * * @covers ::append_head_html * @covers ::append_body_html - * @covers ::get_final_updated_html + * @covers ::get_updated_html */ public function test_append_head_and_body_html(): void { $html = ' @@ -370,16 +370,29 @@ public function test_append_head_and_body_html(): void { $saw_head = false; $saw_body = false; + $did_seek = false; while ( $processor->next_open_tag() ) { + $this->assertStringNotContainsString( $head_injected, $processor->get_updated_html(), 'Only expecting end-of-head injection once document was finalized.' ); + $this->assertStringNotContainsString( $body_injected, $processor->get_updated_html(), 'Only expecting end-of-body injection once document was finalized.' ); $tag = $processor->get_tag(); if ( 'HEAD' === $tag ) { $saw_head = true; } elseif ( 'BODY' === $tag ) { $saw_body = true; + $this->assertTrue( $processor->set_bookmark( 'cuerpo' ) ); + } + if ( ! $did_seek && 'H1' === $tag ) { + $processor->append_head_html( '' ); + $processor->append_body_html( '' ); + $this->assertTrue( $processor->seek( 'cuerpo' ) ); + $did_seek = true; } } + $this->assertTrue( $did_seek ); $this->assertTrue( $saw_head ); $this->assertTrue( $saw_body ); + $this->assertStringContainsString( $head_injected, $processor->get_updated_html(), 'Only expecting end-of-head injection once document was finalized.' ); + $this->assertStringContainsString( $body_injected, $processor->get_updated_html(), 'Only expecting end-of-body injection once document was finalized.' ); $processor->append_head_html( $later_head_injected ); @@ -388,16 +401,16 @@ public function test_append_head_and_body_html(): void { - {$head_injected}{$later_head_injected} + {$head_injected}{$later_head_injected}

Hello World

- {$body_injected} + {$body_injected} "; - $this->assertSame( $expected, $processor->get_final_updated_html() ); + $this->assertSame( $expected, $processor->get_updated_html() ); } /** From 19c04256bee58e4642493146529900404b9f4b1d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 17:07:24 -0700 Subject: [PATCH 049/156] Add test for get_all_denormalized_elements --- ...-class-od-url-metrics-group-collection.php | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) 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 c405a590c9..de9518973d 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 @@ -698,10 +698,11 @@ public function data_provider_element_max_intersection_ratios(): array { } /** - * Test get_all_element_max_intersection_ratios() and get_element_max_intersection_ratio(). + * Test get_all_element_max_intersection_ratios(), get_element_max_intersection_ratio(), and get_all_denormalized_elements(). * * @covers ::get_all_element_max_intersection_ratios * @covers ::get_element_max_intersection_ratio + * @covers ::get_all_denormalized_elements * * @dataProvider data_provider_element_max_intersection_ratios * @@ -718,6 +719,29 @@ public function test_get_all_element_max_intersection_ratios( array $url_metrics foreach ( $expected as $expected_xpath => $expected_max_ratio ) { $this->assertSame( $expected_max_ratio, $group_collection->get_element_max_intersection_ratio( $expected_xpath ) ); } + + // Check get_all_denormalized_elements. + $all_denormalized_elements = $group_collection->get_all_denormalized_elements(); + $xpath_counts = array(); + foreach ( $url_metrics as $url_metric ) { + foreach ( $url_metric->get_elements() as $element ) { + if ( ! isset( $xpath_counts[ $element['xpath'] ] ) ) { + $xpath_counts[ $element['xpath'] ] = 0; + } + $xpath_counts[ $element['xpath'] ] += 1; + } + } + $this->assertCount( count( $xpath_counts ), $all_denormalized_elements ); + foreach ( $all_denormalized_elements as $xpath => $denormalized_elements ) { + foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { + $this->assertContains( $url_metric, iterator_to_array( $group ) ); + $this->assertContains( $element, $url_metric->get_elements() ); + $this->assertInstanceOf( OD_URL_Metric_Group::class, $group ); + $this->assertInstanceOf( OD_URL_Metric::class, $url_metric ); + $this->assertIsArray( $element ); + $this->assertSame( $xpath, $element['xpath'] ); + } + } } /** From ea36bac57d4484ac2281b18544c6f1318c0ae753 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 17:17:48 -0700 Subject: [PATCH 050/156] Add tests for new OD code --- .../tests/test-detection.php | 9 +++++++++ .../tests/test-helper.php | 17 +++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/plugins/optimization-detective/tests/test-detection.php b/plugins/optimization-detective/tests/test-detection.php index 4439041a72..887a10b080 100644 --- a/plugins/optimization-detective/tests/test-detection.php +++ b/plugins/optimization-detective/tests/test-detection.php @@ -19,6 +19,7 @@ public function data_provider_od_get_detection_script(): array { 'expected_exports' => array( 'detectionTimeWindow' => 5000, 'storageLockTTL' => MINUTE_IN_SECONDS, + 'extensionModuleUrls' => array(), ), ), 'filtered' => array( @@ -35,10 +36,18 @@ static function (): int { return HOUR_IN_SECONDS; } ); + add_filter( + 'od_extension_module_urls', + static function ( array $urls ): array { + $urls[] = home_url( '/my-extension.js', 'https' ); + return $urls; + } + ); }, 'expected_exports' => array( 'detectionTimeWindow' => 2500, 'storageLockTTL' => HOUR_IN_SECONDS, + 'extensionModuleUrls' => array( home_url( '/my-extension.js', 'https' ) ), ), ), ); diff --git a/plugins/optimization-detective/tests/test-helper.php b/plugins/optimization-detective/tests/test-helper.php index 77000f25de..c28dfdd306 100644 --- a/plugins/optimization-detective/tests/test-helper.php +++ b/plugins/optimization-detective/tests/test-helper.php @@ -7,6 +7,23 @@ class Test_OD_Helper extends WP_UnitTestCase { + /** + * @covers ::od_initialize_extensions + */ + public function test_od_initialize_extensions(): void { + unset( $GLOBALS['wp_actions']['od_init'] ); + $passed_version = null; + add_action( + 'od_init', + static function ( string $version ) use ( &$passed_version ): void { + $passed_version = $version; + } + ); + od_initialize_extensions(); + $this->assertSame( 1, did_action( 'od_init' ) ); + $this->assertSame( OPTIMIZATION_DETECTIVE_VERSION, $passed_version ); + } + /** * @return array> */ From 01c083d5c17f2aabaac38b69373fa89fa382489e Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 17:48:48 -0700 Subject: [PATCH 051/156] Clarify purpose of overridden get_updated_html method --- .../class-od-html-tag-processor.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/plugins/optimization-detective/class-od-html-tag-processor.php b/plugins/optimization-detective/class-od-html-tag-processor.php index c15c3fba0f..a1673fd5da 100644 --- a/plugins/optimization-detective/class-od-html-tag-processor.php +++ b/plugins/optimization-detective/class-od-html-tag-processor.php @@ -571,13 +571,13 @@ public function append_body_html( string $html ): void { /** * Returns the string representation of the HTML Tag Processor. * - * This can only be called once the end of the document has been reached. It is responsible for adding the pending - * markup to append to the HEAD and the BODY. Originally this was done in an overridden get_updated_html() method - * before calling the parent method. However, every time that seek() is called it the HTML Processor will flush any - * pending updates to the document. This means that if there is any pending markup to append to the end of the BODY - * then the insertion will fail because the closing tag for the BODY has not been encountered yet. Additionally, by - * not processing the buffered text replacements in get_updated_html() then we avoid trying to insert them every - * time that seek() is called which is wasteful as they are only needed once finishing iterating over the document. + * Once the end of the document has been reached this is responsible for adding the pending markup to append to the + * HEAD and the BODY. It waits to do this injection until the end of the document has been reached because every + * time that seek() is called it the HTML Processor will flush any pending updates to the document. This means that + * if there is any pending markup to append to the end of the BODY then the insertion will fail because the closing + * tag for the BODY has not been encountered yet. Additionally, by not prematurely processing the buffered text + * replacements in get_updated_html() then we avoid trying to insert them every time that seek() is called which is + * wasteful as they are only needed once finishing iterating over the document. * * @since 0.4.0 * @see WP_HTML_Tag_Processor::get_updated_html() From c5d6991e0f52094db0f5c121dc987fb7d09918c2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 8 Oct 2024 18:00:06 -0700 Subject: [PATCH 052/156] Add missing since tags --- .../class-od-url-metric-group-collection.php | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) 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 aec416e441..92d9b8ca22 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -160,6 +160,8 @@ public function __construct( array $url_metrics, array $breakpoints, int $sample /** * Clear result cache. + * + * @since 0.3.0 */ public function clear_cache(): void { $this->result_cache = array(); @@ -168,6 +170,8 @@ public function clear_cache(): void { /** * Create groups. * + * @since 0.1.0 + * * @phpstan-return non-empty-array * * @return OD_URL_Metric_Group[] Groups. @@ -188,6 +192,7 @@ private function create_groups(): array { * * Once a group reaches the sample size, the oldest URL metric is pushed out. * + * @since 0.1.0 * @throws InvalidArgumentException If there is no group available to add a URL metric to. * * @param OD_URL_Metric $new_url_metric New URL metric. @@ -207,6 +212,7 @@ public function add_url_metric( OD_URL_Metric $new_url_metric ): void { /** * Gets group for viewport width. * + * @since 0.1.0 * @throws InvalidArgumentException When there is no group for the provided viewport width. This would only happen if a negative width is provided. * * @param int $viewport_width Viewport width. @@ -241,6 +247,8 @@ public function get_group_for_viewport_width( int $viewport_width ): OD_URL_Metr /** * Checks whether any group is populated with at least one URL metric. * + * @since 0.5.0 + * * @return bool Whether at least one group has some URL metrics. */ public function is_any_group_populated(): bool { @@ -269,6 +277,7 @@ public function is_any_group_populated(): bool { * should be contrasted with the `is_every_group_complete()` * method below. * + * @since 0.1.0 * @see OD_URL_Metric_Group_Collection::is_every_group_complete() * * @return bool Whether all groups have some URL metrics. @@ -294,6 +303,7 @@ public function is_every_group_populated(): bool { /** * Checks whether every group is complete. * + * @since 0.1.0 * @see OD_URL_Metric_Group::is_complete() * * @return bool Whether all groups are complete. @@ -320,6 +330,7 @@ public function is_every_group_complete(): bool { /** * Gets the groups with the provided LCP element XPath. * + * @since 0.3.0 * @see OD_URL_Metric_Group::get_lcp_element() * * @param string $xpath XPath for LCP element. @@ -349,6 +360,8 @@ public function get_groups_by_lcp_element( string $xpath ): array { /** * Gets common LCP element. * + * @since 0.3.0 + * * @return ElementData|null */ public function get_common_lcp_element(): ?array { @@ -433,6 +446,8 @@ public function get_all_denormalized_elements(): array { /** * Gets the max intersection ratios of all elements across all groups and their captured URL metrics. * + * @since 0.3.0 + * * @return array Keys are XPaths and values are the intersection ratios. */ public function get_all_element_max_intersection_ratios(): array { @@ -461,6 +476,8 @@ public function get_all_element_max_intersection_ratios(): array { /** * Gets the max intersection ratio of an element across all groups and their captured URL metrics. * + * @since 0.3.0 + * * @param string $xpath XPath for the element. * @return float|null Max intersection ratio of null if tag is unknown (not captured). */ @@ -471,6 +488,8 @@ public function get_element_max_intersection_ratio( string $xpath ): ?float { /** * Gets URL metrics from all groups flattened into one list. * + * @since 0.1.0 + * * @return OD_URL_Metric[] All URL metrics. */ public function get_flattened_url_metrics(): array { @@ -488,6 +507,8 @@ public function get_flattened_url_metrics(): array { /** * Returns an iterator for the groups of URL metrics. * + * @since 0.1.0 + * * @return ArrayIterator Array iterator for OD_URL_Metric_Group instances. */ public function getIterator(): ArrayIterator { @@ -497,6 +518,8 @@ public function getIterator(): ArrayIterator { /** * Counts the URL metric groups in the collection. * + * @since 0.1.0 + * * @return int<0, max> Group count. */ public function count(): int { From 455ef4fe611ae50f2aba9427b7494f6f73d92eae Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 9 Oct 2024 12:55:14 -0700 Subject: [PATCH 053/156] Clarify handling of embed block tags and embed wrapper tags --- .../class-embed-optimizer-tag-visitor.php | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index 8d07f416bd..70d48631ce 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -43,7 +43,7 @@ private function is_embed_figure( OD_HTML_Tag_Processor $processor ): bool { } /** - * Determines whether the processor is currently at a div.wp-block-embed__wrapper tag. + * Determines whether the processor is currently at a div.wp-block-embed__wrapper tag (which is a child of figure.wp-block-embed). * * @since n.e.x.t * @@ -61,6 +61,23 @@ private function is_embed_wrapper( OD_HTML_Tag_Processor $processor ): bool { /** * Visits a tag. * + * This visitor has two entry points, the `figure.wp-block-embed` tag and its child the `div.wp-block-embed__wrapper` + * tag. For example: + * + *
+ *
+ * + * + *
+ *
+ * + * For the `div.wp-block-embed__wrapper` tag, the only thing this tag visitor does is flag it for tracking in URL + * Metrics (by returning true). When visiting the parent `figure.wp-block-embed` tag, it does all the actual + * processing. In particular, it will use the element metrics gathered for the child `div.wp-block-embed__wrapper` + * element to set the min-height style on the `figure.wp-block-embed` to avoid layout shifts. Additionally, when + * the embed is in the initial viewport for any breakpoint, it will add preconnect links for key resources. + * Otherwise, if the embed is not in any initial viewport, it will add lazy-loading logic. + * * @since 0.2.0 * * @param OD_Tag_Visitor_Context $context Tag visitor context. @@ -84,7 +101,7 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $this->reduce_layout_shifts( $context ); - $max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( $this->get_embed_wrapper_xpath( $context ) ); + $max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( self::get_embed_wrapper_xpath( $processor->get_xpath() ) ); if ( $max_intersection_ratio > 0 ) { /* * The following embeds have been chosen for optimization due to their relative popularity among all embed types. @@ -168,15 +185,15 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { } /** - * Gets the XPath for the embed wrapper. + * Gets the XPath for the embed wrapper DIV which is the sole child of the embed block FIGURE. * * @since n.e.x.t * - * @param OD_Tag_Visitor_Context $context Tag visitor context. - * @return string XPath. + * @param string $embed_block_xpath XPath for the embed block FIGURE tag. For example: `/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]`. + * @return string XPath for the child DIV. For example: `/*[1][self::HTML]/*[2][self::BODY]/*[1][self::FIGURE]/*[1][self::DIV]` */ - private function get_embed_wrapper_xpath( OD_Tag_Visitor_Context $context ): string { - return $context->processor->get_xpath() . '/*[1][self::DIV]'; + private static function get_embed_wrapper_xpath( string $embed_block_xpath ): string { + return $embed_block_xpath . '/*[1][self::DIV]'; } /** @@ -184,11 +201,11 @@ private function get_embed_wrapper_xpath( OD_Tag_Visitor_Context $context ): str * * @since n.e.x.t * - * @param OD_Tag_Visitor_Context $context Tag visitor context. + * @param OD_Tag_Visitor_Context $context Tag visitor context, with the cursor currently at an embed block. */ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { $processor = $context->processor; - $embed_wrapper_xpath = $this->get_embed_wrapper_xpath( $context ); + $embed_wrapper_xpath = self::get_embed_wrapper_xpath( $processor->get_xpath() ); /** * Array of tuples of groups and their minimum heights keyed by the minimum viewport width. From a390e15e919b267ee8fcc963351a2ac62dabd2b3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 9 Oct 2024 13:01:49 -0700 Subject: [PATCH 054/156] Replace tuple with assoc array Co-authored-by: felixarntz --- .../class-embed-optimizer-tag-visitor.php | 25 +++++++++++-------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index 70d48631ce..1824ab4a86 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -208,11 +208,11 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { $embed_wrapper_xpath = self::get_embed_wrapper_xpath( $processor->get_xpath() ); /** - * Array of tuples of groups and their minimum heights keyed by the minimum viewport width. + * Collection of the minimum heights for the element with each group keyed by the minimum viewport with. * - * @var array $group_minimum_heights + * @var array $minimums */ - $group_minimum_heights = array(); + $minimums = array(); $denormalized_elements = $context->url_metric_group_collection->get_all_denormalized_elements()[ $embed_wrapper_xpath ] ?? array(); foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { @@ -220,18 +220,21 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { continue; } $group_min_width = $group->get_minimum_viewport_width(); - if ( ! isset( $group_minimum_heights[ $group_min_width ] ) ) { - $group_minimum_heights[ $group_min_width ] = array( $group, $element['resizedBoundingClientRect']['height'] ); + if ( ! isset( $minimums[ $group_min_width ] ) ) { + $minimums[ $group_min_width ] = array( + 'group' => $group, + 'height' => $element['resizedBoundingClientRect']['height'], + ); } else { - $group_minimum_heights[ $group_min_width ][1] = min( - $group_minimum_heights[ $group_min_width ][1], + $minimums[ $group_min_width ]['height'] = min( + $minimums[ $group_min_width ]['height'], $element['resizedBoundingClientRect']['height'] ); } } // Add style rules to set the min-height for each viewport group. - if ( count( $group_minimum_heights ) > 0 ) { + if ( count( $minimums ) > 0 ) { $element_id = $processor->get_attribute( 'id' ); if ( ! is_string( $element_id ) ) { $element_id = 'embed-optimizer-' . md5( $processor->get_xpath() ); @@ -239,12 +242,12 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { } $style_rules = array(); - foreach ( $group_minimum_heights as list( $group, $minimum_height ) ) { + foreach ( $minimums as $minimum ) { $style_rules[] = sprintf( '@media %s { #%s { min-height: %dpx; } }', - od_generate_media_query( $group->get_minimum_viewport_width(), $group->get_maximum_viewport_width() ), + od_generate_media_query( $minimum['group']->get_minimum_viewport_width(), $minimum['group']->get_maximum_viewport_width() ), $element_id, - $minimum_height + $minimum['height'] ); } From a760705fc1b908a7b5db95350ddbcd6a999f895b Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Wed, 9 Oct 2024 13:06:27 -0700 Subject: [PATCH 055/156] Add doc block for detect.js --- plugins/embed-optimizer/detect.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index b7d258ecbf..83f0477666 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -1,3 +1,11 @@ +/** + * Embed Optimizer module for Optimization Detective + * + * When a URL metric is being collected by Optimization Detective, this module adds a ResizeObserver to keep track of + * the changed heights for embed blocks. This data is amended onto the element data of the pending URL metric when it + * is submitted for storage. + */ + const consoleLogPrefix = '[Embed Optimizer]'; /** From 7ca1fbcbcd3944d0585b24fcd44c863699593d40 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 10 Oct 2024 17:59:43 -0700 Subject: [PATCH 056/156] Add API functions to pass to finalize callbacks to avoid direct mutation of URL Metric object --- plugins/embed-optimizer/detect.js | 49 +++++++---- plugins/optimization-detective/detect.js | 100 +++++++++++++++++++--- plugins/optimization-detective/types.d.ts | 24 +++++- 3 files changed, 142 insertions(+), 31 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index 83f0477666..e91e98fa67 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -9,8 +9,13 @@ const consoleLogPrefix = '[Embed Optimizer]'; /** - * @typedef {import("../optimization-detective/types.d.ts").ElementMetrics} ElementMetrics + * @typedef {import("../optimization-detective/types.d.ts").ElementData} ElementMetrics * @typedef {import("../optimization-detective/types.d.ts").URLMetric} URLMetric + * @typedef {import("../optimization-detective/types.d.ts").Extension} Extension + * @typedef {import("../optimization-detective/types.d.ts").InitializeCallback} InitializeCallback + * @typedef {import("../optimization-detective/types.d.ts").InitializeArgs} InitializeArgs + * @typedef {import("../optimization-detective/types.d.ts").FinalizeArgs} FinalizeArgs + * @typedef {import("../optimization-detective/types.d.ts").FinalizeCallback} FinalizeCallback */ /** @@ -33,8 +38,8 @@ const loadedElementContentRects = new Map(); /** * Initialize. * - * @param {Object} args Args. - * @param {boolean} args.isDebug Whether to show debug messages. + * @type {InitializeCallback} + * @param {InitializeArgs} args Args. */ export async function initialize( { isDebug } ) { const embedWrappers = @@ -43,7 +48,7 @@ export async function initialize( { isDebug } ) { ); for ( const embedWrapper of embedWrappers ) { - monitorEmbedWrapperForResizes( embedWrapper ); + monitorEmbedWrapperForResizes( embedWrapper, isDebug ); } if ( isDebug ) { @@ -54,28 +59,34 @@ export async function initialize( { isDebug } ) { /** * Finalize. * - * @param {Object} args Args. - * @param {boolean} args.isDebug Whether to show debug messages. - * @param {URLMetric} args.urlMetric Pending URL metric. + * @type {FinalizeCallback} + * @param {FinalizeArgs} args Args. */ -export async function finalize( { urlMetric, isDebug } ) { +export async function finalize( { + urlMetric, + isDebug, + getElementData, + amendElementData, +} ) { if ( isDebug ) { log( 'URL metric to be sent:', urlMetric ); } - for ( const element of urlMetric.elements ) { - if ( loadedElementContentRects.has( element.xpath ) ) { + for ( const [ xpath, domRect ] of loadedElementContentRects.entries() ) { + if ( + amendElementData( xpath, { resizedBoundingClientRect: domRect } ) + ) { + const elementData = getElementData( xpath ); if ( isDebug ) { log( - `boundingClientRect for ${ element.xpath } resized:`, - element.boundingClientRect, + `boundingClientRect for ${ xpath } resized:`, + elementData.boundingClientRect, '=>', - loadedElementContentRects.get( element.xpath ) + elementData.resizedBoundingClientRect ); } - element.resizedBoundingClientRect = loadedElementContentRects.get( - element.xpath - ); + } else if ( isDebug ) { + log( `Unable to amend element data for ${ xpath }` ); } } } @@ -84,8 +95,9 @@ export async function finalize( { urlMetric, isDebug } ) { * Monitors embed wrapper for resizes. * * @param {HTMLDivElement} embedWrapper Embed wrapper DIV. + * @param {boolean} isDebug Whether debug. */ -function monitorEmbedWrapperForResizes( embedWrapper ) { +function monitorEmbedWrapperForResizes( embedWrapper, isDebug ) { if ( ! ( 'odXpath' in embedWrapper.dataset ) ) { throw new Error( 'Embed wrapper missing data-od-xpath attribute.' ); } @@ -93,6 +105,9 @@ function monitorEmbedWrapperForResizes( embedWrapper ) { const observer = new ResizeObserver( ( entries ) => { const [ entry ] = entries; loadedElementContentRects.set( xpath, entry.contentRect ); + if ( isDebug ) { + log( `Resized element ${ xpath }:`, entry.contentRect ); + } } ); observer.observe( embedWrapper, { box: 'content-box' } ); } diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index cac6480e8e..77c493532d 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -1,6 +1,6 @@ /** * @typedef {import("web-vitals").LCPMetric} LCPMetric - * @typedef {import("types.d.ts").ElementMetrics} ElementMetrics + * @typedef {import("types.d.ts").ElementData} ElementData * @typedef {import("types.d.ts").URLMetric} URLMetric * @typedef {import("types.d.ts").URLMetricGroupStatus} URLMetricGroupStatus * @typedef {import("types.d.ts").Extension} Extension @@ -110,6 +110,60 @@ function getCurrentTime() { return Date.now(); } +/** + * Recursively freezes an object to prevent mutation. + * + * @param {Object} obj Object to recursively freeze. + */ +function recursiveFreeze( obj ) { + for ( const prop of Object.getOwnPropertyNames( obj ) ) { + const value = obj[ prop ]; + if ( null !== value && typeof value === 'object' ) { + recursiveFreeze( value ); + } + } + Object.freeze( obj ); +} + +/** + * Mapping of XPath to element data. + * + * @type {Map} + */ +const elementsByXPath = new Map(); + +/** + * Gets element data. + * + * @param {string} xpath XPath. + * @return {ElementData|null} Element data, or null if no element for the XPath exists. + */ +function getElementData( xpath ) { + const elementData = elementsByXPath.get( xpath ); + if ( elementData ) { + const cloned = structuredClone( elementData ); + recursiveFreeze( cloned ); + return cloned; + } + return null; +} + +/** + * Amends element data. + * + * @param {string} xpath XPath. + * @param {Object} properties Properties. + * @return {boolean} Whether appending element data was successful. + */ +function amendElementData( xpath, properties ) { + if ( ! elementsByXPath.has( xpath ) ) { + return false; + } + const elementData = elementsByXPath.get( xpath ); + Object.assign( elementData, properties ); // TODO: Check if any core properties are being used. + return true; +} + /** * Detects the LCP element, loaded images, client viewport and store for future optimizations. * @@ -354,8 +408,8 @@ export default async function detect( { const isLCP = elementIntersection.target === lcpMetric?.entries[ 0 ]?.element; - /** @type {ElementMetrics} */ - const elementMetrics = { + /** @type {ElementData} */ + const elementData = { isLCP, isLCPCandidate: !! lcpMetricCandidates.find( ( lcpMetricCandidate ) => @@ -368,7 +422,8 @@ export default async function detect( { boundingClientRect: elementIntersection.boundingClientRect, }; - urlMetric.elements.push( elementMetrics ); + urlMetric.elements.push( elementData ); + elementsByXPath.set( elementData.xpath, elementData ); } if ( isDebug ) { @@ -391,12 +446,37 @@ export default async function detect( { ); } ); - for ( const extension of extensions ) { - if ( extension.finalize instanceof Function ) { - extension.finalize( { - isDebug, - urlMetric, - } ); + if ( extensions.length > 0 ) { + /** + * Gets root URL Metric data. + * + * @return {URLMetric} URL Metric. + */ + const getRootData = () => { + const immutableUrlMetric = structuredClone( urlMetric ); + recursiveFreeze( immutableUrlMetric ); + return immutableUrlMetric; + }; + + /** + * Amends root URL metric data. + * + * @param {Object} properties + */ + const amendRootData = ( properties ) => { + Object.assign( urlMetric, properties ); // TODO: Prevent overriding core properties. + }; + + for ( const extension of extensions ) { + if ( extension.finalize instanceof Function ) { + extension.finalize( { + isDebug, + getRootData, + getElementData, + amendElementData, + amendRootData, + } ); + } } } diff --git a/plugins/optimization-detective/types.d.ts b/plugins/optimization-detective/types.d.ts index 3597591ae9..fad448fa49 100644 --- a/plugins/optimization-detective/types.d.ts +++ b/plugins/optimization-detective/types.d.ts @@ -1,4 +1,4 @@ -interface ElementMetrics { +interface ElementData { isLCP: boolean; isLCPCandidate: boolean; xpath: string; @@ -13,7 +13,7 @@ interface URLMetric { width: number; height: number; }; - elements: ElementMetrics[]; + elements: ElementData[]; } interface URLMetricGroupStatus { @@ -21,7 +21,23 @@ interface URLMetricGroupStatus { complete: boolean; } +type InitializeArgs = { + readonly isDebug: boolean, +}; + +type InitializeCallback = ( args: InitializeArgs ) => void; + +type FinalizeArgs = { + readonly getRootData: () => URLMetric, + readonly amendRootData: ( properties: object ) => void, + readonly getElementData: ( xpath: string ) => ElementData|null, + readonly amendElementData: ( xpath: string, properties: object ) => boolean, + readonly isDebug: boolean, +}; + +type FinalizeCallback = ( args: FinalizeArgs ) => void; + interface Extension { - initialize?: Function; - finalize?: Function; + initialize?: InitializeCallback; + finalize?: FinalizeCallback; } From f66445f25ae2de5431fd3f6f11c820bc4ddc4060 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 10 Oct 2024 22:10:25 -0700 Subject: [PATCH 057/156] Improve error handling --- plugins/embed-optimizer/detect.js | 32 ++++++++----- plugins/optimization-detective/detect.js | 56 +++++++++++++++-------- plugins/optimization-detective/types.d.ts | 2 +- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index e91e98fa67..800ce8738f 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -28,6 +28,16 @@ function log( ...message ) { console.log( consoleLogPrefix, ...message ); } +/** + * Log an error. + * + * @param {...*} message + */ +function error( ...message ) { + // eslint-disable-next-line no-console + console.error( consoleLogPrefix, ...message ); +} + /** * Embed element heights. * @@ -63,30 +73,28 @@ export async function initialize( { isDebug } ) { * @param {FinalizeArgs} args Args. */ export async function finalize( { - urlMetric, isDebug, getElementData, amendElementData, } ) { - if ( isDebug ) { - log( 'URL metric to be sent:', urlMetric ); - } - for ( const [ xpath, domRect ] of loadedElementContentRects.entries() ) { - if ( - amendElementData( xpath, { resizedBoundingClientRect: domRect } ) - ) { - const elementData = getElementData( xpath ); + try { + amendElementData( xpath, { resizedBoundingClientRect: domRect } ); if ( isDebug ) { + const elementData = getElementData( xpath ); log( `boundingClientRect for ${ xpath } resized:`, elementData.boundingClientRect, '=>', - elementData.resizedBoundingClientRect + domRect ); } - } else if ( isDebug ) { - log( `Unable to amend element data for ${ xpath }` ); + } catch ( err ) { + error( + `Failed to amend ${ xpath } with resizedBoundingClientRect data:`, + domRect, + err + ); } } } diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 77c493532d..7f81fa8f16 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -153,15 +153,13 @@ function getElementData( xpath ) { * * @param {string} xpath XPath. * @param {Object} properties Properties. - * @return {boolean} Whether appending element data was successful. */ function amendElementData( xpath, properties ) { if ( ! elementsByXPath.has( xpath ) ) { - return false; + throw new Error( `Unknown element with XPath: ${ xpath }` ); } const elementData = elementsByXPath.get( xpath ); Object.assign( elementData, properties ); // TODO: Check if any core properties are being used. - return true; } /** @@ -290,15 +288,23 @@ export default async function detect( { log( 'Proceeding with detection' ); } - /** @type {Extension[]} */ - const extensions = []; + /** @type {Map} */ + const extensions = new Map(); for ( const extensionModuleUrl of extensionModuleUrls ) { - const extension = await import( extensionModuleUrl ); - extensions.push( extension ); - // TODO: There should to be a way to pass additional args into the module. Perhaps extensionModuleUrls should be a mapping of URLs to args. It's important to pass webVitalsLibrarySrc to the extension so that onLCP, onCLS, or onINP can be obtained. - // TODO: Pass additional functions from this module into the extensions. - if ( extension.initialize instanceof Function ) { - extension.initialize( { isDebug } ); + try { + /** @type {Extension} */ + const extension = await import( extensionModuleUrl ); + extensions.set( extensionModuleUrl, extension ); + // TODO: There should to be a way to pass additional args into the module. Perhaps extensionModuleUrls should be a mapping of URLs to args. It's important to pass webVitalsLibrarySrc to the extension so that onLCP, onCLS, or onINP can be obtained. + // TODO: Pass additional functions from this module into the extensions. + if ( extension.initialize instanceof Function ) { + extension.initialize( { isDebug } ); + } + } catch ( err ) { + error( + `Failed to initialize extension '${ extensionModuleUrl }':`, + err + ); } } @@ -446,7 +452,7 @@ export default async function detect( { ); } ); - if ( extensions.length > 0 ) { + if ( extensions.size > 0 ) { /** * Gets root URL Metric data. * @@ -467,15 +473,25 @@ export default async function detect( { Object.assign( urlMetric, properties ); // TODO: Prevent overriding core properties. }; - for ( const extension of extensions ) { + for ( const [ + extensionModuleUrl, + extension, + ] of extensions.entries() ) { if ( extension.finalize instanceof Function ) { - extension.finalize( { - isDebug, - getRootData, - getElementData, - amendElementData, - amendRootData, - } ); + try { + extension.finalize( { + isDebug, + getRootData, + getElementData, + amendElementData, + amendRootData, + } ); + } catch ( err ) { + error( + `Unable to finalize module '${ extensionModuleUrl }':`, + err + ); + } } } } diff --git a/plugins/optimization-detective/types.d.ts b/plugins/optimization-detective/types.d.ts index fad448fa49..e9cdb3d772 100644 --- a/plugins/optimization-detective/types.d.ts +++ b/plugins/optimization-detective/types.d.ts @@ -31,7 +31,7 @@ type FinalizeArgs = { readonly getRootData: () => URLMetric, readonly amendRootData: ( properties: object ) => void, readonly getElementData: ( xpath: string ) => ElementData|null, - readonly amendElementData: ( xpath: string, properties: object ) => boolean, + readonly amendElementData: ( xpath: string, properties: object ) => void, readonly isDebug: boolean, }; From 6e0aa8e88933b2735186c876ca5d4b1340c4a5b2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Thu, 10 Oct 2024 23:24:06 -0700 Subject: [PATCH 058/156] Harden types and disallow setting core properties --- plugins/embed-optimizer/detect.js | 12 ++++-- plugins/optimization-detective/detect.js | 49 ++++++++++++++++++----- plugins/optimization-detective/types.d.ts | 28 ++++++++----- 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index 800ce8738f..2cd041692b 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -16,6 +16,7 @@ const consoleLogPrefix = '[Embed Optimizer]'; * @typedef {import("../optimization-detective/types.d.ts").InitializeArgs} InitializeArgs * @typedef {import("../optimization-detective/types.d.ts").FinalizeArgs} FinalizeArgs * @typedef {import("../optimization-detective/types.d.ts").FinalizeCallback} FinalizeCallback + * @typedef {import("../optimization-detective/types.d.ts").AmendedElementData} AmendedElementData */ /** @@ -51,14 +52,17 @@ const loadedElementContentRects = new Map(); * @type {InitializeCallback} * @param {InitializeArgs} args Args. */ -export async function initialize( { isDebug } ) { +export function initialize( { isDebug } ) { const embedWrappers = /** @type NodeListOf */ document.querySelectorAll( '.wp-block-embed > .wp-block-embed__wrapper[data-od-xpath]' ); for ( const embedWrapper of embedWrappers ) { - monitorEmbedWrapperForResizes( embedWrapper, isDebug ); + monitorEmbedWrapperForResizes( + embedWrapper, // TODO: Why TypeScript error: TS2345: Argument of type Element is not assignable to parameter of type HTMLDivElement. + isDebug + ); } if ( isDebug ) { @@ -79,7 +83,9 @@ export async function finalize( { } ) { for ( const [ xpath, domRect ] of loadedElementContentRects.entries() ) { try { - amendElementData( xpath, { resizedBoundingClientRect: domRect } ); + amendElementData( xpath, { + resizedBoundingClientRect: domRect, + } ); if ( isDebug ) { const elementData = getElementData( xpath ); log( diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 7f81fa8f16..96650478ee 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -1,9 +1,11 @@ /** * @typedef {import("web-vitals").LCPMetric} LCPMetric - * @typedef {import("types.d.ts").ElementData} ElementData - * @typedef {import("types.d.ts").URLMetric} URLMetric - * @typedef {import("types.d.ts").URLMetricGroupStatus} URLMetricGroupStatus - * @typedef {import("types.d.ts").Extension} Extension + * @typedef {import("./types.d.ts").ElementData} ElementData + * @typedef {import("./types.d.ts").URLMetric} URLMetric + * @typedef {import("./types.d.ts").URLMetricGroupStatus} URLMetricGroupStatus + * @typedef {import("./types.d.ts").Extension} Extension + * @typedef {import("./types.d.ts").AmendedRootData} AmendedRootData + * @typedef {import("./types.d.ts").AmendedElementData} AmendedElementData */ const win = window; @@ -151,13 +153,29 @@ function getElementData( xpath ) { /** * Amends element data. * - * @param {string} xpath XPath. - * @param {Object} properties Properties. + * @param {string} xpath XPath. + * @param {AmendedElementData} properties Properties. */ function amendElementData( xpath, properties ) { if ( ! elementsByXPath.has( xpath ) ) { throw new Error( `Unknown element with XPath: ${ xpath }` ); } + for ( const key of Object.getOwnPropertyNames( properties ) ) { + if ( + [ + 'isLCP', + 'isLCPCandidate', + 'xpath', + 'intersectionRatio', + 'intersectionRect', + 'boundingClientRect', + ].includes( key ) + ) { + throw new Error( + `Disallowed setting of key '${ key }' on element.` + ); + } + } const elementData = elementsByXPath.get( xpath ); Object.assign( elementData, properties ); // TODO: Check if any core properties are being used. } @@ -310,7 +328,7 @@ export default async function detect( { const breadcrumbedElements = doc.body.querySelectorAll( '[data-od-xpath]' ); - /** @type {Map} */ + /** @type {Map} */ const breadcrumbedElementsMap = new Map( [ ...breadcrumbedElements ].map( /** @@ -371,7 +389,7 @@ export default async function detect( { // Obtain at least one LCP candidate. More may be reported before the page finishes loading. await new Promise( ( resolve ) => { onLCP( - ( metric ) => { + ( /** @type LCPMetric */ metric ) => { lcpMetricCandidates.push( metric ); resolve(); }, @@ -467,10 +485,19 @@ export default async function detect( { /** * Amends root URL metric data. * - * @param {Object} properties + * @todo Would "extend" be better than "amend"? Or something else? + * + * @param {AmendedRootData} properties */ const amendRootData = ( properties ) => { - Object.assign( urlMetric, properties ); // TODO: Prevent overriding core properties. + for ( const key of Object.getOwnPropertyNames( properties ) ) { + if ( [ 'url', 'viewport', 'elements' ].includes( key ) ) { + throw new Error( + `Disallowed setting of key '${ key }' on root.` + ); + } + } + Object.assign( urlMetric, properties ); }; for ( const [ @@ -479,7 +506,7 @@ export default async function detect( { ] of extensions.entries() ) { if ( extension.finalize instanceof Function ) { try { - extension.finalize( { + await extension.finalize( { isDebug, getRootData, getElementData, diff --git a/plugins/optimization-detective/types.d.ts b/plugins/optimization-detective/types.d.ts index e9cdb3d772..7668e97eb1 100644 --- a/plugins/optimization-detective/types.d.ts +++ b/plugins/optimization-detective/types.d.ts @@ -1,4 +1,8 @@ -interface ElementData { + +// h/t https://stackoverflow.com/a/59801602/93579 +type ExcludeProps = { [k: string]: any } & { [K in keyof T]?: never } + +export interface ElementData { isLCP: boolean; isLCPCandidate: boolean; xpath: string; @@ -7,7 +11,9 @@ interface ElementData { boundingClientRect: DOMRectReadOnly; } -interface URLMetric { +export type AmendedElementData = ExcludeProps + +export interface URLMetric { url: string; viewport: { width: number; @@ -16,28 +22,30 @@ interface URLMetric { elements: ElementData[]; } -interface URLMetricGroupStatus { +export type AmendedRootData = ExcludeProps + +export interface URLMetricGroupStatus { minimumViewportWidth: number; complete: boolean; } -type InitializeArgs = { +export type InitializeArgs = { readonly isDebug: boolean, }; -type InitializeCallback = ( args: InitializeArgs ) => void; +export type InitializeCallback = ( args: InitializeArgs ) => void; -type FinalizeArgs = { +export type FinalizeArgs = { readonly getRootData: () => URLMetric, - readonly amendRootData: ( properties: object ) => void, + readonly amendRootData: ( properties: AmendedRootData ) => void, readonly getElementData: ( xpath: string ) => ElementData|null, - readonly amendElementData: ( xpath: string, properties: object ) => void, + readonly amendElementData: ( xpath: string, properties: AmendedElementData ) => void, readonly isDebug: boolean, }; -type FinalizeCallback = ( args: FinalizeArgs ) => void; +export type FinalizeCallback = ( args: FinalizeArgs ) => Promise; -interface Extension { +export interface Extension { initialize?: InitializeCallback; finalize?: FinalizeCallback; } From 9e99e0d2547c05050e4dde374fe61b793f3358d3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 11 Oct 2024 07:14:07 -0700 Subject: [PATCH 059/156] Reuse sets for reserved property keys --- plugins/optimization-detective/detect.js | 40 +++++++++++++++++------- 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 96650478ee..e670cc6e38 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -134,6 +134,31 @@ function recursiveFreeze( obj ) { */ const elementsByXPath = new Map(); +/** + * Reserved root property keys. + * + * @see {URLMetric} + * @see {AmendedElementData} + * @type {Set} + */ +const reservedRootPropertyKeys = new Set( [ 'url', 'viewport', 'elements' ] ); + +/** + * Reserved element property keys. + * + * @see {ElementData} + * @see {AmendedRootData} + * @type {Set} + */ +const reservedElementPropertyKeys = new Set( [ + 'isLCP', + 'isLCPCandidate', + 'xpath', + 'intersectionRatio', + 'intersectionRect', + 'boundingClientRect', +] ); + /** * Gets element data. * @@ -161,23 +186,14 @@ function amendElementData( xpath, properties ) { throw new Error( `Unknown element with XPath: ${ xpath }` ); } for ( const key of Object.getOwnPropertyNames( properties ) ) { - if ( - [ - 'isLCP', - 'isLCPCandidate', - 'xpath', - 'intersectionRatio', - 'intersectionRect', - 'boundingClientRect', - ].includes( key ) - ) { + if ( reservedElementPropertyKeys.has( key ) ) { throw new Error( `Disallowed setting of key '${ key }' on element.` ); } } const elementData = elementsByXPath.get( xpath ); - Object.assign( elementData, properties ); // TODO: Check if any core properties are being used. + Object.assign( elementData, properties ); } /** @@ -491,7 +507,7 @@ export default async function detect( { */ const amendRootData = ( properties ) => { for ( const key of Object.getOwnPropertyNames( properties ) ) { - if ( [ 'url', 'viewport', 'elements' ].includes( key ) ) { + if ( reservedRootPropertyKeys.has( key ) ) { throw new Error( `Disallowed setting of key '${ key }' on root.` ); From 46ba7e31e96b61add11f2edff313def529d1f796 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 11 Oct 2024 07:25:20 -0700 Subject: [PATCH 060/156] Move functions to root of module --- plugins/embed-optimizer/detect.js | 8 +-- plugins/optimization-detective/detect.js | 81 ++++++++++++------------ 2 files changed, 46 insertions(+), 43 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index 2cd041692b..a84cc9f846 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -20,7 +20,7 @@ const consoleLogPrefix = '[Embed Optimizer]'; */ /** - * Log a message. + * Logs a message. * * @param {...*} message */ @@ -30,7 +30,7 @@ function log( ...message ) { } /** - * Log an error. + * Logs an error. * * @param {...*} message */ @@ -47,7 +47,7 @@ function error( ...message ) { const loadedElementContentRects = new Map(); /** - * Initialize. + * Initializes extension. * * @type {InitializeCallback} * @param {InitializeArgs} args Args. @@ -71,7 +71,7 @@ export function initialize( { isDebug } ) { } /** - * Finalize. + * Finalizes extension. * * @type {FinalizeCallback} * @param {FinalizeArgs} args Args. diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index e670cc6e38..e24cb5841c 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -41,7 +41,7 @@ function isStorageLocked( currentTime, storageLockTTL ) { } /** - * Set the storage lock. + * Sets the storage lock. * * @param {number} currentTime - Current time in milliseconds. */ @@ -55,7 +55,7 @@ function setStorageLock( currentTime ) { } /** - * Log a message. + * Logs a message. * * @param {...*} message */ @@ -65,7 +65,7 @@ function log( ...message ) { } /** - * Log a warning. + * Logs a warning. * * @param {...*} message */ @@ -75,7 +75,7 @@ function warn( ...message ) { } /** - * Log an error. + * Logs an error. * * @param {...*} message */ @@ -128,11 +128,11 @@ function recursiveFreeze( obj ) { } /** - * Mapping of XPath to element data. + * URL metric being assembled for submission. * - * @type {Map} + * @type {URLMetric} */ -const elementsByXPath = new Map(); +let urlMetric; /** * Reserved root property keys. @@ -143,6 +143,40 @@ const elementsByXPath = new Map(); */ const reservedRootPropertyKeys = new Set( [ 'url', 'viewport', 'elements' ] ); +/** + * Gets root URL Metric data. + * + * @return {URLMetric} URL Metric. + */ +function getRootData() { + const immutableUrlMetric = structuredClone( urlMetric ); + recursiveFreeze( immutableUrlMetric ); + return immutableUrlMetric; +} + +/** + * Amends root URL metric data. + * + * @todo Would "extend" be better than "amend"? Or something else? + * + * @param {AmendedRootData} properties + */ +function amendRootData( properties ) { + for ( const key of Object.getOwnPropertyNames( properties ) ) { + if ( reservedRootPropertyKeys.has( key ) ) { + throw new Error( `Disallowed setting of key '${ key }' on root.` ); + } + } + Object.assign( urlMetric, properties ); +} + +/** + * Mapping of XPath to element data. + * + * @type {Map} + */ +const elementsByXPath = new Map(); + /** * Reserved element property keys. * @@ -330,7 +364,6 @@ export default async function detect( { const extension = await import( extensionModuleUrl ); extensions.set( extensionModuleUrl, extension ); // TODO: There should to be a way to pass additional args into the module. Perhaps extensionModuleUrls should be a mapping of URLs to args. It's important to pass webVitalsLibrarySrc to the extension so that onLCP, onCLS, or onINP can be obtained. - // TODO: Pass additional functions from this module into the extensions. if ( extension.initialize instanceof Function ) { extension.initialize( { isDebug } ); } @@ -424,8 +457,7 @@ export default async function detect( { log( 'Detection is stopping.' ); } - /** @type {URLMetric} */ - const urlMetric = { + urlMetric = { url: currentUrl, viewport: { width: win.innerWidth, @@ -487,35 +519,6 @@ export default async function detect( { } ); if ( extensions.size > 0 ) { - /** - * Gets root URL Metric data. - * - * @return {URLMetric} URL Metric. - */ - const getRootData = () => { - const immutableUrlMetric = structuredClone( urlMetric ); - recursiveFreeze( immutableUrlMetric ); - return immutableUrlMetric; - }; - - /** - * Amends root URL metric data. - * - * @todo Would "extend" be better than "amend"? Or something else? - * - * @param {AmendedRootData} properties - */ - const amendRootData = ( properties ) => { - for ( const key of Object.getOwnPropertyNames( properties ) ) { - if ( reservedRootPropertyKeys.has( key ) ) { - throw new Error( - `Disallowed setting of key '${ key }' on root.` - ); - } - } - Object.assign( urlMetric, properties ); - }; - for ( const [ extensionModuleUrl, extension, From 0bc521e78e12937e1a24fb3c29c789f38ef9cbee Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Fri, 11 Oct 2024 07:29:38 -0700 Subject: [PATCH 061/156] Fix TypeScript error related to embedWrapper --- plugins/embed-optimizer/detect.js | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index a84cc9f846..bb4a6fb620 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -53,16 +53,13 @@ const loadedElementContentRects = new Map(); * @param {InitializeArgs} args Args. */ export function initialize( { isDebug } ) { - const embedWrappers = - /** @type NodeListOf */ document.querySelectorAll( - '.wp-block-embed > .wp-block-embed__wrapper[data-od-xpath]' - ); + /** @type NodeListOf */ + const embedWrappers = document.querySelectorAll( + '.wp-block-embed > .wp-block-embed__wrapper[data-od-xpath]' + ); for ( const embedWrapper of embedWrappers ) { - monitorEmbedWrapperForResizes( - embedWrapper, // TODO: Why TypeScript error: TS2345: Argument of type Element is not assignable to parameter of type HTMLDivElement. - isDebug - ); + monitorEmbedWrapperForResizes( embedWrapper, isDebug ); } if ( isDebug ) { From 477cc337c7b8f9c8215e61fdd22c3b357319740f Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 10:09:21 -0700 Subject: [PATCH 062/156] Add missing period to return doc Co-authored-by: Adam Silverstein --- plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index 1824ab4a86..f4b98b14ae 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -48,7 +48,7 @@ private function is_embed_figure( OD_HTML_Tag_Processor $processor ): bool { * @since n.e.x.t * * @param OD_HTML_Tag_Processor $processor Processor. - * @return bool Whether the tag should be measured and stored in URL metrics + * @return bool Whether the tag should be measured and stored in URL metrics. */ private function is_embed_wrapper( OD_HTML_Tag_Processor $processor ): bool { return ( From dca43e9d8ff8cf121ee5c53e1d8e1903840bedc8 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 10:15:02 -0700 Subject: [PATCH 063/156] Eliminate needless ternary Co-authored-by: adamsilverstein --- .../class-od-url-metric-group-collection.php | 4 +--- .../test-class-od-url-metrics-group-collection.php | 14 +++++++++++--- 2 files changed, 12 insertions(+), 6 deletions(-) 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 92d9b8ca22..9943991ca1 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -462,9 +462,7 @@ public function get_all_element_max_intersection_ratios(): array { foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { $element_intersection_ratios[] = $element['intersectionRatio']; } - $elements_max_intersection_ratios[ $xpath ] = count( $element_intersection_ratios ) > 1 - ? (float) max( ...$element_intersection_ratios ) - : $element_intersection_ratios[0]; + $elements_max_intersection_ratios[ $xpath ] = (float) max( $element_intersection_ratios ); } return $elements_max_intersection_ratios; } )(); 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 de9518973d..2b4b3329c3 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 @@ -661,7 +661,15 @@ public function data_provider_element_max_intersection_ratios(): array { }; return array( - 'one-element-sample-size-one' => array( + 'one-element-one-group' => array( + 'url_metrics' => array( + $get_sample_url_metric( 600, $xpath1, 0.5 ), + ), + 'expected' => array( + $xpath1 => 0.5, + ), + ), + 'one-element-three-groups-of-one' => array( 'url_metrics' => array( $get_sample_url_metric( 400, $xpath1, 0.0 ), $get_sample_url_metric( 600, $xpath1, 0.5 ), @@ -671,7 +679,7 @@ public function data_provider_element_max_intersection_ratios(): array { $xpath1 => 1.0, ), ), - 'three-elements-sample-size-two' => array( + 'three-elements-sample-size-two' => array( 'url_metrics' => array( // Group 1. $get_sample_url_metric( 400, $xpath1, 0.0 ), @@ -689,7 +697,7 @@ public function data_provider_element_max_intersection_ratios(): array { $xpath3 => 0.6, ), ), - 'no-url-metrics' => array( + 'no-url-metrics' => array( 'url_metrics' => array(), 'expected' => array(), ), From 73d2252961228c07f8ec1e2af82fd1cd588bb09d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 10:26:25 -0700 Subject: [PATCH 064/156] Rename amend to extend --- plugins/embed-optimizer/detect.js | 12 +++++----- plugins/optimization-detective/detect.js | 28 +++++++++++------------ plugins/optimization-detective/types.d.ts | 8 +++---- 3 files changed, 23 insertions(+), 25 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index bb4a6fb620..9a5f93e71d 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -2,8 +2,8 @@ * Embed Optimizer module for Optimization Detective * * When a URL metric is being collected by Optimization Detective, this module adds a ResizeObserver to keep track of - * the changed heights for embed blocks. This data is amended onto the element data of the pending URL metric when it - * is submitted for storage. + * the changed heights for embed blocks. This data is extended/amended onto the element data of the pending URL metric + * when it is submitted for storage. */ const consoleLogPrefix = '[Embed Optimizer]'; @@ -16,7 +16,7 @@ const consoleLogPrefix = '[Embed Optimizer]'; * @typedef {import("../optimization-detective/types.d.ts").InitializeArgs} InitializeArgs * @typedef {import("../optimization-detective/types.d.ts").FinalizeArgs} FinalizeArgs * @typedef {import("../optimization-detective/types.d.ts").FinalizeCallback} FinalizeCallback - * @typedef {import("../optimization-detective/types.d.ts").AmendedElementData} AmendedElementData + * @typedef {import("../optimization-detective/types.d.ts").ExtendedElementData} ExtendedElementData */ /** @@ -76,11 +76,11 @@ export function initialize( { isDebug } ) { export async function finalize( { isDebug, getElementData, - amendElementData, + extendElementData, } ) { for ( const [ xpath, domRect ] of loadedElementContentRects.entries() ) { try { - amendElementData( xpath, { + extendElementData( xpath, { resizedBoundingClientRect: domRect, } ); if ( isDebug ) { @@ -94,7 +94,7 @@ export async function finalize( { } } catch ( err ) { error( - `Failed to amend ${ xpath } with resizedBoundingClientRect data:`, + `Failed to extend element data for ${ xpath } with resizedBoundingClientRect:`, domRect, err ); diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index e24cb5841c..3f04404118 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -4,8 +4,8 @@ * @typedef {import("./types.d.ts").URLMetric} URLMetric * @typedef {import("./types.d.ts").URLMetricGroupStatus} URLMetricGroupStatus * @typedef {import("./types.d.ts").Extension} Extension - * @typedef {import("./types.d.ts").AmendedRootData} AmendedRootData - * @typedef {import("./types.d.ts").AmendedElementData} AmendedElementData + * @typedef {import("./types.d.ts").ExtendedRootData} ExtendedRootData + * @typedef {import("./types.d.ts").ExtendedElementData} ExtendedElementData */ const win = window; @@ -138,7 +138,7 @@ let urlMetric; * Reserved root property keys. * * @see {URLMetric} - * @see {AmendedElementData} + * @see {ExtendedElementData} * @type {Set} */ const reservedRootPropertyKeys = new Set( [ 'url', 'viewport', 'elements' ] ); @@ -155,13 +155,11 @@ function getRootData() { } /** - * Amends root URL metric data. + * Extends root URL metric data. * - * @todo Would "extend" be better than "amend"? Or something else? - * - * @param {AmendedRootData} properties + * @param {ExtendedRootData} properties */ -function amendRootData( properties ) { +function extendRootData( properties ) { for ( const key of Object.getOwnPropertyNames( properties ) ) { if ( reservedRootPropertyKeys.has( key ) ) { throw new Error( `Disallowed setting of key '${ key }' on root.` ); @@ -181,7 +179,7 @@ const elementsByXPath = new Map(); * Reserved element property keys. * * @see {ElementData} - * @see {AmendedRootData} + * @see {ExtendedRootData} * @type {Set} */ const reservedElementPropertyKeys = new Set( [ @@ -210,12 +208,12 @@ function getElementData( xpath ) { } /** - * Amends element data. + * Extends element data. * - * @param {string} xpath XPath. - * @param {AmendedElementData} properties Properties. + * @param {string} xpath XPath. + * @param {ExtendedElementData} properties Properties. */ -function amendElementData( xpath, properties ) { +function extendElementData( xpath, properties ) { if ( ! elementsByXPath.has( xpath ) ) { throw new Error( `Unknown element with XPath: ${ xpath }` ); } @@ -529,8 +527,8 @@ export default async function detect( { isDebug, getRootData, getElementData, - amendElementData, - amendRootData, + extendElementData, + extendRootData, } ); } catch ( err ) { error( diff --git a/plugins/optimization-detective/types.d.ts b/plugins/optimization-detective/types.d.ts index 7668e97eb1..50b36356f8 100644 --- a/plugins/optimization-detective/types.d.ts +++ b/plugins/optimization-detective/types.d.ts @@ -11,7 +11,7 @@ export interface ElementData { boundingClientRect: DOMRectReadOnly; } -export type AmendedElementData = ExcludeProps +export type ExtendedElementData = ExcludeProps export interface URLMetric { url: string; @@ -22,7 +22,7 @@ export interface URLMetric { elements: ElementData[]; } -export type AmendedRootData = ExcludeProps +export type ExtendedRootData = ExcludeProps export interface URLMetricGroupStatus { minimumViewportWidth: number; @@ -37,9 +37,9 @@ export type InitializeCallback = ( args: InitializeArgs ) => void; export type FinalizeArgs = { readonly getRootData: () => URLMetric, - readonly amendRootData: ( properties: AmendedRootData ) => void, + readonly extendRootData: ( properties: ExtendedRootData ) => void, readonly getElementData: ( xpath: string ) => ElementData|null, - readonly amendElementData: ( xpath: string, properties: AmendedElementData ) => void, + readonly extendElementData: (xpath: string, properties: ExtendedElementData ) => void, readonly isDebug: boolean, }; From da3da34a6262236c46a046b2341627e54df004c4 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 11:34:43 -0700 Subject: [PATCH 065/156] Include dimensions in exception error message for aspect-ratio --- plugins/optimization-detective/class-od-url-metric.php | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 9de7d57a98..797343b395 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -96,11 +96,13 @@ private function prepare_data( array $data ): array { throw new OD_Data_Validation_Exception( esc_html( sprintf( - /* translators: 1: current aspect ratio, 2: minimum aspect ratio, 3: maximum aspect ratio */ - __( 'Viewport aspect ratio (%1$s) is not in the accepted range of %2$s to %3$s.', 'optimization-detective' ), + /* translators: 1: current aspect ratio, 2: minimum aspect ratio, 3: maximum aspect ratio, 4: viewport width, 5: viewport height */ + __( 'Viewport aspect ratio (%1$s) is not in the accepted range of %2$s to %3$s. Viewport dimensions: %4$sx%5$s', 'optimization-detective' ), $aspect_ratio, $min_aspect_ratio, - $max_aspect_ratio + $max_aspect_ratio, + $data['viewport']['width'], + $data['viewport']['height'] ) ) ); From 785c1312a52923655cfee6ec78a524de41b98d05 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 11:45:56 -0700 Subject: [PATCH 066/156] Reuse get_all_denormalized_elements() --- .../class-od-url-metric-group-collection.php | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) 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 ce272704fa..a544141b86 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -493,14 +493,12 @@ public function get_all_elements_positioned_in_any_initial_viewport(): array { $result = ( function () { $elements_positioned = array(); - foreach ( $this->groups as $group ) { - foreach ( $group as $url_metric ) { - foreach ( $url_metric->get_elements() as $element ) { - $elements_positioned[ $element['xpath'] ] = ( - ( $elements_positioned[ $element['xpath'] ] ?? false ) - || - $element['boundingClientRect']['top'] < $url_metric->get_viewport()['height'] - ); + foreach ( $this->get_all_denormalized_elements() as $xpath => $denormalized_elements ) { + $elements_positioned[ $xpath ] = false; + foreach ( $denormalized_elements as list( $group, $url_metric, $element ) ) { + if ( $element['boundingClientRect']['top'] < $url_metric->get_viewport()['height'] ) { + $elements_positioned[ $xpath ] = true; + break; } } } From 0e617672d334923a0c2124887ea7cbbbd3842057 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 11:52:46 -0700 Subject: [PATCH 067/156] Improve usage of is_element_positioned_in_any_initial_viewport --- .../class-image-prioritizer-img-tag-visitor.php | 7 +++---- .../class-od-url-metric-group-collection.php | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php index 7a34c025c2..b0ea74ece5 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php @@ -84,16 +84,15 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $updated_fetchpriority = false; // That is, remove it. } - $element_max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath ); - $is_positioned_in_any_initial_viewport = $context->url_metric_group_collection->is_element_positioned_in_any_initial_viewport( $xpath ); + $element_max_intersection_ratio = $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath ); // If the element was not found, we don't know if it was visible for not, so don't do anything. - if ( is_null( $element_max_intersection_ratio ) || is_null( $is_positioned_in_any_initial_viewport ) ) { + if ( is_null( $element_max_intersection_ratio ) ) { $processor->set_meta_attribute( 'unknown-tag', true ); // Mostly useful for debugging why an IMG isn't optimized. } else { // TODO: Take into account whether the element has the computed style of visibility:hidden, in such case it should also get fetchpriority=low. $is_visible = $element_max_intersection_ratio > 0.0; - if ( $is_positioned_in_any_initial_viewport ) { + if ( true === $context->url_metric_group_collection->is_element_positioned_in_any_initial_viewport( $xpath ) ) { if ( ! $is_visible ) { // If an element is positioned in the initial viewport and yet it is it not visible, it may be // located in a subsequent carousel slide or inside a hidden navigation menu which could be 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 a544141b86..d42a56e31c 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -527,7 +527,7 @@ public function get_element_max_intersection_ratio( string $xpath ): ?float { * @since n.e.x.t * * @param string $xpath XPath for the element. - * @return bool Whether element is positioned in any initial viewport of null if unknown. + * @return bool|null Whether element is positioned in any initial viewport of null if unknown. */ public function is_element_positioned_in_any_initial_viewport( string $xpath ): ?bool { return $this->get_all_elements_positioned_in_any_initial_viewport()[ $xpath ] ?? null; From 882df0c3ab48dacdb1dba079789a219ecffa89e9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 13:00:16 -0700 Subject: [PATCH 068/156] Clarify purpose of is_element_positioned_in_any_initial_viewport --- ...ocated-above-or-along-initial-viewport.php | 111 ++++++++++++++++++ .../class-od-url-metric-group-collection.php | 2 + 2 files changed, 113 insertions(+) create mode 100644 plugins/image-prioritizer/tests/test-cases/images-located-above-or-along-initial-viewport.php diff --git a/plugins/image-prioritizer/tests/test-cases/images-located-above-or-along-initial-viewport.php b/plugins/image-prioritizer/tests/test-cases/images-located-above-or-along-initial-viewport.php new file mode 100644 index 0000000000..2cfc863452 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/images-located-above-or-along-initial-viewport.php @@ -0,0 +1,111 @@ + static function ( Test_Image_Prioritizer_Helper $test_case ): void { + $slug = od_get_url_metrics_slug( od_get_normalized_query_vars() ); + $sample_size = od_get_url_metrics_breakpoint_sample_size(); + $outside_viewport_rect = array_merge( + $test_case->get_sample_dom_rect(), + array( + 'top' => 100000, + ) + ); + + $get_dom_rect = static function ( $left, $top, $width, $height ) { + $dom_rect = array( + 'top' => $top, + 'left' => $left, + 'width' => $width, + 'height' => $height, + 'x' => $left, + 'y' => $top, + ); + $dom_rect['bottom'] = $dom_rect['top'] + $height; + $dom_rect['right'] = $dom_rect['left'] + $width; + return $dom_rect; + }; + + $width = 10; + $height = 10; + $above_viewport_rect = $get_dom_rect( 0, -100, $width, $height ); + $left_of_viewport_rect = $get_dom_rect( -100, 0, $width, $height ); + $right_of_viewport_rect = $get_dom_rect( 10000000, 0, $width, $height ); + $below_viewport_rect = $get_dom_rect( 0, 1000000, $width, $height ); + + foreach ( array_merge( od_get_breakpoint_max_widths(), array( 1000 ) ) as $viewport_width ) { + for ( $i = 0; $i < $sample_size; $i++ ) { + OD_URL_Metrics_Post_Type::store_url_metric( + $slug, + $test_case->get_sample_url_metric( + array( + 'viewport_width' => $viewport_width, + 'elements' => array( + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $above_viewport_rect, + 'boundingClientRect' => $above_viewport_rect, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $left_of_viewport_rect, + 'boundingClientRect' => $left_of_viewport_rect, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[3][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $right_of_viewport_rect, + 'boundingClientRect' => $right_of_viewport_rect, + ), + array( + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[4][self::IMG]', + 'isLCP' => false, + 'intersectionRatio' => 0.0, + 'intersectionRect' => $below_viewport_rect, + 'boundingClientRect' => $below_viewport_rect, + ), + ), + ) + ) + ); + } + } + }, + 'buffer' => ' + + + + ... + + + + Left of Viewport + Above viewport + Right of viewport + + + Below viewport + + + ', + 'expected' => ' + + + + ... + + + + Left of Viewport + Above viewport + Right of viewport + + + Below viewport + + + ', +); 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 d42a56e31c..2caaba1a83 100644 --- a/plugins/optimization-detective/class-od-url-metric-group-collection.php +++ b/plugins/optimization-detective/class-od-url-metric-group-collection.php @@ -481,6 +481,8 @@ public function get_all_element_max_intersection_ratios(): array { * case it will have intersectionRatio of 0. Or the element may not be visible due to it or an ancestor having the * `visibility:hidden` style, such as in the case of a dropdown navigation menu. When, for example, an IMG element * is positioned in any initial viewport, it should not get `loading=lazy` but rather `fetchpriority=low`. + * Furthermore, the element may be positioned _above_ the initial viewport or to the left or right of the viewport, + * in which case the element may be dynamically displayed at any time in response to a user interaction. * * @since n.e.x.t * From 299e68d9ca928bc5261029a6aa745fc9c9ac86d3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Oct 2024 23:36:24 +0000 Subject: [PATCH 069/156] Bump phpstan/php-8-stubs from 0.4.0 to 0.4.2 Bumps [phpstan/php-8-stubs](https://github.com/phpstan/php-8-stubs) from 0.4.0 to 0.4.2. - [Release notes](https://github.com/phpstan/php-8-stubs/releases) - [Commits](https://github.com/phpstan/php-8-stubs/compare/0.4.0...0.4.2) --- updated-dependencies: - dependency-name: phpstan/php-8-stubs dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- composer.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/composer.lock b/composer.lock index 1aac8e9f81..4de94bd65d 100644 --- a/composer.lock +++ b/composer.lock @@ -658,16 +658,16 @@ }, { "name": "phpstan/php-8-stubs", - "version": "0.4.0", + "version": "0.4.2", "source": { "type": "git", "url": "https://github.com/phpstan/php-8-stubs.git", - "reference": "693817d86d0d0de1d39b97a70bff4fa728384aa1" + "reference": "64fbb357f86728a3d0a06d57178bf968bcf82206" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/693817d86d0d0de1d39b97a70bff4fa728384aa1", - "reference": "693817d86d0d0de1d39b97a70bff4fa728384aa1", + "url": "https://api.github.com/repos/phpstan/php-8-stubs/zipball/64fbb357f86728a3d0a06d57178bf968bcf82206", + "reference": "64fbb357f86728a3d0a06d57178bf968bcf82206", "shasum": "" }, "type": "library", @@ -684,9 +684,9 @@ "description": "PHP stubs extracted from php-src", "support": { "issues": "https://github.com/phpstan/php-8-stubs/issues", - "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.0" + "source": "https://github.com/phpstan/php-8-stubs/tree/0.4.2" }, - "time": "2024-09-30T19:56:21+00:00" + "time": "2024-10-09T07:25:55+00:00" }, { "name": "phpstan/phpdoc-parser", From edcff0cc550421897acf8811d42a66d5a08bfcdc Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 17:20:45 -0700 Subject: [PATCH 070/156] Install typescript --- package-lock.json | 8 ++++---- package.json | 1 + 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4b20d4cc0b..4032f6836a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "lodash": "4.17.21", "micromatch": "^4.0.8", "npm-run-all": "^4.1.5", + "typescript": "^5.6.3", "webpackbar": "^6.0.1" }, "engines": { @@ -19776,11 +19777,10 @@ } }, "node_modules/typescript": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.3.3.tgz", - "integrity": "sha512-pXWcraxM0uxAS+tN0AG/BF2TyqmHO014Z070UsJ+pFvYuRSq8KH8DmWpnbXe0pEPDHXZV3FcAbJkijJ5oNEnWw==", + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", "dev": true, - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" diff --git a/package.json b/package.json index edfa9eafd6..71211e996a 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "lodash": "4.17.21", "micromatch": "^4.0.8", "npm-run-all": "^4.1.5", + "typescript": "^5.6.3", "webpackbar": "^6.0.1" }, "scripts": { From b8d108d1e8fe41a6316f7d461ee9461994acabc7 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 17:25:45 -0700 Subject: [PATCH 071/156] Add tsconfig.json --- tsconfig.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tsconfig.json diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..301ecee421 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "checkJs": true, + "noEmit": true, + "allowJs": true, + "target": "es2020", + "moduleResolution": "node", + "module": "esnext" + }, + "include": [ + "plugins/**/*.js", + "plugins/**/*.ts", + "plugins/**/*.d.ts" + ], + "exclude": [ + "node_modules/**/*", + "plugins/*/build/*" + ] +} From 697aa3f33017f2d684539bf38778496dd9edc45d Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 17:35:56 -0700 Subject: [PATCH 072/156] Add skipLibCheck --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 301ecee421..b6bcd23e99 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -5,7 +5,8 @@ "allowJs": true, "target": "es2020", "moduleResolution": "node", - "module": "esnext" + "module": "esnext", + "skipLibCheck": true }, "include": [ "plugins/**/*.js", From 1df19bcc2b46c9a96510ea551fb94e0a53c326c5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 17:36:15 -0700 Subject: [PATCH 073/156] Add ts files and tsc to lint-staged --- lint-staged.config.js | 7 ++++++- package.json | 1 + 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/lint-staged.config.js b/lint-staged.config.js index 36c59ab33b..a52deabb77 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -25,7 +25,12 @@ const joinFiles = ( files ) => { const PLUGIN_BASE_NAME = path.basename( __dirname ); module.exports = { - '**/*.js': ( files ) => `npm run lint-js -- ${ joinFiles( files ) }`, + '**/*.{js,ts}': ( files ) => { + return [ + `npm run lint-js -- ${ joinFiles( files ) }`, + `npm run tsc`, // TODO: How to pass joinFiles( files ) here? + ]; + }, '**/*.php': ( files ) => { const commands = [ 'composer phpstan' ]; diff --git a/package.json b/package.json index 71211e996a..c51fcc3565 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "generate-pending-release-diffs": "bin/generate-pending-release-diffs.sh", "format-js": "wp-scripts format", "lint-js": "wp-scripts lint-js", + "tsc": "tsc", "format-php": "composer format:all", "phpstan": "composer phpstan", "lint-php": "composer lint:all", From c23c790a3fc3d8678cc7d913e7cc3d0b4bd13cf1 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 17:40:02 -0700 Subject: [PATCH 074/156] Add tsc to js-lint workflow --- .github/workflows/js-lint.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/js-lint.yml b/.github/workflows/js-lint.yml index 4df00e3ff4..365d32b9d6 100644 --- a/.github/workflows/js-lint.yml +++ b/.github/workflows/js-lint.yml @@ -47,4 +47,6 @@ jobs: - name: npm install run: npm ci - name: JS Lint - run: npm run lint-js \ No newline at end of file + run: npm run lint-js + - name: TypeScript compile + run: npm run tsc From 543c279cb996178f279345d0e5a767519e9f37db Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 17:53:14 -0700 Subject: [PATCH 075/156] Rename types.d.ts to types.ts --- plugins/embed-optimizer/detect.js | 16 ++++++++-------- plugins/optimization-detective/detect.js | 12 ++++++------ .../{types.d.ts => types.ts} | 0 tsconfig.json | 1 - 4 files changed, 14 insertions(+), 15 deletions(-) rename plugins/optimization-detective/{types.d.ts => types.ts} (100%) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index 9a5f93e71d..0086d59137 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -9,14 +9,14 @@ const consoleLogPrefix = '[Embed Optimizer]'; /** - * @typedef {import("../optimization-detective/types.d.ts").ElementData} ElementMetrics - * @typedef {import("../optimization-detective/types.d.ts").URLMetric} URLMetric - * @typedef {import("../optimization-detective/types.d.ts").Extension} Extension - * @typedef {import("../optimization-detective/types.d.ts").InitializeCallback} InitializeCallback - * @typedef {import("../optimization-detective/types.d.ts").InitializeArgs} InitializeArgs - * @typedef {import("../optimization-detective/types.d.ts").FinalizeArgs} FinalizeArgs - * @typedef {import("../optimization-detective/types.d.ts").FinalizeCallback} FinalizeCallback - * @typedef {import("../optimization-detective/types.d.ts").ExtendedElementData} ExtendedElementData + * @typedef {import("../optimization-detective/types.ts").ElementData} ElementMetrics + * @typedef {import("../optimization-detective/types.ts").URLMetric} URLMetric + * @typedef {import("../optimization-detective/types.ts").Extension} Extension + * @typedef {import("../optimization-detective/types.ts").InitializeCallback} InitializeCallback + * @typedef {import("../optimization-detective/types.ts").InitializeArgs} InitializeArgs + * @typedef {import("../optimization-detective/types.ts").FinalizeArgs} FinalizeArgs + * @typedef {import("../optimization-detective/types.ts").FinalizeCallback} FinalizeCallback + * @typedef {import("../optimization-detective/types.ts").ExtendedElementData} ExtendedElementData */ /** diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index 3f04404118..9445ffbe13 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -1,11 +1,11 @@ /** * @typedef {import("web-vitals").LCPMetric} LCPMetric - * @typedef {import("./types.d.ts").ElementData} ElementData - * @typedef {import("./types.d.ts").URLMetric} URLMetric - * @typedef {import("./types.d.ts").URLMetricGroupStatus} URLMetricGroupStatus - * @typedef {import("./types.d.ts").Extension} Extension - * @typedef {import("./types.d.ts").ExtendedRootData} ExtendedRootData - * @typedef {import("./types.d.ts").ExtendedElementData} ExtendedElementData + * @typedef {import("./types.ts").ElementData} ElementData + * @typedef {import("./types.ts").URLMetric} URLMetric + * @typedef {import("./types.ts").URLMetricGroupStatus} URLMetricGroupStatus + * @typedef {import("./types.ts").Extension} Extension + * @typedef {import("./types.ts").ExtendedRootData} ExtendedRootData + * @typedef {import("./types.ts").ExtendedElementData} ExtendedElementData */ const win = window; diff --git a/plugins/optimization-detective/types.d.ts b/plugins/optimization-detective/types.ts similarity index 100% rename from plugins/optimization-detective/types.d.ts rename to plugins/optimization-detective/types.ts diff --git a/tsconfig.json b/tsconfig.json index b6bcd23e99..251efb95b5 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -11,7 +11,6 @@ "include": [ "plugins/**/*.js", "plugins/**/*.ts", - "plugins/**/*.d.ts" ], "exclude": [ "node_modules/**/*", From d6f76463c1b65c2cbb589169b359d6cd8266f558 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 17:53:33 -0700 Subject: [PATCH 076/156] Apply prettier to types.ts --- plugins/optimization-detective/types.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/plugins/optimization-detective/types.ts b/plugins/optimization-detective/types.ts index 50b36356f8..fc4e375b60 100644 --- a/plugins/optimization-detective/types.ts +++ b/plugins/optimization-detective/types.ts @@ -1,6 +1,5 @@ - // h/t https://stackoverflow.com/a/59801602/93579 -type ExcludeProps = { [k: string]: any } & { [K in keyof T]?: never } +type ExcludeProps< T > = { [ k: string ]: any } & { [ K in keyof T ]?: never }; export interface ElementData { isLCP: boolean; @@ -11,7 +10,7 @@ export interface ElementData { boundingClientRect: DOMRectReadOnly; } -export type ExtendedElementData = ExcludeProps +export type ExtendedElementData = ExcludeProps< ElementData >; export interface URLMetric { url: string; @@ -22,7 +21,7 @@ export interface URLMetric { elements: ElementData[]; } -export type ExtendedRootData = ExcludeProps +export type ExtendedRootData = ExcludeProps< URLMetric >; export interface URLMetricGroupStatus { minimumViewportWidth: number; @@ -30,20 +29,23 @@ export interface URLMetricGroupStatus { } export type InitializeArgs = { - readonly isDebug: boolean, + readonly isDebug: boolean; }; export type InitializeCallback = ( args: InitializeArgs ) => void; export type FinalizeArgs = { - readonly getRootData: () => URLMetric, - readonly extendRootData: ( properties: ExtendedRootData ) => void, - readonly getElementData: ( xpath: string ) => ElementData|null, - readonly extendElementData: (xpath: string, properties: ExtendedElementData ) => void, - readonly isDebug: boolean, + readonly getRootData: () => URLMetric; + readonly extendRootData: ( properties: ExtendedRootData ) => void; + readonly getElementData: ( xpath: string ) => ElementData | null; + readonly extendElementData: ( + xpath: string, + properties: ExtendedElementData + ) => void; + readonly isDebug: boolean; }; -export type FinalizeCallback = ( args: FinalizeArgs ) => Promise; +export type FinalizeCallback = ( args: FinalizeArgs ) => Promise< void >; export interface Extension { initialize?: InitializeCallback; From 5c22c69534ce52e00c2abfb48e2e0d119196d4b6 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 17:54:44 -0700 Subject: [PATCH 077/156] Remove unused type import --- plugins/embed-optimizer/detect.js | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index 0086d59137..596177fd3e 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -9,7 +9,6 @@ const consoleLogPrefix = '[Embed Optimizer]'; /** - * @typedef {import("../optimization-detective/types.ts").ElementData} ElementMetrics * @typedef {import("../optimization-detective/types.ts").URLMetric} URLMetric * @typedef {import("../optimization-detective/types.ts").Extension} Extension * @typedef {import("../optimization-detective/types.ts").InitializeCallback} InitializeCallback From 27bcb987d69a817a7529a78901ec793cb7e68ee3 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Mon, 14 Oct 2024 17:55:38 -0700 Subject: [PATCH 078/156] Remove redundant node_modules exclude --- tsconfig.json | 1 - 1 file changed, 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index 251efb95b5..137042b518 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,6 @@ "plugins/**/*.ts", ], "exclude": [ - "node_modules/**/*", "plugins/*/build/*" ] } From 4f125dee78efd65cabfa26afceb8bf2e6f043be2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 15 Oct 2024 09:32:08 -0700 Subject: [PATCH 079/156] Move get_attribute_value into abstract class --- ...lass-image-prioritizer-img-tag-visitor.php | 22 ++++--------------- .../class-image-prioritizer-tag-visitor.php | 22 +++++++++++++++++++ ...ss-image-prioritizer-video-tag-visitor.php | 4 ++-- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php index 80e88075f8..eb1feb0197 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php @@ -40,20 +40,6 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $xpath = $processor->get_xpath(); - /** - * Gets attribute value. - * - * @param string $attribute_name Attribute name. - * @return string|true|null Normalized attribute value. - */ - $get_attribute_value = static function ( string $attribute_name ) use ( $processor ) { - $value = $processor->get_attribute( $attribute_name ); - if ( is_string( $value ) ) { - $value = strtolower( trim( $value, " \t\f\r\n" ) ); - } - return $value; - }; - /* * When the same LCP element is common/shared among all viewport groups, make sure that the element has * fetchpriority=high, even though it won't really be needed because a preload link with fetchpriority=high @@ -61,7 +47,7 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { */ $common_lcp_element = $context->url_metric_group_collection->get_common_lcp_element(); if ( ! is_null( $common_lcp_element ) && $xpath === $common_lcp_element['xpath'] ) { - if ( 'high' === $get_attribute_value( 'fetchpriority' ) ) { + if ( 'high' === $this->get_attribute_value( $processor, 'fetchpriority' ) ) { $processor->set_meta_attribute( 'fetchpriority-already-added', true ); } else { $processor->set_attribute( 'fetchpriority', 'high' ); @@ -92,7 +78,7 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { } else { // Otherwise, make sure visible elements omit the loading attribute, and hidden elements include loading=lazy. $is_visible = $element_max_intersection_ratio > 0.0; - $loading = $get_attribute_value( 'loading' ); + $loading = $this->get_attribute_value( $processor, 'loading' ); if ( $is_visible && 'lazy' === $loading ) { $processor->remove_attribute( 'loading' ); } elseif ( ! $is_visible && 'lazy' !== $loading ) { @@ -104,7 +90,7 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { // Ensure that sizes=auto is set properly. $sizes = $processor->get_attribute( 'sizes' ); if ( is_string( $sizes ) ) { - $is_lazy = 'lazy' === $get_attribute_value( 'loading' ); + $is_lazy = 'lazy' === $this->get_attribute_value( $processor, 'loading' ); $has_auto = $this->sizes_attribute_includes_valid_auto( $sizes ); if ( $is_lazy && ! $has_auto ) { @@ -138,7 +124,7 @@ static function ( string $value ): bool { ) ); - $crossorigin = $get_attribute_value( 'crossorigin' ); + $crossorigin = $this->get_attribute_value( $processor, 'crossorigin' ); if ( null !== $crossorigin ) { $link_attributes['crossorigin'] = 'use-credentials' === $crossorigin ? 'use-credentials' : 'anonymous'; } diff --git a/plugins/image-prioritizer/class-image-prioritizer-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-tag-visitor.php index e0f4d564e7..32104590b9 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-tag-visitor.php @@ -14,6 +14,8 @@ /** * Tag visitor that optimizes image tags. * + * @phpstan-type NormalizedAttributeNames 'fetchpriority'|'loading'|'crossorigin' + * * @since 0.1.0 * @access private */ @@ -36,4 +38,24 @@ abstract public function __invoke( OD_Tag_Visitor_Context $context ): bool; protected function is_data_url( string $url ): bool { return str_starts_with( strtolower( $url ), 'data:' ); } + + /** + * Gets attribute value for select attributes. + * + * @since n.e.x.t + * @todo Move this into the OD_HTML_Tag_Processor/OD_HTML_Processor class eventually. + * + * @phpstan-param NormalizedAttributeNames $attribute_name + * + * @param OD_HTML_Tag_Processor $processor Processor. + * @param string $attribute_name Attribute name. + * @return string|true|null Normalized attribute value. + */ + protected function get_attribute_value( OD_HTML_Tag_Processor $processor, string $attribute_name ) { + $value = $processor->get_attribute( $attribute_name ); + if ( is_string( $value ) ) { + $value = strtolower( trim( $value, " \t\f\r\n" ) ); + } + return $value; + } } diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index 06bafdae0c..50b8fbf7a0 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -41,7 +41,7 @@ final class Image_Prioritizer_Video_Tag_Visitor extends Image_Prioritizer_Tag_Vi * * @param OD_Tag_Visitor_Context $context Tag visitor context. * - * @return bool Whether the visitor visited the tag. + * @return bool Whether the tag should be tracked in URL metrics. */ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $processor = $context->processor; @@ -75,7 +75,7 @@ static function ( string $value ): bool { ) ); - $crossorigin = $processor->get_attribute( 'crossorigin' ); + $crossorigin = $this->get_attribute_value( $processor, 'crossorigin' ); if ( null !== $crossorigin ) { $link_attributes['crossorigin'] = 'use-credentials' === $crossorigin ? 'use-credentials' : 'anonymous'; } From 0f8f82de676b1746b3c55849b05e694aa0f8799a Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 15 Oct 2024 09:41:52 -0700 Subject: [PATCH 080/156] Simplify collection of link attributes --- ...ss-image-prioritizer-video-tag-visitor.php | 24 ++++++------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index 50b8fbf7a0..f2528d5c53 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -58,21 +58,13 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $xpath = $processor->get_xpath(); // If this element is the LCP (for a breakpoint group), add a preload link for it. - foreach ( $context->url_metrics_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) { - $link_attributes = array_merge( - array( - 'rel' => 'preload', - 'fetchpriority' => 'high', - 'as' => 'image', - ), - array_filter( - array( - 'href' => (string) $processor->get_attribute( self::POSTER ), - ), - static function ( string $value ): bool { - return '' !== $value; - } - ) + foreach ( $context->url_metric_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) { + $link_attributes = array( + 'rel' => 'preload', + 'fetchpriority' => 'high', + 'as' => 'image', + 'href' => $poster, + 'media' => 'screen', ); $crossorigin = $this->get_attribute_value( $processor, 'crossorigin' ); @@ -80,8 +72,6 @@ static function ( string $value ): bool { $link_attributes['crossorigin'] = 'use-credentials' === $crossorigin ? 'use-credentials' : 'anonymous'; } - $link_attributes['media'] = 'screen'; - $context->link_collection->add_link( $link_attributes, $group->get_minimum_viewport_width(), From d3f44c86b52258abaf8a2013f2f5bf53445414f5 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 15 Oct 2024 11:25:06 -0700 Subject: [PATCH 081/156] Add tests for video poster optimization --- ...ss-image-prioritizer-video-tag-visitor.php | 2 + ...-poster-lcp-element-on-all-breakpoints.php | 54 ++++++++++ ...ith-poster-lcp-element-on-desktop-only.php | 87 +++++++++++++++ ...t-on-mobile-and-desktop-but-not-tablet.php | 101 ++++++++++++++++++ ...out-poster-lcp-element-on-desktop-only.php | 82 ++++++++++++++ 5 files changed, 326 insertions(+) create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-all-breakpoints.php create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-desktop-only.php create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet.php create mode 100644 plugins/image-prioritizer/tests/test-cases/video-without-poster-lcp-element-on-desktop-only.php diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index f2528d5c53..ce4e298b5e 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -57,6 +57,8 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $xpath = $processor->get_xpath(); + // TODO: If $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath ) is 0.0, then the video is not in any initial viewport and the VIDEO tag could get the preload=none attribute added. + // If this element is the LCP (for a breakpoint group), add a preload link for it. foreach ( $context->url_metric_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) { $link_attributes = array( diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-all-breakpoints.php b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-all-breakpoints.php new file mode 100644 index 0000000000..bec8ff230b --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-all-breakpoints.php @@ -0,0 +1,54 @@ + 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; + } + ); + + foreach ( array_merge( $breakpoint_max_widths, array( 1000 ) ) as $viewport_width ) { + 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' => array( + array( + 'isLCP' => true, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::VIDEO]', + ), + ), + ) + ) + ); + } + }, + 'buffer' => ' + + + + ... + + + + + + ', + 'expected' => ' + + + + ... + + + + + + + + ', +); diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-desktop-only.php b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-desktop-only.php new file mode 100644 index 0000000000..eceac8e3ca --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-desktop-only.php @@ -0,0 +1,87 @@ + 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; + } + ); + + foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) { + $elements = array( + array( + 'isLCP' => true, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + ), + array( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]', + ), + ); + 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' => $non_desktop_viewport_width, + 'elements' => $elements, + ) + ) + ); + } + + 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( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + ), + array( + 'isLCP' => true, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]', + ), + ), + ) + ) + ); + }, + 'buffer' => ' + + + + ... + + + Mobile Logo + + + + ', + 'expected' => ' + + + + ... + + + + + Mobile Logo + + + + + ', +); 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 new file mode 100644 index 0000000000..3b4d59e85c --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet.php @@ -0,0 +1,101 @@ + 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' => 480, + 'elements' => array( + array( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + ), + array( + 'isLCP' => true, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]', + ), + ), + ) + ) + ); + + foreach ( array( 600, 782 ) as $tablet_viewport_width ) { + $elements = array( + array( + 'isLCP' => true, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + ), + array( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]', + ), + ); + 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' => $tablet_viewport_width, + 'elements' => $elements, + ) + ) + ); + } + + 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( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + ), + array( + 'isLCP' => true, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]', + ), + ), + ) + ) + ); + }, + 'buffer' => ' + + + + ... + + + Tablet header + + + + ', + 'expected' => ' + + + + ... + + + + + + Tablet header + + + + + ', +); diff --git a/plugins/image-prioritizer/tests/test-cases/video-without-poster-lcp-element-on-desktop-only.php b/plugins/image-prioritizer/tests/test-cases/video-without-poster-lcp-element-on-desktop-only.php new file mode 100644 index 0000000000..5e5f2cadc8 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-without-poster-lcp-element-on-desktop-only.php @@ -0,0 +1,82 @@ + 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; + } + ); + + foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) { + $elements = array( + array( + 'isLCP' => true, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + ), + array( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[2][self::VIDEO]', + ), + ); + 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' => $non_desktop_viewport_width, + 'elements' => $elements, + ) + ) + ); + } + + 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( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', + ), + ), + ) + ) + ); + }, + 'buffer' => ' + + + + ... + + + Mobile Logo + + + + ', + 'expected' => ' + + + + ... + + + + Mobile Logo + + + + + ', +); From 7d289c26011a2716a5613d226dd4c27e9e8a2cc2 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 15 Oct 2024 11:31:56 -0700 Subject: [PATCH 082/156] Remove single-use constants --- ...ass-image-prioritizer-video-tag-visitor.php | 18 ++---------------- 1 file changed, 2 insertions(+), 16 deletions(-) diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index ce4e298b5e..152489f629 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -22,20 +22,6 @@ */ final class Image_Prioritizer_Video_Tag_Visitor extends Image_Prioritizer_Tag_Visitor { - /** - * Video tag. - * - * @var string - */ - const VIDEO = 'VIDEO'; - - /** - * Poster attribute. - * - * @var string - */ - const POSTER = 'poster'; - /** * Visits a tag. * @@ -45,12 +31,12 @@ final class Image_Prioritizer_Video_Tag_Visitor extends Image_Prioritizer_Tag_Vi */ public function __invoke( OD_Tag_Visitor_Context $context ): bool { $processor = $context->processor; - if ( self::VIDEO !== $processor->get_tag() ) { + if ( 'VIDEO' !== $processor->get_tag() ) { return false; } // Skip empty poster attributes and data: URLs. - $poster = trim( (string) $processor->get_attribute( self::POSTER ) ); + $poster = trim( (string) $processor->get_attribute( 'poster' ) ); if ( '' === $poster || $this->is_data_url( $poster ) ) { return false; } From b7e95c8e8d188a91bff7b0c3aa6c9826731a9f33 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 15 Oct 2024 11:34:25 -0700 Subject: [PATCH 083/156] Improve uniqueness of tag visitor IDs --- plugins/image-prioritizer/helper.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/image-prioritizer/helper.php b/plugins/image-prioritizer/helper.php index 4f7d9cf4c2..32461c163a 100644 --- a/plugins/image-prioritizer/helper.php +++ b/plugins/image-prioritizer/helper.php @@ -59,7 +59,7 @@ function image_prioritizer_render_generator_meta_tag(): void { } /** - * Registers tag visitors for images. + * Registers tag visitors. * * @since 0.1.0 * @@ -68,11 +68,11 @@ function image_prioritizer_render_generator_meta_tag(): void { function image_prioritizer_register_tag_visitors( OD_Tag_Visitor_Registry $registry ): void { // Note: The class is invocable (it has an __invoke() method). $img_visitor = new Image_Prioritizer_Img_Tag_Visitor(); - $registry->register( 'img-tags', $img_visitor ); + $registry->register( 'image-prioritizer/img', $img_visitor ); $bg_image_visitor = new Image_Prioritizer_Background_Image_Styled_Tag_Visitor(); - $registry->register( 'bg-image-tags', $bg_image_visitor ); + $registry->register( 'image-prioritizer/background-image', $bg_image_visitor ); $video_visitor = new Image_Prioritizer_Video_Tag_Visitor(); - $registry->register( 'video-tags', $video_visitor ); + $registry->register( 'image-prioritizer/video', $video_visitor ); } From 1ee41504581e3b70c23598783d01d6e235a1c9bb Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 15 Oct 2024 11:48:25 -0700 Subject: [PATCH 084/156] Remove comment about only running changed files through tsc --- lint-staged.config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lint-staged.config.js b/lint-staged.config.js index a52deabb77..225d3ac95f 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -28,7 +28,7 @@ module.exports = { '**/*.{js,ts}': ( files ) => { return [ `npm run lint-js -- ${ joinFiles( files ) }`, - `npm run tsc`, // TODO: How to pass joinFiles( files ) here? + `npm run tsc`, ]; }, '**/*.php': ( files ) => { From d9e02cb0b688c071306f0c4a370bb035c81984d9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 15 Oct 2024 11:50:47 -0700 Subject: [PATCH 085/156] Apply prettier fix --- lint-staged.config.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/lint-staged.config.js b/lint-staged.config.js index 225d3ac95f..362534e74a 100644 --- a/lint-staged.config.js +++ b/lint-staged.config.js @@ -26,10 +26,7 @@ const PLUGIN_BASE_NAME = path.basename( __dirname ); module.exports = { '**/*.{js,ts}': ( files ) => { - return [ - `npm run lint-js -- ${ joinFiles( files ) }`, - `npm run tsc`, - ]; + return [ `npm run lint-js -- ${ joinFiles( files ) }`, `npm run tsc` ]; }, '**/*.php': ( files ) => { const commands = [ 'composer phpstan' ]; From 38f2e70ed018a48f51792d38e71e925f0ae060f9 Mon Sep 17 00:00:00 2001 From: Weston Ruter Date: Tue, 15 Oct 2024 12:42:48 -0700 Subject: [PATCH 086/156] Combine width and height into a single translation string placeholder --- plugins/optimization-detective/class-od-url-metric.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/plugins/optimization-detective/class-od-url-metric.php b/plugins/optimization-detective/class-od-url-metric.php index 797343b395..e5743048bc 100644 --- a/plugins/optimization-detective/class-od-url-metric.php +++ b/plugins/optimization-detective/class-od-url-metric.php @@ -96,13 +96,12 @@ private function prepare_data( array $data ): array { throw new OD_Data_Validation_Exception( esc_html( sprintf( - /* translators: 1: current aspect ratio, 2: minimum aspect ratio, 3: maximum aspect ratio, 4: viewport width, 5: viewport height */ - __( 'Viewport aspect ratio (%1$s) is not in the accepted range of %2$s to %3$s. Viewport dimensions: %4$sx%5$s', 'optimization-detective' ), + /* translators: 1: current aspect ratio, 2: minimum aspect ratio, 3: maximum aspect ratio, 4: viewport dimensions */ + __( 'Viewport aspect ratio (%1$s) is not in the accepted range of %2$s to %3$s. Viewport dimensions: %4$s', 'optimization-detective' ), $aspect_ratio, $min_aspect_ratio, $max_aspect_ratio, - $data['viewport']['width'], - $data['viewport']['height'] + $data['viewport']['width'] . 'x' . $data['viewport']['height'] ) ) ); From 906f5f78681bb77eee202131ee0f8ae1daffa886 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 16 Oct 2024 12:38:26 +0200 Subject: [PATCH 087/156] Fix typo --- plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php index f4b98b14ae..e3b4e9c077 100644 --- a/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php +++ b/plugins/embed-optimizer/class-embed-optimizer-tag-visitor.php @@ -208,7 +208,7 @@ private function reduce_layout_shifts( OD_Tag_Visitor_Context $context ): void { $embed_wrapper_xpath = self::get_embed_wrapper_xpath( $processor->get_xpath() ); /** - * Collection of the minimum heights for the element with each group keyed by the minimum viewport with. + * Collection of the minimum heights for the element with each group keyed by the minimum viewport width. * * @var array $minimums */ From 7a9d556c64bccb6754b1fb6dffb3191ba543dd24 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 16 Oct 2024 12:38:48 +0200 Subject: [PATCH 088/156] Add new method for reducing poster image size --- ...ss-image-prioritizer-video-tag-visitor.php | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index 152489f629..578d6d8aa6 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -41,6 +41,8 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { return false; } + $this->reduce_poster_image_size( $context ); + $xpath = $processor->get_xpath(); // TODO: If $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath ) is 0.0, then the video is not in any initial viewport and the VIDEO tag could get the preload=none attribute added. @@ -69,4 +71,32 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { return true; } + + /** + * Reduce poster image size by choosing one that fits the maximum video size more closely. + * + * @since n.e.x.t + * + * @param OD_Tag_Visitor_Context $context Tag visitor context, with the cursor currently at an embed block. + */ + private function reduce_poster_image_size( OD_Tag_Visitor_Context $context ): void { + $processor = $context->processor; + $xpath = $processor->get_xpath(); + + $max_element_width = 0; + + $denormalized_elements = $context->url_metric_group_collection->get_all_denormalized_elements()[ $xpath ] ?? array(); + + foreach ( $denormalized_elements as list( , , $element ) ) { + $max_element_width = max( $max_element_width, $element['boundingClientRect']['width'] ?? 0 ); + } + + $poster = trim( (string) $processor->get_attribute( 'poster' ) ); + $poster_id = attachment_url_to_postid( $poster ); + + if ( $poster_id > 0 && $max_element_width > 0 ) { + $smaller_image_url = wp_get_attachment_image_url( $poster_id, array( (int) $max_element_width, 0 ) ); + $processor->set_attribute( 'poster', $smaller_image_url ); + } + } } From c2b20266efef9677f5c980c1f1a45e5082390b25 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 16 Oct 2024 12:58:33 +0200 Subject: [PATCH 089/156] Add test case --- .../test-cases/video-with-large-poster.php | 101 ++++++++++++++++++ .../image-prioritizer/tests/test-helper.php | 11 +- 2 files changed, 110 insertions(+), 2 deletions(-) create mode 100644 plugins/image-prioritizer/tests/test-cases/video-with-large-poster.php diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-large-poster.php b/plugins/image-prioritizer/tests/test-cases/video-with-large-poster.php new file mode 100644 index 0000000000..c0f5e3c897 --- /dev/null +++ b/plugins/image-prioritizer/tests/test-cases/video-with-large-poster.php @@ -0,0 +1,101 @@ + static function ( Test_Image_Prioritizer_Helper $test_case, WP_UnitTest_Factory $factory ) use( &$full_url, &$expected_url ): void { + $breakpoint_max_widths = array( 480, 600, 782 ); + + add_filter( + 'od_breakpoint_max_widths', + static function () use ( $breakpoint_max_widths ) { + return $breakpoint_max_widths; + } + ); + + foreach ( $breakpoint_max_widths as $non_desktop_viewport_width ) { + $elements = array( + array( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::VIDEO]', + 'boundingClientRect' => $test_case->get_sample_dom_rect(), + ), + ); + 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' => $non_desktop_viewport_width, + 'elements' => $elements, + ) + ) + ); + } + + 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( + 'isLCP' => false, + 'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::VIDEO]', + 'boundingClientRect' => $test_case->get_sample_dom_rect(), + ), + ), + ) + ) + ); + + $attachment_id = $factory->attachment->create_object( + DIR_TESTDATA . '/images/33772.jpg', + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + wp_generate_attachment_metadata( $attachment_id, DIR_TESTDATA . '/images/33772.jpg' ); + + $dom_rect = $test_case->get_sample_dom_rect(); + + $full_url = wp_get_attachment_url( $attachment_id ); + $expected_url = wp_get_attachment_image_url( $attachment_id, array( (int) $dom_rect['width'], 0 ) ); + }, + 'buffer' => static function () use ( &$full_url ) { + return " + + + + ... + + + + + + "; + }, + 'expected' => static function () use ( &$full_url, &$expected_url ) { + return " + + + + ... + + + + + + + "; + }, +); diff --git a/plugins/image-prioritizer/tests/test-helper.php b/plugins/image-prioritizer/tests/test-helper.php index c58bdaff07..8c286b31d0 100644 --- a/plugins/image-prioritizer/tests/test-helper.php +++ b/plugins/image-prioritizer/tests/test-helper.php @@ -79,16 +79,23 @@ public function data_provider_test_filter_tag_visitors(): array { * @covers Image_Prioritizer_Background_Image_Styled_Tag_Visitor * * @dataProvider data_provider_test_filter_tag_visitors + * + * @param callable $set_up Setup function. + * @param callable|string $buffer Content before. + * @param callable|string $expected Expected content after. */ - public function test_image_prioritizer_register_tag_visitors( Closure $set_up, string $buffer, string $expected ): void { - $set_up( $this ); + public function test_image_prioritizer_register_tag_visitors( Closure $set_up, $buffer, $expected ): void { + $set_up( $this, $this::factory() ); + $buffer = is_string( $buffer ) ? $buffer : $buffer(); $buffer = preg_replace( '::s', '', od_optimize_template_output_buffer( $buffer ) ); + $expected = is_string( $expected ) ? $expected : $expected(); + $this->assertEquals( $this->remove_initial_tabs( $expected ), $this->remove_initial_tabs( $buffer ), From e1276850e64e84bb04600be91990b697636ab85e Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 16 Oct 2024 13:09:21 +0200 Subject: [PATCH 090/156] Lint fixes --- .../tests/test-cases/video-with-large-poster.php | 6 +++--- plugins/image-prioritizer/tests/test-helper.php | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-large-poster.php b/plugins/image-prioritizer/tests/test-cases/video-with-large-poster.php index c0f5e3c897..3f8a895a6e 100644 --- a/plugins/image-prioritizer/tests/test-cases/video-with-large-poster.php +++ b/plugins/image-prioritizer/tests/test-cases/video-with-large-poster.php @@ -1,10 +1,10 @@ static function ( Test_Image_Prioritizer_Helper $test_case, WP_UnitTest_Factory $factory ) use( &$full_url, &$expected_url ): void { + 'set_up' => static function ( Test_Image_Prioritizer_Helper $test_case, WP_UnitTest_Factory $factory ) use ( &$full_url, &$expected_url ): void { $breakpoint_max_widths = array( 480, 600, 782 ); add_filter( @@ -65,7 +65,7 @@ static function () use ( $breakpoint_max_widths ) { $full_url = wp_get_attachment_url( $attachment_id ); $expected_url = wp_get_attachment_image_url( $attachment_id, array( (int) $dom_rect['width'], 0 ) ); }, - 'buffer' => static function () use ( &$full_url ) { + 'buffer' => static function () use ( &$full_url ) { return " diff --git a/plugins/image-prioritizer/tests/test-helper.php b/plugins/image-prioritizer/tests/test-helper.php index 8c286b31d0..8919154e2f 100644 --- a/plugins/image-prioritizer/tests/test-helper.php +++ b/plugins/image-prioritizer/tests/test-helper.php @@ -84,7 +84,7 @@ public function data_provider_test_filter_tag_visitors(): array { * @param callable|string $buffer Content before. * @param callable|string $expected Expected content after. */ - public function test_image_prioritizer_register_tag_visitors( Closure $set_up, $buffer, $expected ): void { + public function test_image_prioritizer_register_tag_visitors( callable $set_up, $buffer, $expected ): void { $set_up( $this, $this::factory() ); $buffer = is_string( $buffer ) ? $buffer : $buffer(); From ca0f538a9c662c4e6f62a8c513fa2cf7b41cfe87 Mon Sep 17 00:00:00 2001 From: Pascal Birchler Date: Wed, 16 Oct 2024 14:43:05 +0200 Subject: [PATCH 091/156] Extract other logic to own method too --- ...ss-image-prioritizer-video-tag-visitor.php | 86 ++++++++++++------- 1 file changed, 53 insertions(+), 33 deletions(-) diff --git a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php index 578d6d8aa6..01cf7e1c10 100644 --- a/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php +++ b/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php @@ -35,39 +35,10 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { return false; } - // Skip empty poster attributes and data: URLs. - $poster = trim( (string) $processor->get_attribute( 'poster' ) ); - if ( '' === $poster || $this->is_data_url( $poster ) ) { - return false; - } - - $this->reduce_poster_image_size( $context ); - - $xpath = $processor->get_xpath(); - // TODO: If $context->url_metric_group_collection->get_element_max_intersection_ratio( $xpath ) is 0.0, then the video is not in any initial viewport and the VIDEO tag could get the preload=none attribute added. - // If this element is the LCP (for a breakpoint group), add a preload link for it. - foreach ( $context->url_metric_group_collection->get_groups_by_lcp_element( $xpath ) as $group ) { - $link_attributes = array( - 'rel' => 'preload', - 'fetchpriority' => 'high', - 'as' => 'image', - 'href' => $poster, - 'media' => 'screen', - ); - - $crossorigin = $this->get_attribute_value( $processor, 'crossorigin' ); - if ( null !== $crossorigin ) { - $link_attributes['crossorigin'] = 'use-credentials' === $crossorigin ? 'use-credentials' : 'anonymous'; - } - - $context->link_collection->add_link( - $link_attributes, - $group->get_minimum_viewport_width(), - $group->get_maximum_viewport_width() - ); - } + $this->reduce_poster_image_size( $context ); + $this->preload_poster_image( $context ); return true; } @@ -81,7 +52,14 @@ public function __invoke( OD_Tag_Visitor_Context $context ): bool { */ private function reduce_poster_image_size( OD_Tag_Visitor_Context $context ): void { $processor = $context->processor; - $xpath = $processor->get_xpath(); + + // Skip empty poster attributes and data: URLs. + $poster = trim( (string) $processor->get_attribute( 'poster' ) ); + if ( '' === $poster || $this->is_data_url( $poster ) ) { + return; + } + + $xpath = $processor->get_xpath(); $max_element_width = 0; @@ -91,7 +69,6 @@ private function reduce_poster_image_size( OD_Tag_Visitor_Context $context ): vo $max_element_width = max( $max_element_width, $element['boundingClientRect']['width'] ?? 0 ); } - $poster = trim( (string) $processor->get_attribute( 'poster' ) ); $poster_id = attachment_url_to_postid( $poster ); if ( $poster_id > 0 && $max_element_width > 0 ) { @@ -99,4 +76,47 @@ private function reduce_poster_image_size( OD_Tag_Visitor_Context $context ): vo $processor->set_attribute( 'poster', $smaller_image_url ); } } + + /** + * Preload poster image for the LCP