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

Leverage URL metrics to reserve space for embeds to reduce CLS #1373

Draft
wants to merge 26 commits into
base: trunk
Choose a base branch
from

Conversation

westonruter
Copy link
Member

@westonruter westonruter commented Jul 17, 2024

Fixes #1310

TODO: Summarize the changes in this PR. In the mean time, see the videos below.

To do

  • Collecting the URL metrics needs to wait until after all the embeds have loaded. This is not currently the case for Tweets, for example.
  • The min-height style should be set on the figure.wp-block-embed but the measurement of the height of the embed should only come from the child .wp-block-embed__wrapper. In this way, an embed can be allowed to shrink its height over time.
  • Wait to set the min-height until there are URL metrics gathered for the smallest breakpoint group?
  • Embed Optimizer is sending the min-height of embeds before they are shown in the viewport. We should have a resizedBoundingClientRect instead of overriding boundingClientRect? We can then only consider elements that have the final rect set. This depends on Allow URL metric schema to be extended #1492.
  • Add tests.

Demo Videos

Before

bad-cls-without-embed-optimizer.webm

After

Once URL metrics have been collected from visitors by Optimization Detective:

good-cls-with-embed-optimizer.webm

@westonruter westonruter added the [Type] Enhancement A suggestion for improvement of an existing feature label Jul 17, 2024
@swissspidy swissspidy added the [Plugin] Embed Optimizer Issues for the Embed Optimizer plugin (formerly Auto Sizes) label Jul 26, 2024
@westonruter westonruter force-pushed the add/embed-optimizer-min-height-reservation branch from f38247b to 947ca41 Compare July 26, 2024 18:23
@westonruter westonruter added this to the embed-optimizer n.e.x.t milestone Jul 26, 2024
@westonruter westonruter added the [Plugin] Optimization Detective Issues for the Optimization Detective plugin label Jul 26, 2024
@westonruter westonruter force-pushed the add/embed-optimizer-min-height-reservation branch from 3f38eb0 to b3ca4ad Compare July 26, 2024 22:06
return false;
}

$max_intersection_ratio = $context->url_metrics_group_collection->get_element_max_intersection_ratio( $processor->get_xpath() );
$embed_wrapper_xpath = $processor->get_xpath() . '/*[1][self::DIV]';
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not ideal to be constructing this XPath manually.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've opened #1407 to explore this, but it's not necessary for this PR to move forward.

@westonruter
Copy link
Member Author

westonruter commented Jul 26, 2024

I noticed Query Monitor flagging a PHP deprecation error when loading a page with Optimization Detective and Embed Optimizer active. A TikTok embed is present on the page. I'm getting:

PHP Deprecated: Implicit conversion from float 739.9976196289062 to int loses precision in /var/www/html/wp-content/plugins/optimization-detective/class-od-url-metrics-group-collection.php on line 486

Call stack:

image

Update: Addressed in #1411.

@westonruter
Copy link
Member Author

westonruter commented Jul 26, 2024

I also saw this for some reason:

PHP Notice: OD_HTML_Tag_Processor::get_updated_html(): Unable to append markup to optimization_detective_end_of_body since the bookmark no longer exists. in /var/www/html/wp-includes/functions.php on line 6085

Update: See fix below in #1373 (comment)

@westonruter westonruter force-pushed the add/embed-optimizer-min-height-reservation branch from 02b8fb3 to b17d8ba Compare July 30, 2024 00:40
@westonruter westonruter changed the base branch from trunk to fix/od-schema July 30, 2024 00:40
@westonruter westonruter force-pushed the add/embed-optimizer-min-height-reservation branch from b17d8ba to 76369b4 Compare July 30, 2024 00:42
Base automatically changed from fix/od-schema to trunk July 30, 2024 14:58
@westonruter westonruter force-pushed the add/embed-optimizer-min-height-reservation branch from 1f5e5f8 to 77bea30 Compare August 2, 2024 00:06
Comment on lines 88 to 95
$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 );
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 should be a helper method on OD_HTML_Tag_Processor.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@westonruter westonruter force-pushed the add/embed-optimizer-min-height-reservation branch 2 times, most recently from 019526c to 36e5620 Compare August 13, 2024 23:03
@westonruter
Copy link
Member Author

Scratched code to explore using onCLS to detect when an embed has loaded (and causes a layout shift. Problem: In the case of the Twitter embed, the source node being reported is REMOVED from the DOM, so we cannot see the ancestor which has a data-od-xpath attribute.

Code scratch
	const { onLCP, onCLS } = await import( webVitalsLibrarySrc );

	onCLS(
		( metric ) => {
			for ( const entry of metric.entries ) {
				if (
					entry.entryType === 'layout-shift' &&
					! entry.hadRecentInput
				) {
					console.info( entry );
					for ( const source of entry.sources ) {
						console.info( source.node );
					}
				}
			}
		},
		{
			// This is necessary in order to collect all layout shifts, even those which don't cause a bad CLS score.
			reportAllChanges: true,
		}
	);

@westonruter westonruter modified the milestones: embed-optimizer 0.3.0, embed-optimizer n.e.x.t Aug 15, 2024
@westonruter westonruter force-pushed the add/embed-optimizer-min-height-reservation branch from 363b50d to 0ba2d6e Compare August 17, 2024 23:49
@westonruter
Copy link
Member Author

westonruter commented Aug 18, 2024

I'm seeing this again:

OD_HTML_Tag_Processor::get_updated_html(): Unable to append markup to optimization_detective_end_of_body since the bookmark no longer exists.

wp-includes/functions.php:6095
wp_trigger_error()
wp-content/plugins/optimization-detective/class-od-html-tag-processor.php:612
OD_HTML_Tag_Processor->warn()
wp-content/plugins/optimization-detective/class-od-html-tag-processor.php:579
OD_HTML_Tag_Processor->get_updated_html()
wp-includes/html-api/class-wp-html-tag-processor.php:2500
WP_HTML_Tag_Processor->seek()
wp-content/plugins/optimization-detective/class-od-html-tag-processor.php:428
OD_HTML_Tag_Processor->seek()
wp-content/plugins/optimization-detective/optimization.php:229
od_optimize_template_output_buffer()
wp-content/plugins/performance-lab/includes/server-timing/load.php:138
{closure}()
wp-includes/class-wp-hook.php:324
apply_filters('od_template_output_buffer')
wp-content/plugins/optimization-detective/optimization.php:66
{closure}()
wp-content/plugins/optimization-detective/optimization.php:66
ob_end_flush()
wp-includes/functions.php:5437
wp_ob_end_flush_all()
wp-includes/class-wp-hook.php:324
do_action('shutdown')
wp-includes/load.php:1278
shutdown_action_hook()
wp-includes/load.php:1278

Update: Fixed in edc52fa. The issue is that every time seek() is called, it will also call get_updated_html(). So I introduced a get_final_updated_html() which is intended to be called at the very end of iterating over the document.

@westonruter westonruter force-pushed the add/embed-optimizer-min-height-reservation branch from d10dd3c to edc52fa Compare August 18, 2024 22:12
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.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we also have JavaScript hooks you could leverage if the idea is to make this extensible.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These aren't available as script modules though, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, but they could be added separately to filter the an args array here for example

setStorageLock( getCurrentTime() );
for ( const extension of extensions ) {
if ( extension.finalize instanceof Function ) {
extension.finalize( {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So extensions add their own data by directly modifying the passed urlMetric? Why not have them return the new data instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As this PR is currently implemented, Embed Optimizer overrides the boundingClientRect with the final rect reported the ResizeObserver for embeds. But since then I've realized this isn't ideal because we can't know if the boundingClientRect was the initial size or the final size. So in that case I think it would be better if the schema is made extensible (cf. #1490 (comment)) so that Embed Optimizer can add a resizedBoundingClientRect which we then exclusively look for when setting the min-height. So in that case, the extension's finalize function could just return an object that gets merged on top of the urlMetric. But it seems there could be use cases for extensions to still be able modify the entire object?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think returning an object and using structuredClone here to avoid unintended modifications is cleaner.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, sounds good. But using structuredClone() where? We wouldn't need to pass in the urlMetric anymore to the finalize function, so would a clone be needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Er wait, we do need to pass the urlMetric because Embed Optimizer needs to amend Element data in the elements key.

westonruter and others added 2 commits August 22, 2024 10:50
Co-authored-by: swissspidy <[email protected]>
… add/embed-optimizer-min-height-reservation
}

/**
* Initialize.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Initialize.
* Finalize.

This is needed because get_json_params() can return null. Also, no need to force the request body to be JSON.

See 1da219f
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blocked [Plugin] Embed Optimizer Issues for the Embed Optimizer plugin (formerly Auto Sizes) [Plugin] Optimization Detective Issues for the Optimization Detective plugin [Type] Enhancement A suggestion for improvement of an existing feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Embeds cause layout shift which can be reduced with Optimization Detective
3 participants