Skip to content
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

Merged
merged 20 commits into from May 23, 2024

Conversation

westonruter
Copy link
Member

@westonruter westonruter commented Apr 23, 2024

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 HTML link 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 a get_lcp_element method on the OD_URL_Metrics_Group class. The minimum viewport and maximum viewport can be obtained via the methods on the OD_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, or null. The array case meant that there was an LCP element found. The false case meant that there were URL Metrics gathered, but no supported LCP element was present. Lastly, the null meant that there was no data available at all. In c6681cf and 07e19c1 I've merged these false and null states into just null. 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.

@westonruter westonruter added the [Plugin] Optimization Detective Issues for the Optimization Detective plugin label Apr 23, 2024
@westonruter westonruter added this to the image-prioritizer 0.1.0 milestone Apr 23, 2024
@westonruter westonruter added the [Type] Feature A new feature within an existing module label Apr 23, 2024
@westonruter westonruter changed the base branch from trunk to feature/image-prioritizer April 23, 2024 18:16
@westonruter westonruter force-pushed the add/image-prioritizer-plugin branch 2 times, most recently from ccebab7 to e09577d Compare May 3, 2024 19:56
@westonruter westonruter changed the base branch from feature/image-prioritizer to trunk May 3, 2024 19:56
@westonruter westonruter force-pushed the add/image-prioritizer-plugin branch 7 times, most recently from 5bfbf7e to 72e71a9 Compare May 15, 2024 21:17
@westonruter westonruter force-pushed the add/image-prioritizer-plugin branch from 4c949d4 to 3ec6dfc Compare May 20, 2024 23:55
@westonruter
Copy link
Member Author

Last commit before heavily rebasing: 4bc0540

Comment on lines +211 to +214
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;
Copy link
Member Author

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.

Comment on lines +368 to 373
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 );
}
}
Copy link
Member Author

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.

Comment on lines +422 to +424
public function get_tag(): ?string {
return $this->processor->get_tag();
}
Copy link
Member Author

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().

Comment on lines -298 to +223
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'] )
Copy link
Member Author

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.

Comment on lines -259 to +184
$common_lcp_element = current( $lcp_elements_by_minimum_viewport_widths );
$common_lcp_xpath = key( $groups_by_lcp_element_xpath );
Copy link
Member Author

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.

Comment on lines -368 to -376
// 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;
}
}
}
Copy link
Member Author

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 {
Copy link
Member Author

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().

Comment on lines -359 to -383
// 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;
}
);
Copy link
Member Author

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 {
Copy link
Member Author

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 {
Copy link
Member Author

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.

Comment on lines -188 to -193
'no-lcp-image' => array(
'lcp_elements_by_minimum_viewport_widths' => array(
0 => false,
),
'expected' => '',
),
Copy link
Member Author

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.

Comment on lines -194 to -205
'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">
',
),
Copy link
Member Author

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.

Comment on lines -206 to -220
'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">
',
),
Copy link
Member Author

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.

Comment on lines -221 to -244
'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)">
',
),
Copy link
Member Author

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.

Comment on lines -245 to -269
'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)">
',
),
Copy link
Member Author

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.

Comment on lines -280 to -293
'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)">
',
),
Copy link
Member Author

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.

@westonruter westonruter marked this pull request as ready for review May 21, 2024 23:34
Copy link

github-actions bot commented May 21, 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 <westonruter@git.wordpress.org>
Co-authored-by: swissspidy <swissspidy@git.wordpress.org>

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

Copy link
Member

@swissspidy swissspidy left a 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 :)

westonruter and others added 3 commits May 23, 2024 10:37
Co-authored-by: Pascal Birchler <pascalb@google.com>
Co-authored-by: Pascal Birchler <pascalb@google.com>
Co-authored-by: Pascal Birchler <pascalb@google.com>
@westonruter westonruter merged commit 0a95707 into trunk May 23, 2024
14 checks passed
@westonruter westonruter deleted the add/image-prioritizer-plugin branch May 23, 2024 18:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
[Plugin] Optimization Detective Issues for the Optimization Detective plugin [Type] Feature A new feature within an existing module
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

2 participants