-
Notifications
You must be signed in to change notification settings - Fork 99
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
Leverage URL metrics to reserve space for embeds to reduce CLS #1373
Conversation
f38247b
to
947ca41
Compare
3f38eb0
to
b3ca4ad
Compare
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]'; |
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.
It's not ideal to be constructing this XPath manually.
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.
I've opened #1407 to explore this, but it's not necessary for this PR to move forward.
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:
Call stack: Update: Addressed in #1411. |
I also saw this for some reason:
Update: See fix below in #1373 (comment) |
02b8fb3
to
b17d8ba
Compare
b17d8ba
to
76369b4
Compare
1f5e5f8
to
77bea30
Compare
$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 ); |
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 should be a helper method on OD_HTML_Tag_Processor
.
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.
See prior art in the AMP plugin: https://github.com/ampproject/amp-toolbox-php/blob/78531851c59fa5f306315372719f0064b5542cf4/src/Dom/Element.php#L49-L69
019526c
to
36e5620
Compare
Scratched code to explore using 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,
}
); |
// Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action is used | ||
// because it is the first action that fires once the theme is loaded. | ||
add_action( 'after_setup_theme', $bootstrap, PHP_INT_MIN ); | ||
/* | ||
* Wait until after the plugins have loaded and the theme has loaded. The after_setup_theme action could be | ||
* used since it is the first action that fires once the theme is loaded. However, plugins may embed this | ||
* logic inside a module which initializes even later at the init action. The earliest action that this | ||
* plugin has hooks for is the init action at the default priority of 10 (which includes the rest_api_init | ||
* action), so this is why it gets initialized at priority 9. | ||
*/ | ||
add_action( 'init', $bootstrap, 9 ); |
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.
I'm not sure I understand this. Why is the timing of this (in Image Prioritizer and Embed Optimizer) relevant for other plugins? Are you thinking of extensions to those plugins (rather than extensions to Optimization Detective)?
Another thought: Why can't this use the od_init
action?
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.
Bootstrapping at init
is all about the embeddability of these plugins inside of other plugins, which is why this whole bootstrap logic was first devised (#1081, #1159, #1185).
For example, in elementor/elementor#28844 I've created a new module for Elementor that embeds the Optimization Detective plugin and its extensions. Elementor initializes its modules at init
priority 0. Since none of the functionality in Optimization Detective needs to be initialized before init
, there's no need for the current use of after_setup_theme
to initialize. So this postpones initialization to latest possible point which opens the door to be embedded in more places.
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 still feels like a rather shaky foundation to me. For example, why does it use priority 9
?
I think I'm still not following what this is meant to do: I think I understand now why certain code needs to run before this callback, but why does certain other code need to run after this callback? For instance, why couldn't we use init
on 999999
, or even wp_loaded
or a later hook?
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.
Priority 9 is one less than the default priority of 10, as noted in the comment. There are callbacks added at this default priority in the plugin. So priority 9 is the last.
If we used init
with an even later priority, then we'd need to adjust the other init
actions' priorities as well in this plugin, for example:
performance/plugins/optimization-detective/storage/class-od-url-metrics-post-type.php
Line 48 in 94fc5e7
add_action( 'init', array( __CLASS__, 'register_post_type' ) ); |
add_action( 'init', 'embed_optimizer_add_hooks' ); |
If we went with wp_loaded
, then we couldn't use the init
action in the plugins at all, which as I understand is too late for when post types should be registered.
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.
So basically this needs to be called as late as possible, but before any of Optimization Detective's own hooks would fire?
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.
Yes, exactly. As late as possible, and hopefully after any other possible embedding plugins' modules initialize (e.g. Elementor initializes at init
priority 0
).
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.
Makes sense. Since we have full control over the code in Optimization Detective, we may want to consider moving the plugin's init
callbacks to a very late priority, so that we can also push this back even further, while still being on init
. That way we can cater for other plugins even better, since I wouldn't find it unreasonable for other plugins to load modules on init
with the default priority either.
Given the callbacks from Optimization Detective do not have to run after everything else, maybe we just use a very high number (rather than PHP_INT_MAX
which I would only use if it technically has to be the last thing).
We can open a follow up PR for this though, if you agree?
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.
Sounds good!
* plugin has hooks for is the init action at the default priority of 10 (which includes the rest_api_init | ||
* action), so this is why it gets initialized at priority 9. | ||
*/ | ||
add_action( 'init', $bootstrap, 9 ); |
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.
See other comment.
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.
See #1373 (comment)
* (the default sample size). Therefore, given the number (n) of visited elements on the page this will only | ||
* end up running n*4*3 times. | ||
* | ||
* @todo Should there be an OD_Element class which has a $url_metric property which then in turn has a $group property. Then this would only need to return array<string, OD_Element[]>. |
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.
Sounds good. For the future, ideally the different plugin modifications would happen in separate PRs too. It's more convenient all in one PR, but would be better for review, plus you could still use sub-PRs like you've done before, to e.g. build something in Embed Optimizer on top of another PR change to the Optimization Detective infrastructure.
setStorageLock( getCurrentTime() ); | ||
for ( const extension of extensions ) { | ||
if ( extension.finalize instanceof Function ) { | ||
extension.finalize( { |
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.
Looking at this old comment thread, does it still require an update? Asking since urlMetric
is still being passed below.
…ion of URL Metric object
return; | ||
} | ||
wp_admin_notice( | ||
esc_html__( 'The Image Prioritizer plugin requires a newer version of the Optimization Detective plugin. Please update your plugins.', 'image-prioritizer' ), |
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.
can we add a link to the plugin update screen here?
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.
We could, but the user may not have the capability to access the plugin update screen. Plugins may not be updatable on the environment either due to it being a read only filesystem. So these complications would seem to make adding a link maybe more trouble than its worth.
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.
ah, good point. I was trying to make this more helpful... maybe don't show the message to users that can't upgrade plugins?
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.
True, but I think this might be overkill. It would be unlikely that someone would update Image Prioritizer but not also update Optimization Detective.
* | ||
* @return string The processed HTML. | ||
*/ | ||
public function get_updated_html(): string { | ||
if ( ! $this->reached_end_of_document ) { |
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.
Nice!
Co-authored-by: Adam Silverstein <adamjs@google.com>
Co-authored-by: adamsilverstein <adamsilverstein@git.wordpress.org>
url.searchParams.set( '_wpnonce', restApiNonce ); | ||
url.searchParams.set( 'slug', urlMetricSlug ); | ||
url.searchParams.set( 'nonce', urlMetricNonce ); | ||
navigator.sendBeacon( |
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.
💚 💚 💚 great to see this use navigator.sendBeacon
(I wondered about that previously)
* | ||
* @param string[] $extension_module_urls Extension module URLs. | ||
*/ | ||
$extension_module_urls = (array) apply_filters( 'od_extension_module_urls', 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.
Great!
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.
Looks great, left a few questions
Fixes #1310
The changes in this pull request grew larger than the strict scope of reducing CLS for embeds since the work to implement this uncovered shortcomings with the current Optimization Detective API. This PR includes the following key pieces:
initialize
function. They also are able to register functionality to run just before the URL metric data is being submitted by implementing an exportedfinalize
function which amends the URL metric data as needed. This builds on Allow URL metric schema to be extended #1492 which enabled the URL metric schema to be extended. This PR includes Embed Optimizer extending the URL metric element schema to include aresizedBoundingClientRect
key which is populated with the script module via aResizeObserver
which starts observing in the module'sinitialize
function. New script modules are added simply by appending to the array passed to theod_extension_module_urls
filter.OD_URL_Metric_Group_Collection::get_all_denormalized_elements()
). In order to avoid the need to domethod_exists()
checks and the like, a newod_init
action is introduced which passes the Optimization Detective version as its only argument. Embed Optimizer and Image Prioritizer then check this passed version to see if it meets the requirements. If not, then an admin notice is displayed. If yes, then it goes ahead and loads the Optimization Detective extension code.after_setup_theme
action. In proposing embedding Optimization Detective in Elementor (Leverage URL metrics to reserve space for embeds to reduce CLS #1373), I found that modules load atinit
with priority 0. This makes the current bootstrap code unusable when it is loaded in such a module, necessitating an ugly workaround. Since Optimization Detective doesn't need to bootstrap beforeinit
anyway, the solution seems to simply be to move the bootstrapping logic toinit
as well. This is implemented in this PR by moving toinit
at priority 9 since the plugin has code that runs atinit
with the default priority of 10.Accounting for responsive embeds
Initially I was thinking the optimization logic should wait to set the
min-height
until there are URL metrics gathered for the smallest breakpoint group. For example, let's say the page is responsive and on desktop an embed is much taller than it is on mobile. If the desktop metrics are collected before mobile metrics are collected, then themin-height
could be being set for desktop could be too high for mobile, resulting in a gap below the embed on mobile. For example, consider this tweet on mobile vs desktop:The tweet on mobile here is 574px high whereas on desktop it is 825px, so if desktop metrics were only collected then the result would be a worst case gap of 251px at the bottom before the next content:
A simple way to solve this would not to set the
min-height
on thefigure.wp-block-embed
element at all, but rather to create a newstyle
rule with responsive styles that sets multiplemin-height
styles for the element according to each breakpoint. This is implemented in this PR. For example, the following CSS is added to thehead
for a Twitter embed:For follow-up
init
to an even later action. Leverage URL metrics to reserve space for embeds to reduce CLS #1373 (comment)nonce
tohmac
to disambiguate from_wpnonce
. Leverage URL metrics to reserve space for embeds to reduce CLS #1373 (comment)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