-
Notifications
You must be signed in to change notification settings - Fork 133
Add ability to prime URL metrics #1850
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
base: trunk
Are you sure you want to change the base?
Conversation
…ing between parent and iframe
…ints logic to dedicated functions
… generating a new UUID
… visibility hidden
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## trunk #1850 +/- ##
==========================================
- Coverage 68.81% 65.86% -2.95%
==========================================
Files 90 93 +3
Lines 8006 8394 +388
==========================================
+ Hits 5509 5529 +20
- Misses 2497 2865 +368
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
'prime_url_metrics_verification_token', | ||
odPrimeUrlMetricsVerificationToken | ||
); | ||
} |
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.
Authentication for REST API
-
WP Nonce Limitation: The default WordPress (WP) nonce does not function correctly when generated for the parent page and then passed to an iframe for REST API requests.
-
Custom Token Authentication: To address this, I have added a custom token-based authentication mechanism. This generates a time-limited token used to authenticate REST API requests made via the iframe.
In #1835 PR, WP nonces are introduced for REST API requests for logged-in users. This may allow us to eliminate the custom token authentication if URL metrics are collected exclusively from logged-in users.
}; | ||
|
||
// Load the iframe | ||
iframe.src = task.url; |
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.
Currently if the IFRAME
shares the same origin as the parent, then it allows it to access the parent session. This ensures that the user session in the page loaded within the iframe (which is a frontend page) matches the logged-in user of the WordPress dashboard.
But if the WordPress admin dashboard and the frontend have different origins, WP nonces won’t work for REST API authentication because the iframe will not recognize the logged-in session. As the different origin does not allow iframe to access parents session. For context I am talking about the REST nonce introduced in #1835.
iframe.style.transform = 'scale(0.05)'; | ||
iframe.style.transformOrigin = '0 0'; | ||
iframe.style.pointerEvents = 'none'; | ||
iframe.style.opacity = '0.000001'; |
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.
As the detect.js
requires the iframe to be visible in the viewport to resolve the onLCP promise. Traditional methods like moving the iframe off-screen using translate
, setting visibility: hidden
, or opacity: 0
cause the promise to hang.
performance/plugins/optimization-detective/detect.js
Lines 496 to 503 in 6ca5c4b
// Obtain at least one LCP candidate. More may be reported before the page finishes loading. | |
await new Promise( ( resolve ) => { | |
onLCP( | |
( /** @type LCPMetric */ metric ) => { | |
lcpMetricCandidates.push( metric ); | |
resolve(); | |
}, | |
{ |
I am using a workaround using the following CSS to keep the iframe minimally visible and functional:
position: fixed;
top: 0px;
left: 0px;
transform: scale(0.05);
transform-origin: 0px 0px;
pointer-events: none;
opacity: 1e-6;
z-index: -9999;
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 was wondering how you were going to resolve solve for this!
'OD_PRIME_URL_METRICS_REQUEST_SUCCESS', | ||
'*' | ||
); | ||
resolve(); |
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.
Parent and IFRAME
communication is handled via postMessage. A message is sent to the parent, and the promise resolves immediately.
If the promise isn't resolved immediately, navigating to a new URL causes the code following the promise to never execute. This is because changing the iframe.src
does not trigger events like pagehide
, pageswap
, or visibilitychange
.
performance/plugins/optimization-detective/detect.js
Lines 568 to 569 in 6ca5c4b
// Wait for the page to be hidden. | |
await new Promise( ( resolve ) => { |
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 wonder. Do we even need to post a message here? As soon as the iframe
is destroyed won't it automatically cause the URL Metric to be sent, right?
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.
The problem is we need to signal the parent that we can move to next URL or breakpoint using postMessage
as the load
event can't be used. Check this comment for detailed explanation #1850 (comment) .
Will it makes sense to send the postMessage
after the navigator.sendBeacon
then?
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, I think it makes sense to send the message after the beacon is sent, definitely.
Co-authored-by: Weston Ruter <[email protected]>
Co-authored-by: Weston Ruter <[email protected]>
* | ||
* @since n.e.x.t | ||
* | ||
* @return non-empty-string|null Source. |
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.
The literal values could be used instead:
* @return non-empty-string|null Source. | |
* @return 'visitor'|'user'|'synthetic'|null Source. |
source: restApiNonce ? 'user' : 'visitor', | ||
}; | ||
|
||
if ( odPrimeUrlMetricsVerificationToken ) { | ||
urlMetric.source = 'synthetic'; | ||
} |
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.
Populating the source
should probably be done in the endpoint itself. It should be a readonly
param.
'source' => array( | ||
'description' => __( 'The source of the URL Metric.', 'optimization-detective' ), | ||
'type' => 'string', | ||
'required' => false, | ||
'enum' => array( | ||
'visitor', | ||
'user', | ||
'synthetic', | ||
), | ||
), |
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.
Should also be readonly
, populated in the endpoint callback here:
performance/plugins/optimization-detective/storage/class-od-rest-url-metrics-store-endpoint.php
Lines 214 to 217 in ff8b62b
// Now supply the readonly args which were omitted from the REST API params due to being `readonly`. | |
'timestamp' => microtime( true ), | |
'uuid' => wp_generate_uuid4(), | |
'etag' => $request->get_param( 'current_etag' ), |
The value can be synthetic
if the prime_url_metrics_verification_token
param is present, or else user
if is_user_logged_in()
. Otherwise, it can be visitor
.
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.
Done in 5e64b39
Co-authored-by: Weston Ruter <[email protected]>
Co-authored-by: Weston Ruter <[email protected]>
…oved code quality - Rename endpoint classes and functions to consistently use "priming mode" terminology - Add TypeScript definitions for URL metrics priming features - Extract verification token logic into a dedicated function - Fix navigation issue in classic editor when using Update button - Improve JavaScript documentation with comprehensive JSDoc comments - Replace nested loops with flatMap for more concise code in JavaScript files - Use more descriptive variable names throughout the codebase - Add validation for cursor parameters to prevent potential issues - Reorganize code structure for better readability and maintenance
Re: #1850 (comment)
I don't think that is safe because mobile and phablet may have the same LCP element, but then desktop has something else. Or desktop and tablet could have the same LCP element, but mobile and tablet have something else. This would mean the LCP image would be incorrectly prioritized more often. The current logic is to only add But adding
I think this highlights how uncommon the "phablet" viewport is, or rather that the "small" breakpoint in Gutenberg is very uncommon:
When asking Gemini about devices with a viewport width between 480px and 600px, it says:
And:
Now regards to this:
These are supposed to be the max widths for the breakpoints, not the most common viewports. We should align them with the breakpoints defined in the Gutenberg's
Given this, we may need to introduce a new breakpoint lower than 480px so that we can capture URL Metrics for more accurate optimizations since many flavors of mobile fit in the 0-480px viewport group. I asked Gemini to give me a bar chart showing the popular viewport widths (which I do not know whether it is accurate!): ![]() Importantly: there are no popular devices between the 412px and 782px. So the current 600px max breakpoint width is actually going to be largely unused on real sites. It would seem more useful to add a breakpoint that subdivides the large mobile set of mobile viewports, for large smartphone and small smartphone: ![]() According to the common WordPress breakpoints referenced in that Gutenberg stylesheet, it seems like 380px would be the logical choice to divide those two groups. The devices that fall into those two groups, again per Gemini:
All this to say, we could add
This would allow you to use the following device viewports as you had compiled above: $device_viewports = array(
array( 'width' => 360, 'height' => 780 ), // Small Smartphone.
array( 'width' => 414, 'height' => 896 ), // Large Smartphone.
array( 'width' => 768, 'height' => 1024 ), // Tablet.
array( 'width' => 1920, 'height' => 1080 ), // Desktop.
); Each would get assigned to a different viewport group. And given the relative unpopularity of devices between 480px and 600px, it probably doesn't make sense to add a device specifically to test it. We even might want to consider removing |
Summary
Fixes #1311
Relevant technical choices
This PR introduces a mechanism for priming URL metrics across the site:
TODOS:
Demos
Settings page:
UI.mp4
Saving post in Block Editor:
Block.Editor.mp4
Saving post in Classic Editor:
Classic.Editor.mp4
CLI:
cli-demo.mp4