Skip to content

Conversation

@westonruter
Copy link
Member

@westonruter westonruter commented Jun 5, 2024

  • Update the readmes with further refinements for the two plugins.
  • Run npm run since

Pending Release Diffs

image-prioritizer

svn status:

?       class-image-prioritizer-background-image-styled-tag-visitor.php
?       class-image-prioritizer-img-tag-visitor.php
?       class-image-prioritizer-tag-visitor.php
?       helper.php
?       hooks.php
?       load.php
?       readme.txt
svn diff

optimization-detective

svn status:

M       class-od-html-tag-walker.php
?       class-od-preload-link-collection.php
?       class-od-tag-visitor-registry.php
M       class-od-url-metrics-group-collection.php
M       class-od-url-metrics-group.php
M       load.php
M       optimization.php
M       readme.txt
M       storage/class-od-storage-lock.php
M       storage/class-od-url-metrics-post-type.php
M       storage/data.php
M       storage/rest-api.php
svn diff
Index: class-od-html-tag-walker.php
===================================================================
--- class-od-html-tag-walker.php	(revision 3098155)
+++ class-od-html-tag-walker.php	(working copy)
@@ -27,7 +27,7 @@
 	 *
 	 * @link https://html.spec.whatwg.org/multipage/syntax.html#void-elements
 	 * @see WP_HTML_Processor::is_void()
-	 * @todo Reuse `WP_HTML_Processor::is_void()` once WordPress 6.4 is the minimum-supported version.
+	 * @todo Reuse `WP_HTML_Processor::is_void()` once WordPress 6.5 is the minimum-supported version. See <https://github.com/WordPress/performance/pull/1115>.
 	 *
 	 * @var string[]
 	 */
@@ -170,6 +170,23 @@
 	private $processor;
 
 	/**
+	 * XPath for the current tag.
+	 *
+	 * This is used so that repeated calls to {@see self::get_xpath()} won't needlessly reconstruct the string. This
+	 * gets cleared whenever {@see self::open_tags()} iterates to the next tag.
+	 *
+	 * @var string|null
+	 */
+	private $current_xpath = null;
+
+	/**
+	 * Whether walking has started.
+	 *
+	 * @var bool
+	 */
+	private $did_start_walking = false;
+
+	/**
 	 * Constructor.
 	 *
 	 * @param string $html HTML to process.
@@ -187,8 +204,15 @@
 	 * @since 0.1.0
 	 *
 	 * @return Generator<string> Tag name of current open tag.
+	 *
+	 * @throws Exception When walking has already started.
 	 */
 	public function open_tags(): Generator {
+		if ( $this->did_start_walking ) {
+			throw new Exception( esc_html__( 'Open tags may only be iterated over once per instance.', 'optimization-detective' ) );
+		}
+		$this->did_start_walking = true;
+
 		$p = $this->processor;
 
 		/*
@@ -237,6 +261,8 @@
 					++$this->open_stack_indices[ $level ];
 				}
 
+				$this->current_xpath = null; // Clear cache.
+
 				// Now that the breadcrumbs are constructed, yield the tag name so that they can be queried if desired.
 				// Other mutations may be performed to the open tag's attributes by the callee at this point as well.
 				yield $tag_name;
@@ -339,11 +365,13 @@
 	 * @return string XPath.
 	 */
 	public function get_xpath(): string {
-		$xpath = '';
-		foreach ( $this->get_breadcrumbs() as list( $tag_name, $index ) ) {
-			$xpath .= sprintf( '/*[%d][self::%s]', $index + 1, $tag_name );
+		if ( null === $this->current_xpath ) {
+			$this->current_xpath = '';
+			foreach ( $this->get_breadcrumbs() as list( $tag_name, $index ) ) {
+				$this->current_xpath .= sprintf( '/*[%d][self::%s]', $index + 1, $tag_name );
+			}
 		}
-		return $xpath;
+		return $this->current_xpath;
 	}
 
 	/**
@@ -381,6 +409,21 @@
 	}
 
 	/**
+	 * Returns the uppercase name of the matched tag.
+	 *
+	 * This is a wrapper around the underlying WP_HTML_Tag_Processor method of the same name since only a limited number of
+	 * methods can be exposed to prevent moving the pointer in such a way as the breadcrumb calculation is invalidated.
+	 *
+	 * @since 0.3.0
+	 * @see WP_HTML_Tag_Processor::get_tag()
+	 *
+	 * @return string|null Name of currently matched tag in input HTML, or `null` if none found.
+	 */
+	public function get_tag(): ?string {
+		return $this->processor->get_tag();
+	}
+
+	/**
 	 * Returns the value of a requested attribute from a matched tag opener if that attribute exists.
 	 *
 	 * This is a wrapper around the underlying WP_HTML_Tag_Processor method of the same name since only a limited number of
@@ -410,10 +453,32 @@
 	 * @return bool Whether an attribute value was set.
 	 */
 	public function set_attribute( string $name, $value ): bool {
-		return $this->processor->set_attribute( $name, $value );
+		$existing_value = $this->processor->get_attribute( $name );
+		$result         = $this->processor->set_attribute( $name, $value );
+		if ( $result ) {
+			if ( is_string( $existing_value ) ) {
+				$this->set_meta_attribute( "replaced-{$name}", $existing_value );
+			} else {
+				$this->set_meta_attribute( "added-{$name}", true );
+			}
+		}
+		return $result;
 	}
 
 	/**
+	 * Sets a meta attribute.
+	 *
+	 * All meta attributes are prefixed with 'data-od-'.
+	 *
+	 * @param string      $name  Meta attribute name.
+	 * @param string|true $value Value.
+	 * @return bool Whether an attribute was set.
+	 */
+	public function set_meta_attribute( string $name, $value ): bool {
+		return $this->processor->set_attribute( "data-od-{$name}", $value );
+	}
+
+	/**
 	 * Removes an attribute from the currently-matched tag.
 	 *
 	 * This is a wrapper around the underlying WP_HTML_Tag_Processor method of the same name since only a limited number of
@@ -426,7 +491,12 @@
 	 * @return bool Whether an attribute was removed.
 	 */
 	public function remove_attribute( string $name ): bool {
-		return $this->processor->remove_attribute( $name );
+		$old_value = $this->processor->get_attribute( $name );
+		$result    = $this->processor->remove_attribute( $name );
+		if ( $result ) {
+			$this->set_meta_attribute( "removed-{$name}", is_string( $old_value ) ? $old_value : true );
+		}
+		return $result;
 	}
 
 	/**
Index: class-od-url-metrics-group-collection.php
===================================================================
--- class-od-url-metrics-group-collection.php	(revision 3098155)
+++ class-od-url-metrics-group-collection.php	(working copy)
@@ -14,6 +14,8 @@
 /**
  * Collection of URL groups according to the breakpoints.
  *
+ * @phpstan-import-type ElementData from OD_URL_Metric
+ *
  * @implements IteratorAggregate<int, OD_URL_Metrics_Group>
  *
  * @since 0.1.0
@@ -69,6 +71,20 @@
 	private $freshness_ttl;
 
 	/**
+	 * Result cache.
+	 *
+	 * @var array{
+	 *          get_group_for_viewport_width?: array<int, OD_URL_Metrics_Group>,
+	 *          is_every_group_populated?: bool,
+	 *          is_every_group_complete?: bool,
+	 *          get_groups_by_lcp_element?: array<string, OD_URL_Metrics_Group[]>,
+	 *          get_common_lcp_element?: ElementData|null,
+	 *          get_all_element_max_intersection_ratios?: array<string, float>
+	 *      }
+	 */
+	private $result_cache = array();
+
+	/**
 	 * Constructor.
 	 *
 	 * @throws InvalidArgumentException When an invalid argument is supplied.
@@ -141,6 +157,13 @@
 	}
 
 	/**
+	 * Clear result cache.
+	 */
+	public function clear_cache(): void {
+		$this->result_cache = array();
+	}
+
+	/**
 	 * Create groups.
 	 *
 	 * @phpstan-return non-empty-array<OD_URL_Metrics_Group>
@@ -151,10 +174,10 @@
 		$groups    = array();
 		$min_width = 0;
 		foreach ( $this->breakpoints as $max_width ) {
-			$groups[]  = new OD_URL_Metrics_Group( array(), $min_width, $max_width, $this->sample_size, $this->freshness_ttl );
+			$groups[]  = new OD_URL_Metrics_Group( array(), $min_width, $max_width, $this->sample_size, $this->freshness_ttl, $this );
 			$min_width = $max_width + 1;
 		}
-		$groups[] = new OD_URL_Metrics_Group( array(), $min_width, PHP_INT_MAX, $this->sample_size, $this->freshness_ttl );
+		$groups[] = new OD_URL_Metrics_Group( array(), $min_width, PHP_INT_MAX, $this->sample_size, $this->freshness_ttl, $this );
 		return $groups;
 	}
 
@@ -188,20 +211,29 @@
 	 * @return OD_URL_Metrics_Group URL metrics group for the viewport width.
 	 */
 	public function get_group_for_viewport_width( int $viewport_width ): OD_URL_Metrics_Group {
-		foreach ( $this->groups as $group ) {
-			if ( $group->is_viewport_width_in_range( $viewport_width ) ) {
-				return $group;
+		if ( array_key_exists( __FUNCTION__, $this->result_cache ) && array_key_exists( $viewport_width, $this->result_cache[ __FUNCTION__ ] ) ) {
+			return $this->result_cache[ __FUNCTION__ ][ $viewport_width ];
+		}
+
+		$result = ( function () use ( $viewport_width ) {
+			foreach ( $this->groups as $group ) {
+				if ( $group->is_viewport_width_in_range( $viewport_width ) ) {
+					return $group;
+				}
 			}
-		}
-		throw new InvalidArgumentException(
-			esc_html(
-				sprintf(
+			throw new InvalidArgumentException(
+				esc_html(
+					sprintf(
 					/* translators: %d is viewport width */
-					__( 'No URL metrics group found for viewport width: %d', 'optimization-detective' ),
-					$viewport_width
+						__( 'No URL metrics group found for viewport width: %d', 'optimization-detective' ),
+						$viewport_width
+					)
 				)
-			)
-		);
+			);
+		} )();
+
+		$this->result_cache[ __FUNCTION__ ][ $viewport_width ] = $result;
+		return $result;
 	}
 
 	/**
@@ -217,12 +249,21 @@
 	 * @return bool Whether all groups have some URL metrics.
 	 */
 	public function is_every_group_populated(): bool {
-		foreach ( $this->groups as $group ) {
-			if ( count( $group ) === 0 ) {
-				return false;
+		if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) {
+			return $this->result_cache[ __FUNCTION__ ];
+		}
+
+		$result = ( function () {
+			foreach ( $this->groups as $group ) {
+				if ( count( $group ) === 0 ) {
+					return false;
+				}
 			}
-		}
-		return true;
+			return true;
+		} )();
+
+		$this->result_cache[ __FUNCTION__ ] = $result;
+		return $result;
 	}
 
 	/**
@@ -233,15 +274,149 @@
 	 * @return bool Whether all groups are complete.
 	 */
 	public function is_every_group_complete(): bool {
-		foreach ( $this->groups as $group ) {
-			if ( ! $group->is_complete() ) {
-				return false;
+		if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) {
+			return $this->result_cache[ __FUNCTION__ ];
+		}
+
+		$result = ( function () {
+			foreach ( $this->groups as $group ) {
+				if ( ! $group->is_complete() ) {
+					return false;
+				}
 			}
+
+			return true;
+		} )();
+
+		$this->result_cache[ __FUNCTION__ ] = $result;
+		return $result;
+	}
+
+	/**
+	 * Gets the groups with the provided LCP element XPath.
+	 *
+	 * @see OD_URL_Metrics_Group::get_lcp_element()
+	 *
+	 * @param string $xpath XPath for LCP element.
+	 * @return OD_URL_Metrics_Group[] Groups which have the LCP element.
+	 */
+	public function get_groups_by_lcp_element( string $xpath ): array {
+		if ( array_key_exists( __FUNCTION__, $this->result_cache ) && array_key_exists( $xpath, $this->result_cache[ __FUNCTION__ ] ) ) {
+			return $this->result_cache[ __FUNCTION__ ][ $xpath ];
 		}
-		return true;
+
+		$result = ( function () use ( $xpath ) {
+			$groups = array();
+			foreach ( $this->groups as $group ) {
+				$lcp_element = $group->get_lcp_element();
+				if ( ! is_null( $lcp_element ) && $xpath === $lcp_element['xpath'] ) {
+					$groups[] = $group;
+				}
+			}
+
+			return $groups;
+		} )();
+
+		$this->result_cache[ __FUNCTION__ ][ $xpath ] = $result;
+		return $result;
 	}
 
 	/**
+	 * Gets common LCP element.
+	 *
+	 * @return ElementData|null
+	 */
+	public function get_common_lcp_element(): ?array {
+		if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) {
+			return $this->result_cache[ __FUNCTION__ ];
+		}
+
+		$result = ( function () {
+
+			// If every group isn't populated, then we can't say whether there is a common LCP element across every viewport group.
+			if ( ! $this->is_every_group_populated() ) {
+				return null;
+			}
+
+			// Look at the LCP elements across all the viewport groups.
+			$groups_by_lcp_element_xpath   = array();
+			$lcp_elements_by_xpath         = array();
+			$group_has_unknown_lcp_element = false;
+			foreach ( $this->groups as $group ) {
+				$lcp_element = $group->get_lcp_element();
+				if ( ! is_null( $lcp_element ) ) {
+					$groups_by_lcp_element_xpath[ $lcp_element['xpath'] ][] = $group;
+					$lcp_elements_by_xpath[ $lcp_element['xpath'] ][]       = $lcp_element;
+				} else {
+					$group_has_unknown_lcp_element = true;
+				}
+			}
+
+			if (
+				// All breakpoints share the same LCP element.
+				1 === count( $groups_by_lcp_element_xpath )
+				&&
+				// The breakpoints don't share a common lack of a detected LCP element.
+				! $group_has_unknown_lcp_element
+			) {
+				$xpath = key( $lcp_elements_by_xpath );
+
+				return $lcp_elements_by_xpath[ $xpath ][0];
+			}
+
+			return null;
+		} )();
+
+		$this->result_cache[ __FUNCTION__ ] = $result;
+		return $result;
+	}
+
+	/**
+	 * Gets the max intersection ratios of all elements across all groups and their captured URL metrics.
+	 *
+	 * @return array<string, float> Keys are XPaths and values are the intersection ratios.
+	 */
+	public function get_all_element_max_intersection_ratios(): array {
+		if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) {
+			return $this->result_cache[ __FUNCTION__ ];
+		}
+
+		$result = ( function () {
+			$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'];
+					}
+				}
+			}
+			return $element_max_intersection_ratios;
+		} )();
+
+		$this->result_cache[ __FUNCTION__ ] = $result;
+		return $result;
+	}
+
+	/**
+	 * Gets the max intersection ratio of an element across all groups and their captured URL metrics.
+	 *
+	 * @param string $xpath XPath for the element.
+	 * @return float|null Max intersection ratio of null if tag is unknown (not captured).
+	 */
+	public function get_element_max_intersection_ratio( string $xpath ): ?float {
+		return $this->get_all_element_max_intersection_ratios()[ $xpath ] ?? null;
+	}
+
+	/**
 	 * Gets URL metrics from all groups flattened into one list.
 	 *
 	 * @return OD_URL_Metric[] All URL metrics.
Index: class-od-url-metrics-group.php
===================================================================
--- class-od-url-metrics-group.php	(revision 3098155)
+++ class-od-url-metrics-group.php	(working copy)
@@ -15,6 +15,7 @@
  * URL metrics grouped by viewport according to breakpoints.
  *
  * @implements IteratorAggregate<int, OD_URL_Metric>
+ * @phpstan-import-type ElementData from OD_URL_Metric
  *
  * @since 0.1.0
  * @access private
@@ -61,17 +62,35 @@
 	private $freshness_ttl;
 
 	/**
+	 * Collection that this instance belongs to.
+	 *
+	 * @var OD_URL_Metrics_Group_Collection|null
+	 */
+	private $collection;
+
+	/**
+	 * Result cache.
+	 *
+	 * @var array{
+	 *          get_lcp_element?: ElementData|null,
+	 *          is_complete?: bool
+	 *      }
+	 */
+	private $result_cache = array();
+
+	/**
 	 * Constructor.
 	 *
 	 * @throws InvalidArgumentException If arguments are valid.
 	 *
-	 * @param OD_URL_Metric[] $url_metrics            URL metrics to add to the group.
-	 * @param int             $minimum_viewport_width Minimum possible viewport width for the group. Must be zero or greater.
-	 * @param int             $maximum_viewport_width Maximum possible viewport width for the group. Must be greater than zero and the minimum viewport width.
-	 * @param int             $sample_size            Sample size for the maximum number of viewports in a group between breakpoints.
-	 * @param int             $freshness_ttl          Freshness age (TTL) for a given URL metric.
+	 * @param OD_URL_Metric[]                      $url_metrics            URL metrics to add to the group.
+	 * @param int                                  $minimum_viewport_width Minimum possible viewport width for the group. Must be zero or greater.
+	 * @param int                                  $maximum_viewport_width Maximum possible viewport width for the group. Must be greater than zero and the minimum viewport width.
+	 * @param int                                  $sample_size            Sample size for the maximum number of viewports in a group between breakpoints.
+	 * @param int                                  $freshness_ttl          Freshness age (TTL) for a given URL metric.
+	 * @param OD_URL_Metrics_Group_Collection|null $collection             Collection that this instance belongs to. Optional.
 	 */
-	public function __construct( array $url_metrics, int $minimum_viewport_width, int $maximum_viewport_width, int $sample_size, int $freshness_ttl ) {
+	public function __construct( array $url_metrics, int $minimum_viewport_width, int $maximum_viewport_width, int $sample_size, int $freshness_ttl, ?OD_URL_Metrics_Group_Collection $collection = null ) {
 		if ( $minimum_viewport_width < 0 ) {
 			throw new InvalidArgumentException(
 				esc_html__( 'The minimum viewport width must be at least zero.', 'optimization-detective' )
@@ -116,6 +135,10 @@
 		}
 		$this->freshness_ttl = $freshness_ttl;
 
+		if ( ! is_null( $collection ) ) {
+			$this->collection = $collection;
+		}
+
 		$this->url_metrics = $url_metrics;
 	}
 
@@ -122,7 +145,7 @@
 	/**
 	 * Gets the minimum possible viewport width (inclusive).
 	 *
-	 * @return int Minimum viewport width.
+	 * @return int<0, max> Minimum viewport width.
 	 */
 	public function get_minimum_viewport_width(): int {
 		return $this->minimum_viewport_width;
@@ -131,7 +154,7 @@
 	/**
 	 * Gets the maximum possible viewport width (inclusive).
 	 *
-	 * @return int Minimum viewport width.
+	 * @return int<1, max> Minimum viewport width.
 	 */
 	public function get_maximum_viewport_width(): int {
 		return $this->maximum_viewport_width;
@@ -164,6 +187,11 @@
 			);
 		}
 
+		$this->result_cache = array();
+		if ( ! is_null( $this->collection ) ) {
+			$this->collection->clear_cache();
+		}
+
 		$this->url_metrics[] = $url_metric;
 
 		// If we have too many URL metrics now, remove the oldest ones up to the sample size.
@@ -191,16 +219,103 @@
 	 * @return bool Whether complete.
 	 */
 	public function is_complete(): bool {
-		if ( count( $this->url_metrics ) < $this->sample_size ) {
-			return false;
+		if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) {
+			return $this->result_cache[ __FUNCTION__ ];
 		}
-		$current_time = microtime( true );
-		foreach ( $this->url_metrics as $url_metric ) {
-			if ( $current_time > $url_metric->get_timestamp() + $this->freshness_ttl ) {
+
+		$result = ( function () {
+			if ( count( $this->url_metrics ) < $this->sample_size ) {
 				return false;
 			}
+			$current_time = microtime( true );
+			foreach ( $this->url_metrics as $url_metric ) {
+				if ( $current_time > $url_metric->get_timestamp() + $this->freshness_ttl ) {
+					return false;
+				}
+			}
+
+			return true;
+		} )();
+
+		$this->result_cache[ __FUNCTION__ ] = $result;
+		return $result;
+	}
+
+	/**
+	 * Gets the LCP element in the viewport group.
+	 *
+	 * @return ElementData|null LCP element data or null if not available, either because there are no URL metrics or
+	 *                          the LCP element type is not supported.
+	 */
+	public function get_lcp_element(): ?array {
+		if ( array_key_exists( __FUNCTION__, $this->result_cache ) ) {
+			return $this->result_cache[ __FUNCTION__ ];
 		}
-		return true;
+
+		$result = ( function () {
+
+			// No metrics have been gathered for this group so there is no LCP element.
+			if ( count( $this->url_metrics ) === 0 ) {
+				return null;
+			}
+
+			// The following arrays all share array indices.
+
+			/**
+			 * Seen breadcrumbs counts.
+			 *
+			 * @var array<int, string> $seen_breadcrumbs
+			 */
+			$seen_breadcrumbs = array();
+
+			/**
+			 * Breadcrumb counts.
+			 *
+			 * @var array<int, int> $breadcrumb_counts
+			 */
+			$breadcrumb_counts = array();
+
+			/**
+			 * Breadcrumb element.
+			 *
+			 * @var array<int, ElementData> $breadcrumb_element
+			 */
+			$breadcrumb_element = array();
+
+			foreach ( $this->url_metrics as $url_metric ) {
+				foreach ( $url_metric->get_elements() as $element ) {
+					if ( ! $element['isLCP'] ) {
+						continue;
+					}
+
+					$i = array_search( $element['xpath'], $seen_breadcrumbs, true );
+					if ( false === $i ) {
+						$i                       = count( $seen_breadcrumbs );
+						$seen_breadcrumbs[ $i ]  = $element['xpath'];
+						$breadcrumb_counts[ $i ] = 0;
+					}
+
+					$breadcrumb_counts[ $i ] += 1;
+					$breadcrumb_element[ $i ] = $element;
+					break; // We found the LCP element for the URL metric, go to the next URL metric.
+				}
+			}
+
+			// Now sort by the breadcrumb counts in descending order, so the remaining first key is the most common breadcrumb.
+			if ( $seen_breadcrumbs ) {
+				arsort( $breadcrumb_counts );
+				$most_common_breadcrumb_index = key( $breadcrumb_counts );
+
+				$lcp_element = $breadcrumb_element[ $most_common_breadcrumb_index ];
+			} else {
+				$lcp_element = null;
+			}
+
+			return $lcp_element;
+		} )();
+
+		$this->result_cache[ __FUNCTION__ ] = $result;
+		return $result;
 	}
 
 	/**
Index: load.php
===================================================================
--- load.php	(revision 3098155)
+++ load.php	(working copy)
@@ -1,11 +1,11 @@
 <?php
 /**
  * Plugin Name: Optimization Detective
- * Plugin URI: https://github.com/WordPress/performance/issues/869
- * Description: Uses real user metrics to improve heuristics WordPress applies on the frontend to improve image loading priority.
+ * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/optimization-detective
+ * 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.4
  * Requires PHP: 7.2
- * Version: 0.2.0
+ * Version: 0.3.0
  * Author: WordPress Performance Team
  * Author URI: https://make.wordpress.org/performance/
  * License: GPLv2 or later
@@ -65,7 +65,7 @@
 	}
 )(
 	'optimization_detective_pending_plugin',
-	'0.2.0',
+	'0.3.0',
 	static function ( string $version ): void {
 
 		// Define the constant.
@@ -113,6 +113,8 @@
 
 		// Optimization logic.
 		require_once __DIR__ . '/class-od-html-tag-walker.php';
+		require_once __DIR__ . '/class-od-preload-link-collection.php';
+		require_once __DIR__ . '/class-od-tag-visitor-registry.php';
 		require_once __DIR__ . '/optimization.php';
 
 		// Add hooks for the above requires.
Index: optimization.php
===================================================================
--- optimization.php	(revision 3098155)
+++ optimization.php	(working copy)
@@ -105,80 +105,6 @@
 }
 
 /**
- * Constructs preload links.
- *
- * @since 0.1.0
- * @access private
- *
- * @param array<int, array{background_image?: string, img_attributes?: array{src?: string, srcset?: string, sizes?: string, crossorigin?: string}}|false> $lcp_elements_by_minimum_viewport_widths LCP elements keyed by minimum viewport width, amended with element details.
- * @return string Markup for zero or more preload link tags.
- */
-function od_construct_preload_links( array $lcp_elements_by_minimum_viewport_widths ): string {
-	$preload_links = array();
-
-	// This uses a for loop to be able to access the following element within the iteration, using a numeric index.
-	$minimum_viewport_widths = array_keys( $lcp_elements_by_minimum_viewport_widths );
-	for ( $i = 0, $len = count( $minimum_viewport_widths ); $i < $len; $i++ ) {
-		$lcp_element = $lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_widths[ $i ] ];
-		if ( false === $lcp_element ) {
-			// No supported LCP element at this breakpoint, so nothing to preload.
-			continue;
-		}
-
-		$link_attributes = array();
-
-		if ( ! empty( $lcp_element['background_image'] ) ) {
-			$link_attributes['href'] = $lcp_element['background_image'];
-		} elseif ( ! empty( $lcp_element['img_attributes'] ) ) {
-			foreach ( $lcp_element['img_attributes'] as $name => $value ) {
-				// Map img attribute name to link attribute name.
-				if ( 'srcset' === $name || 'sizes' === $name ) {
-					$name = 'image' . $name;
-				} elseif ( 'src' === $name ) {
-					$name = 'href';
-				}
-				$link_attributes[ $name ] = $value;
-			}
-		}
-
-		// Skip constructing a link if it is missing required attributes.
-		if ( empty( $link_attributes['href'] ) && empty( $link_attributes['imagesrcset'] ) ) {
-			_doing_it_wrong(
-				__FUNCTION__,
-				esc_html(
-					__( 'Attempted to construct preload link without an available href or imagesrcset. Supplied LCP element: ', 'optimization-detective' ) . wp_json_encode( $lcp_element )
-				),
-				''
-			);
-			continue;
-		}
-
-		// Add media query if it's going to be something other than just `min-width: 0px`.
-		$minimum_viewport_width = $minimum_viewport_widths[ $i ];
-		$maximum_viewport_width = isset( $minimum_viewport_widths[ $i + 1 ] ) ? $minimum_viewport_widths[ $i + 1 ] - 1 : null;
-		$media_features         = array( 'screen' );
-		if ( $minimum_viewport_width > 0 ) {
-			$media_features[] = sprintf( '(min-width: %dpx)', $minimum_viewport_width );
-		}
-		if ( null !== $maximum_viewport_width ) {
-			$media_features[] = sprintf( '(max-width: %dpx)', $maximum_viewport_width );
-		}
-		$link_attributes['media'] = implode( ' and ', $media_features );
-
-		// Construct preload link.
-		$link_tag = '<link data-od-added-tag rel="preload" fetchpriority="high" as="image"';
-		foreach ( $link_attributes as $name => $value ) {
-			$link_tag .= sprintf( ' %s="%s"', $name, esc_attr( $value ) );
-		}
-		$link_tag .= ">\n";
-
-		$preload_links[] = $link_tag;
-	}
-
-	return implode( '', $preload_links );
-}
-
-/**
  * Determines whether the response has an HTML Content-Type.
  *
  * @since 0.2.0
@@ -211,6 +137,8 @@
  *
  * @param string $buffer Template output buffer.
  * @return string Filtered template output buffer.
+ *
+ * @throws Exception Except it won't really.
  */
 function od_optimize_template_output_buffer( string $buffer ): string {
 	if ( ! od_is_response_html_content_type() ) {
@@ -230,155 +158,40 @@
 	// Whether we need to add the data-od-xpath attribute to elements and whether the detection script should be injected.
 	$needs_detection = ! $group_collection->is_every_group_complete();
 
-	$lcp_elements_by_minimum_viewport_widths = od_get_lcp_elements_by_minimum_viewport_widths( $group_collection );
-	$all_breakpoints_have_url_metrics        = $group_collection->is_every_group_populated();
+	// Walk over all tags in the document and ensure fetchpriority is set/removed, and construct preload links for image LCP elements.
+	$preload_links = new OD_Preload_Link_Collection();
+	$walker        = new OD_HTML_Tag_Walker( $buffer );
 
-	/**
-	 * Optimized lookup of the LCP element viewport widths by XPath.
-	 *
-	 * @var array<string, int[]> $lcp_element_minimum_viewport_widths_by_xpath
-	 */
-	$lcp_element_minimum_viewport_widths_by_xpath = array();
-	foreach ( $lcp_elements_by_minimum_viewport_widths as $minimum_viewport_width => $lcp_element ) {
-		if ( false !== $lcp_element ) {
-			$lcp_element_minimum_viewport_widths_by_xpath[ $lcp_element['xpath'] ][] = $minimum_viewport_width;
-		}
-	}
+	$tag_visitor_registry = new OD_Tag_Visitor_Registry();
 
-	// Prepare to set fetchpriority attribute on the image when all breakpoints have the same LCP element.
-	if (
-		// All breakpoints share the same LCP element (or all have none at all).
-		1 === count( $lcp_elements_by_minimum_viewport_widths )
-		&&
-		// The breakpoints don't share a common lack of an LCP image.
-		! in_array( false, $lcp_elements_by_minimum_viewport_widths, true )
-		&&
-		// All breakpoints have URL metrics being reported.
-		$all_breakpoints_have_url_metrics
-	) {
-		$common_lcp_element = current( $lcp_elements_by_minimum_viewport_widths );
-	} else {
-		$common_lcp_element = null;
-	}
-
 	/**
-	 * Mapping of XPath to true to indicate whether the element was found in the document.
+	 * Fires to register tag visitors before walking over the document to perform optimizations.
 	 *
-	 * After processing through the entire document, only the elements which were actually found in the document can get
-	 * preload links.
+	 * @since 0.3.0
 	 *
-	 * @var array<string, true> $detected_lcp_element_xpaths
+	 * @param OD_Tag_Visitor_Registry         $tag_visitor_registry Tag visitor registry.
+	 * @param OD_URL_Metrics_Group_Collection $group_collection     URL Metrics Group collection.
+	 * @param OD_Preload_Link_Collection      $preload_links        Preload links collection.
 	 */
-	$detected_lcp_element_xpaths = array();
+	do_action( 'od_register_tag_visitors', $tag_visitor_registry, $group_collection, $preload_links );
 
-	// Walk over all tags in the document and ensure fetchpriority is set/removed, and gather IMG attributes or background-image for preloading.
-	$walker = new OD_HTML_Tag_Walker( $buffer );
-	foreach ( $walker->open_tags() as $tag_name ) {
-		$is_img_tag = (
-			'IMG' === $tag_name
-			&&
-			$walker->get_attribute( 'src' )
-			&&
-			! str_starts_with( (string) $walker->get_attribute( 'src' ), 'data:' )
-		);
-
-		/*
-		 * Note that CSS allows for a `background`/`background-image` to have multiple `url()` CSS functions, resulting
-		 * in multiple background images being layered on top of each other. This ability is not employed in core. Here
-		 * is a regex to search WPDirectory for instances of this: /background(-image)?:[^;}]+?url\([^;}]+?[^_]url\(/.
-		 * It is used in Jetpack with the second background image being a gradient. To support multiple background
-		 * images, this logic would need to be modified to make $background_image an array and to have a more robust
-		 * parser of the `url()` functions from the property value.
-		 */
-		$background_image_url = null;
-		$style                = $walker->get_attribute( 'style' );
-		if (
-			$style
-			&&
-			preg_match( '/background(-image)?\s*:[^;]*?url\(\s*[\'"]?(?<background_image>.+?)[\'"]?\s*\)/', (string) $style, $matches )
-			&&
-			! str_starts_with( $matches['background_image'], 'data:' )
-		) {
-			$background_image_url = $matches['background_image'];
+	$visitors  = iterator_to_array( $tag_visitor_registry );
+	$generator = $walker->open_tags();
+	while ( $generator->valid() ) {
+		$did_visit = false;
+		foreach ( $visitors as $visitor ) {
+			$did_visit = $visitor( $walker, $group_collection, $preload_links ) || $did_visit;
 		}
 
-		if ( ! ( $is_img_tag || $background_image_url ) ) {
-			continue;
+		if ( $did_visit && $needs_detection ) {
+			$walker->set_meta_attribute( 'xpath', $walker->get_xpath() );
 		}
-
-		$xpath = $walker->get_xpath();
-
-		// Ensure the fetchpriority attribute is set on the element properly.
-		if ( $is_img_tag ) {
-			if ( $common_lcp_element && $xpath === $common_lcp_element['xpath'] ) {
-				if ( 'high' === $walker->get_attribute( 'fetchpriority' ) ) {
-					$walker->set_attribute( 'data-od-fetchpriority-already-added', true );
-				} else {
-					$walker->set_attribute( 'fetchpriority', 'high' );
-					$walker->set_attribute( 'data-od-added-fetchpriority', true );
-				}
-
-				// Never include loading=lazy on the LCP image common across all breakpoints.
-				if ( 'lazy' === $walker->get_attribute( 'loading' ) ) {
-					$walker->set_attribute( 'data-od-removed-loading', $walker->get_attribute( 'loading' ) );
-					$walker->remove_attribute( 'loading' );
-				}
-			} elseif ( $all_breakpoints_have_url_metrics && $walker->get_attribute( 'fetchpriority' ) ) {
-				// Note: The $all_breakpoints_have_url_metrics condition here allows for server-side heuristics to
-				// continue to apply while waiting for all breakpoints to have metrics collected for them.
-				$walker->set_attribute( 'data-od-removed-fetchpriority', $walker->get_attribute( 'fetchpriority' ) );
-				$walker->remove_attribute( 'fetchpriority' );
-			}
-		}
-
-		// TODO: If the image is visible (intersectionRatio!=0) in any of the URL metrics, remove loading=lazy.
-		// TODO: Conversely, if an image is the LCP element for one breakpoint but not another, add loading=lazy. This won't hurt performance since the image is being preloaded.
-
-		// Capture the attributes from the LCP elements to use in preload links.
-		if ( isset( $lcp_element_minimum_viewport_widths_by_xpath[ $xpath ] ) ) {
-			$detected_lcp_element_xpaths[ $xpath ] = true;
-
-			if ( $is_img_tag ) {
-				$img_attributes = array();
-				foreach ( array( 'src', 'srcset', 'sizes', 'crossorigin' ) as $attr_name ) {
-					$value = $walker->get_attribute( $attr_name );
-					if ( is_string( $value ) ) {
-						$img_attributes[ $attr_name ] = $value;
-					}
-				}
-				foreach ( $lcp_element_minimum_viewport_widths_by_xpath[ $xpath ] as $minimum_viewport_width ) {
-					if ( is_array( $lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ] ) ) {
-						$lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ]['img_attributes'] = $img_attributes;
-					}
-				}
-			} elseif ( $background_image_url ) {
-				foreach ( $lcp_element_minimum_viewport_widths_by_xpath[ $xpath ] as $minimum_viewport_width ) {
-					if ( is_array( $lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ] ) ) {
-						$lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ]['background_image'] = $background_image_url;
-					}
-				}
-			}
-		}
-
-		if ( $needs_detection ) {
-			$walker->set_attribute( 'data-od-xpath', $xpath );
-		}
+		$generator->next();
 	}
 
-	// If there were any LCP elements captured in URL Metrics that no longer exist in the document, we need to behave as
-	// if they didn't exist in the first place as there is nothing that can be preloaded.
-	foreach ( array_keys( $lcp_element_minimum_viewport_widths_by_xpath ) as $xpath ) {
-		if ( empty( $detected_lcp_element_xpaths[ $xpath ] ) ) {
-			foreach ( $lcp_element_minimum_viewport_widths_by_xpath[ $xpath ] as $minimum_viewport_width ) {
-				$lcp_elements_by_minimum_viewport_widths[ $minimum_viewport_width ] = false;
-			}
-		}
-	}
-
 	// Inject any preload links at the end of the HEAD.
-	$head_injection = od_construct_preload_links( $lcp_elements_by_minimum_viewport_widths );
-	if ( $head_injection ) {
-		$walker->append_head_html( $head_injection );
+	if ( count( $preload_links ) > 0 ) {
+		$walker->append_head_html( $preload_links->get_html() );
 	}
 
 	// Inject detection script.
Index: readme.txt
===================================================================
--- readme.txt	(revision 3098155)
+++ readme.txt	(working copy)
@@ -1,19 +1,19 @@
-=== Optimization Detective (Developer Preview) ===
+=== Optimization Detective ===
 
 Contributors:      wordpressdotorg
 Requires at least: 6.4
 Tested up to:      6.5
 Requires PHP:      7.2
-Stable tag:        0.2.0
+Stable tag:        0.3.0
 License:           GPLv2 or later
 License URI:       https://www.gnu.org/licenses/gpl-2.0.html
-Tags:              performance, images
+Tags:              performance, optimization, rum
 
-Uses real user metrics to improve heuristics WordPress applies on the frontend to improve image loading priority.
+Provides an API for leveraging real user metrics to detect optimizations to apply on the frontend to improve page performance.
 
 == Description ==
 
-This plugin captures real user metrics about what elements are displayed on the page across a variety of device form factors (e.g. desktop, tablet, and phone) in order to apply loading optimizations which are not possible with WordPress’s current server-side heuristics.
+This plugin captures real user metrics about what elements are displayed on the page across a variety of device form factors (e.g. desktop, tablet, and phone) in order to apply loading optimizations which are not possible with WordPress’s current server-side heuristics. This plugin is a dependency which does not provide end-user functionality on its own. For that, please install the dependent plugin [Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) (among [others](https://github.com/WordPress/performance/labels/%5BPlugin%5D%20Optimization%20Detective) to come from the WordPress Core Performance team).
 
 = Background =
 
@@ -23,17 +23,17 @@
 
 = Technical Foundation =
 
-At the core of Optimization Detective is the “URL Metric”, information about a page according to how it was loaded by a client with a specific viewport width. This includes which elements were visible in the initial viewport and which one was the LCP element. Each URL on a site can have an associated set of these URL Metrics (stored in a custom post type) which are gathered from real users. It gathers a sample of URL Metrics according to common responsive breakpoints (e.g. mobile, tablet, and desktop). When no more URL Metrics are needed for a URL due to the sample size being obtained for the breakpoints, it discontinues serving the JavaScript to gather the metrics (leveraging the [web-vitals.js](https://github.com/GoogleChrome/web-vitals) library). With the URL Metrics in hand, the output-buffered page is sent through the HTML Tag Processor and the images which were the LCP element for various breakpoints will get prioritized with high-priority preload links (along with `fetchpriority=high` on the actual `img` tag when it is the common LCP element across all breakpoints). LCP elements with background images added via inline `background-image` styles are also prioritized with preload links.
+At the core of Optimization Detective is the “URL Metric”, information about a page according to how it was loaded by a client with a specific viewport width. This includes which elements were visible in the initial viewport and which one was the LCP element. Each URL on a site can have an associated set of these URL Metrics (stored in a custom post type) which are gathered from real users. It gathers a sample of URL Metrics according to common responsive breakpoints (e.g. mobile, tablet, and desktop). When no more URL Metrics are needed for a URL due to the sample size being obtained for the breakpoints, it discontinues serving the JavaScript to gather the metrics (leveraging the [web-vitals.js](https://github.com/GoogleChrome/web-vitals) library). With the URL Metrics in hand, the output-buffered page is sent through the HTML Tag Processor and--when the [Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) dependent plugin is installed--the images which were the LCP element for various breakpoints will get prioritized with high-priority preload links (along with `fetchpriority=high` on the actual `img` tag when it is the common LCP element across all breakpoints). LCP elements with background images added via inline `background-image` styles are also prioritized with preload links.
 
 URL Metrics have a “freshness TTL” after which they will be stale and the JavaScript will be served again to start gathering metrics again to ensure that the right elements continue to get their loading prioritized. When a URL Metrics custom post type hasn't been touched in a while, it is automatically garbage-collected.
 
-Prioritizing the loading of images which are the LCP element is only the first optimization implemented as a proof of concept for how other optimizations might also be applied. See a [list of issues](https://github.com/WordPress/performance/labels/%5BPlugin%5D%20Optimization%20Detective) for planned additional optimizations which are only feasible with the URL Metrics RUM data.
+👉 **Note:** This plugin optimizes pages for actual visitors, and it depends on visitors to optimize pages (since URL metrics need to be collected). As such, you won't see optimizations applied immediately after activating the plugin (and dependent plugin(s)). And since administrator users are not normal visitors typically, optimizations are not applied for admins by default (but this can be overridden with the `od_can_optimize_response` filter below). URL metrics are not collected for administrators because it is likely that additional elements will be present on the page which are not also shown to non-administrators, meaning the URL metrics could not reliably be reused between them. 
 
-Note that by default, URL Metrics are not gathered for administrator users, since they are not normal site visitors, and it is likely that additional elements will be present on the page which are not also shown to non-administrators.
+There are currently **no settings** and no user interface for this plugin since it is designed to work without any configuration.
 
 When the `WP_DEBUG` constant is enabled, additional logging for Optimization Detective is added to the browser console.
 
-= Filters =
+= Hooks =
 
 **Filter:** `od_breakpoint_max_widths` (default: [480, 600, 782])
 
@@ -68,7 +68,9 @@
 
 `
 <?php
-add_filter( 'od_url_metrics_breakpoint_sample_size', function () { return 1; } );
+add_filter( 'od_url_metrics_breakpoint_sample_size', function (): int {
+	return 1;
+} );
 `
 
 **Filter:** `od_url_metric_storage_lock_ttl` (default: 1 minute)
@@ -77,7 +79,7 @@
 
 `
 <?php
-add_filter( 'od_metrics_storage_lock_ttl', function ( $ttl ) {
+add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int {
     return is_user_logged_in() ? 0 : $ttl;
 } );
 `
@@ -97,7 +99,7 @@
 
 **Filter:** `od_template_output_buffer` (default: the HTML response)
 
-Filters the template output buffer prior to sending to the client. This filter is added to implement #43258.
+Filters the template output buffer prior to sending to the client. This filter is added to implement [#43258](https://core.trac.wordpress.org/ticket/43258) in WordPress core.
 
 == Installation ==
 
@@ -115,10 +117,6 @@
 
 == Frequently Asked Questions ==
 
-= What is the status of this plugin and what does “developer preview” mean? =
-
-This initial release of the Optimization Detective plugin is a preview for the kinds of optimizations that can be applied with this foundation. The intention is that this plugin will serve as an API, planned eventually to be proposed for WordPress core, in which other plugins can extend the functionality to apply additional optimizations. Additional documentation will be made available as development progresses. Follow [progress on GitHub](https://github.com/WordPress/performance/labels/%5BPlugin%5D%20Optimization%20Detective).
-
 = Where can I submit my plugin feedback? =
 
 Feedback is encouraged and much appreciated, especially since this plugin may contain future WordPress core features. If you have suggestions or requests for new features, you can [submit them as an issue in the WordPress Performance Team's GitHub repository](https://github.com/WordPress/performance/issues/new/choose). If you need help with troubleshooting or have a question about the plugin, please [create a new topic on our support forum](https://wordpress.org/support/plugin/optimization-detective/#new-topic-0).
@@ -137,6 +135,10 @@
 
 == Changelog ==
 
+= 0.3.0 =
+
+* The image optimization features have been split out into a new dependent plugin called [Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/), which also now optimizes image lazy-loading. ([1088](https://github.com/WordPress/performance/issues/1088))
+
 = 0.2.0 =
 
 **Enhancements**
@@ -161,3 +163,9 @@
 = 0.1.0 =
 
 * Initial release.
+
+== Upgrade Notice ==
+
+= 0.3.0 =
+
+Image loading optimizations have been moved to a new dependent plugin called Image Prioritizer. The Optimization Detective plugin now serves as a dependency.
Index: storage/class-od-storage-lock.php
===================================================================
--- storage/class-od-storage-lock.php	(revision 3098155)
+++ storage/class-od-storage-lock.php	(working copy)
@@ -35,7 +35,7 @@
 		 * Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable
 		 * locking when a user is logged-in with code like the following:
 		 *
-		 *     add_filter( 'od_metrics_storage_lock_ttl', static function ( $ttl ) {
+		 *     add_filter( 'od_metrics_storage_lock_ttl', static function ( int $ttl ): int {
 		 *         return is_user_logged_in() ? 0 : $ttl;
 		 *     } );
 		 *
Index: storage/class-od-url-metrics-post-type.php
===================================================================
--- storage/class-od-url-metrics-post-type.php	(revision 3098155)
+++ storage/class-od-url-metrics-post-type.php	(working copy)
@@ -118,7 +118,7 @@
 	 */
 	public static function get_url_metrics_from_post( WP_Post $post ): array {
 		$this_function   = __FUNCTION__;
-		$trigger_warning = static function ( $message ) use ( $this_function ): void {
+		$trigger_warning = static function ( string $message ) use ( $this_function ): void {
 			wp_trigger_error( $this_function, esc_html( $message ), E_USER_WARNING );
 		};
 
Index: storage/data.php
===================================================================
--- storage/data.php	(revision 3098155)
+++ storage/data.php	(working copy)
@@ -298,87 +298,3 @@
 
 	return $sample_size;
 }
-
-/**
- * Gets the LCP element for each breakpoint.
- *
- * The array keys are the minimum viewport width required for the element to be LCP. If there are URL metrics for a
- * given breakpoint and yet there is no supported LCP element, then the array value is `false`. (Currently only IMG is
- * a supported LCP element.) If there is a supported LCP element at the breakpoint, then the array value is an array
- * representing that element, including its breadcrumbs. If two adjoining breakpoints have the same value, then the
- * latter is dropped.
- *
- * @since 0.1.0
- * @access private
- *
- * @param OD_URL_Metrics_Group_Collection $group_collection URL metrics group collection.
- * @return array<int, array{xpath: string}|false> LCP elements keyed by its minimum viewport width. If there is no
- *                                                supported LCP element at a breakpoint, then `false` is used. Note that
- *                                                the array shape is actually an ElementData from OD_URL_Metric but
- *                                                PHPStan does not support importing a type onto a function.
- */
-function od_get_lcp_elements_by_minimum_viewport_widths( OD_URL_Metrics_Group_Collection $group_collection ): array {
-	$lcp_element_by_viewport_minimum_width = array();
-	foreach ( $group_collection as $group ) {
-
-		// The following arrays all share array indices.
-		$seen_breadcrumbs   = array();
-		$breadcrumb_counts  = array();
-		$breadcrumb_element = array();
-
-		foreach ( $group as $url_metric ) {
-			foreach ( $url_metric->get_elements() as $element ) {
-				if ( ! $element['isLCP'] ) {
-					continue;
-				}
-
-				$i = array_search( $element['xpath'], $seen_breadcrumbs, true );
-				if ( false === $i ) {
-					$i                       = count( $seen_breadcrumbs );
-					$seen_breadcrumbs[ $i ]  = $element['xpath'];
-					$breadcrumb_counts[ $i ] = 0;
-				}
-
-				$breadcrumb_counts[ $i ] += 1;
-				$breadcrumb_element[ $i ] = $element;
-				break; // We found the LCP element for the URL metric, go to the next URL metric.
-			}
-		}
-
-		// Now sort by the breadcrumb counts in descending order, so the remaining first key is the most common breadcrumb.
-		if ( $seen_breadcrumbs ) {
-			arsort( $breadcrumb_counts );
-			$most_common_breadcrumb_index = key( $breadcrumb_counts );
-
-			$lcp_element_by_viewport_minimum_width[ $group->get_minimum_viewport_width() ] = $breadcrumb_element[ $most_common_breadcrumb_index ];
-		} elseif ( count( $group ) > 0 ) {
-			$lcp_element_by_viewport_minimum_width[ $group->get_minimum_viewport_width() ] = false; // No LCP image at this breakpoint.
-		}
-	}
-
-	// Now merge the breakpoints when there is an LCP element common between them.
-	$prev_lcp_element = null;
-	return array_filter(
-		$lcp_element_by_viewport_minimum_width,
-		static function ( $lcp_element ) use ( &$prev_lcp_element ) {
-			$include = (
-				// First element in list.
-				null === $prev_lcp_element
-				||
-				( is_array( $prev_lcp_element ) && is_array( $lcp_element )
-					?
-					// This breakpoint and previous breakpoint had LCP element, and they were not the same element.
-					$prev_lcp_element['xpath'] !== $lcp_element['xpath']
-					:
-					// This LCP element and the last LCP element were not the same. In this case, either variable may be
-					// false or an array, but both cannot be an array. If both are false, we don't want to include since
-					// it is the same. If one is an array and the other is false, then do want to include because this
-					// indicates a difference at this breakpoint.
-					$prev_lcp_element !== $lcp_element
-				)
-			);
-			$prev_lcp_element = $lcp_element;
-			return $include;
-		}
-	);
-}
Index: storage/rest-api.php
===================================================================
--- storage/rest-api.php	(revision 3098155)
+++ storage/rest-api.php	(working copy)
@@ -51,7 +51,7 @@
 			'description'       => __( 'Nonce originally computed by server required to authorize the request.', 'optimization-detective' ),
 			'required'          => true,
 			'pattern'           => '^[0-9a-f]+$',
-			'validate_callback' => static function ( $nonce, WP_REST_Request $request ) {
+			'validate_callback' => static function ( string $nonce, WP_REST_Request $request ) {
 				if ( ! od_verify_url_metrics_storage_nonce( $nonce, $request->get_param( 'slug' ), $request->get_param( 'url' ) ) ) {
 					return new WP_Error( 'invalid_nonce', __( 'URL metrics nonce verification failure.', 'optimization-detective' ) );
 				}

@westonruter westonruter added [Type] Documentation Documentation to be added or enhanced skip changelog PRs that should not be mentioned in changelogs labels Jun 5, 2024
@westonruter westonruter requested a review from swissspidy June 5, 2024 17:32
@westonruter westonruter requested a review from felixarntz as a code owner June 5, 2024 17:32
@github-actions
Copy link

github-actions bot commented Jun 5, 2024

The following accounts have interacted with this PR and/or linked issues. I will continue to update these lists as activity occurs. You can also manually ask me to refresh this list by adding the props-bot label.

If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.

Co-authored-by: westonruter <[email protected]>
Co-authored-by: swissspidy <[email protected]>

To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook.

@westonruter westonruter force-pushed the publish/image-prioritizer branch from c3e9b44 to 7f68623 Compare June 5, 2024 17:45
@github-actions github-actions bot temporarily deployed to wp.org plugin: optimization-detective June 5, 2024 18:17 Destroyed
@github-actions github-actions bot temporarily deployed to wp.org plugin: optimization-detective June 5, 2024 18:18 Destroyed
@westonruter westonruter merged commit 625a713 into trunk Jun 5, 2024
@westonruter westonruter deleted the publish/image-prioritizer branch June 5, 2024 18:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

skip changelog PRs that should not be mentioned in changelogs [Type] Documentation Documentation to be added or enhanced

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants