-
Notifications
You must be signed in to change notification settings - Fork 83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Refactor optimization logic in Optimization Detective #1172
Conversation
807d2a1
to
0915b64
Compare
32ed804
to
bf49068
Compare
ccebab7
to
e09577d
Compare
5bfbf7e
to
72e71a9
Compare
4c949d4
to
3ec6dfc
Compare
Last commit before heavily rebasing: 4bc0540 |
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; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since WP_HTML_Tag_Processor
doesn't rewind after iterating over tags in the document, it's important that open_tags()
also not support being invoked multiple times to walk over the document.
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 ); | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a small optimization in case get_xpath()
is called multiple times when visiting the same tag.
public function get_tag(): ?string { | ||
return $this->processor->get_tag(); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This ensures that when passing the OD_HTML_Tag_Walker
instance around, the current tag can always be obtained even when not looping over open_tags()
.
preg_match( '/background(-image)?\s*:[^;]*?url\(\s*[\'"]?(?<background_image>.+?)[\'"]?\s*\)/', (string) $style, $matches ) | ||
preg_match( '/background(-image)?\s*:[^;]*?url\(\s*[\'"]?\s*(?<background_image>.+?)\s*[\'"]?\s*\)/', (string) $style, $matches ) | ||
&& | ||
! str_starts_with( $matches['background_image'], 'data:' ) | ||
! $is_data_url( $matches['background_image'] ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a little hardening bugfix to account for whitespace around the URL or, more unlikely, the scheme to be DATA:
which does work in browsers.
$common_lcp_element = current( $lcp_elements_by_minimum_viewport_widths ); | ||
$common_lcp_xpath = key( $groups_by_lcp_element_xpath ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just capturing the data we need (the XPath) as opposed to the entire element.
// 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; | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is not needed anymore now that preload links are added during iteration above.
* 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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This logic was moved to OD_URL_Metrics_Group::get_lcp_element()
.
// 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; | ||
} | ||
); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Now handled by array_reduce()
in OD_Preload_Link_Collection::get_adjacent_deduplicated_links()
* @throws OD_Data_Validation_Exception When failing to instantiate a URL metric. | ||
* @return array<string, mixed> Data. | ||
*/ | ||
public function data_provider_test_get_lcp_elements_by_minimum_viewport_widths(): array { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Tests moved to a5b4699.
* @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 { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Logic moved to OD_Preload_Link_Collection
class.
'no-lcp-image' => array( | ||
'lcp_elements_by_minimum_viewport_widths' => array( | ||
0 => false, | ||
), | ||
'expected' => '', | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test case already handled by test_od_optimize_template_output_buffer
via no-url-metrics
and no-lcp-image-with-populated-url-metrics
.
'one-non-responsive-lcp-image' => array( | ||
'lcp_elements_by_minimum_viewport_widths' => array( | ||
0 => array( | ||
'img_attributes' => array( | ||
'src' => 'https://example.com/image.jpg', | ||
), | ||
), | ||
), | ||
'expected' => ' | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/image.jpg" media="screen"> | ||
', | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test case already handled by test_od_optimize_template_output_buffer
via common-lcp-image-with-fully-populated-sample-data
.
'one-responsive-lcp-image' => array( | ||
'lcp_elements_by_minimum_viewport_widths' => array( | ||
0 => array( | ||
'img_attributes' => array( | ||
'src' => 'https://example.com/elva-fairy-800w.jpg', | ||
'srcset' => 'https://example.com/elva-fairy-480w.jpg 480w, https://example.com/elva-fairy-800w.jpg 800w', | ||
'sizes' => '(max-width: 600px) 480px, 800px', | ||
'crossorigin' => 'anonymous', | ||
), | ||
), | ||
), | ||
'expected' => ' | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/elva-fairy-800w.jpg" imagesrcset="https://example.com/elva-fairy-480w.jpg 480w, https://example.com/elva-fairy-800w.jpg 800w" imagesizes="(max-width: 600px) 480px, 800px" crossorigin="anonymous" media="screen"> | ||
', | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test case already handled by test_od_optimize_template_output_buffer
via common-lcp-image-with-fully-populated-sample-data
.
'two-breakpoint-responsive-lcp-images' => array( | ||
'lcp_elements_by_minimum_viewport_widths' => array( | ||
0 => array( | ||
'img_attributes' => array( | ||
'src' => 'https://example.com/elva-fairy-800w.jpg', | ||
'srcset' => 'https://example.com/elva-fairy-480w.jpg 480w, https://example.com/elva-fairy-800w.jpg 800w', | ||
'sizes' => '(max-width: 600px) 480px, 800px', | ||
'crossorigin' => 'anonymous', | ||
), | ||
), | ||
601 => array( | ||
'img_attributes' => array( | ||
'src' => 'https://example.com/alt-elva-fairy-800w.jpg', | ||
'srcset' => 'https://example.com/alt-elva-fairy-480w.jpg 480w, https://example.com/alt-elva-fairy-800w.jpg 800w', | ||
'sizes' => '(max-width: 600px) 480px, 800px', | ||
'crossorigin' => 'anonymous', | ||
), | ||
), | ||
), | ||
'expected' => ' | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/elva-fairy-800w.jpg" imagesrcset="https://example.com/elva-fairy-480w.jpg 480w, https://example.com/elva-fairy-800w.jpg 800w" imagesizes="(max-width: 600px) 480px, 800px" crossorigin="anonymous" media="screen and (max-width: 600px)"> | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/alt-elva-fairy-800w.jpg" imagesrcset="https://example.com/alt-elva-fairy-480w.jpg 480w, https://example.com/alt-elva-fairy-800w.jpg 800w" imagesizes="(max-width: 600px) 480px, 800px" crossorigin="anonymous" media="screen and (min-width: 601px)"> | ||
', | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test case essentially already handled by test_od_optimize_template_output_buffer
via responsive-background-images
.
'two-non-consecutive-responsive-lcp-images' => array( | ||
'lcp_elements_by_minimum_viewport_widths' => array( | ||
0 => array( | ||
'img_attributes' => array( | ||
'src' => 'https://example.com/elva-fairy-800w.jpg', | ||
'srcset' => 'https://example.com/elva-fairy-480w.jpg 480w, https://example.com/elva-fairy-800w.jpg 800w', | ||
'sizes' => '(max-width: 600px) 480px, 800px', | ||
'crossorigin' => 'anonymous', | ||
), | ||
), | ||
481 => false, | ||
601 => array( | ||
'img_attributes' => array( | ||
'src' => 'https://example.com/alt-elva-fairy-800w.jpg', | ||
'srcset' => 'https://example.com/alt-elva-fairy-480w.jpg 480w, https://example.com/alt-elva-fairy-800w.jpg 800w', | ||
'sizes' => '(max-width: 600px) 480px, 800px', | ||
'crossorigin' => 'anonymous', | ||
), | ||
), | ||
), | ||
'expected' => ' | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/elva-fairy-800w.jpg" imagesrcset="https://example.com/elva-fairy-480w.jpg 480w, https://example.com/elva-fairy-800w.jpg 800w" imagesizes="(max-width: 600px) 480px, 800px" crossorigin="anonymous" media="screen and (max-width: 480px)"> | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/alt-elva-fairy-800w.jpg" imagesrcset="https://example.com/alt-elva-fairy-480w.jpg 480w, https://example.com/alt-elva-fairy-800w.jpg 800w" imagesizes="(max-width: 600px) 480px, 800px" crossorigin="anonymous" media="screen and (min-width: 601px)"> | ||
', | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test case already handled by test_od_optimize_template_output_buffer
via different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group
.
'two-background-lcp-images' => array( | ||
'lcp_elements_by_minimum_viewport_widths' => array( | ||
0 => array( | ||
'background_image' => 'https://example.com/mobile.jpg', | ||
), | ||
481 => array( | ||
'background_image' => 'https://example.com/desktop.jpg', | ||
), | ||
), | ||
'expected' => ' | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/mobile.jpg" media="screen and (max-width: 480px)"> | ||
<link data-od-added-tag rel="preload" fetchpriority="high" as="image" href="https://example.com/desktop.jpg" media="screen and (min-width: 481px)"> | ||
', | ||
), |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Test case essentially already handled by test_od_optimize_template_output_buffer
via responsive-background-images
.
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 If you're merging code through a pull request on GitHub, copy and paste the following into the bottom of the merge commit message.
To understand the WordPress project's expectations around crediting contributors, please review the Contributor Attribution page in the Core Handbook. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Appreciate the detailed description and individual commits that made it easy to follow the refactoring process here.
Also great that we're using a high PHPStan level, provides some extra peace of mind :)
plugins/optimization-detective/class-od-preload-link-collection.php
Outdated
Show resolved
Hide resolved
Co-authored-by: Pascal Birchler <pascalb@google.com>
Co-authored-by: Pascal Birchler <pascalb@google.com>
Co-authored-by: Pascal Birchler <pascalb@google.com>
This PR refactors the logic to facilitate extracting optimization logic into separate dependent plugins, for example Image Prioritizer (#1088).
To review the changes here, I suggest reviewing commit-by-commit, as I've carefully rebased changes to keep each commit as atomic as possible.
The biggest change is the introduction of the
OD_Preload_Link_Collection
class. Instead of building up an array data structure while walking over the document and then manually constructing HTMLlink
tags once iteration is over, the use of this new class makes the logic much clearer as the pending preload links are added during iteration in a well-defined object interface.Additionally, instead of passing around an array of LCP element data arrays keyed by the minimum viewport widths (via
od_get_lcp_elements_by_minimum_viewport_widths()
), this logic is now moved over to a more logical place as aget_lcp_element
method on theOD_URL_Metrics_Group
class. The minimum viewport and maximum viewport can be obtained via the methods on theOD_URL_Metrics_Group
instance. This simplifies construction of the preload links, as each group knows its minimum and maximum viewport width, without having to iterate over other keys to discover which one is the next-largest.Also, there was a somewhat subtle distinction when obtaining the LCP element data for a given viewport group: it could return an
array
,false
, ornull
. The array case meant that there was an LCP element found. Thefalse
case meant that there were URL Metrics gathered, but no supported LCP element was present. Lastly, thenull
meant that there was no data available at all. In c6681cf and 07e19c1 I've merged thesefalse
andnull
states into justnull
. The logic was going ahead and preloading an LCP element from a smaller viewport group when the next-largest viewport group was missing data. This is now undone so that it will only preload images which are actually detected as being LCP elements.Work in this PR leads into #1235 which will address #1088.