-
Notifications
You must be signed in to change notification settings - Fork 109
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
Add ability to prime URL metrics #1850
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 ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## trunk #1850 +/- ##
==========================================
- Coverage 66.36% 64.43% -1.94%
==========================================
Files 88 89 +1
Lines 6975 7318 +343
==========================================
+ Hits 4629 4715 +86
- Misses 2346 2603 +257
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. |
'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;
'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.
<h2><?php esc_html_e( 'Prime URL Metrics', 'optimization-detective' ); ?></h2> | ||
<div id="od-prime-url-metrics-container"> | ||
<button id="od-prime-url-metrics-control-button" class="button button-primary"><?php esc_html_e( 'Start', 'optimization-detective' ); ?></button> | ||
<progress id="od-prime-url-metrics-progress" value="0" max="0"></progress> |
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 I have only added one button which can start/pause/resume URL metrics priming and a progress bar indicating the current batch's progress.
ui.mp4
I would like to get suggestions on the UI to improve it.
* @param array<string, int> $cursor Cursor to resume from. | ||
* @return array<string, mixed> Batch of URLs to prime metrics for and the updated cursor. | ||
*/ | ||
function od_get_batch_for_iframe_url_metrics_priming( array $cursor ): 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.
I have added a batch function with a cursor mechanism to fetch URLs in manageable chunks. The current default batch size is set to 10, which seems too low. I would like to get suggestion on optimal batch size.
This function also got complex because of the need of handling multiple layers, including providers, provider subtypes, and pages of each subtype for batching.
* @param string[] $urls Array of exact URLs, as stored in post_title of od_url_metrics. | ||
* @return array<string, OD_URL_Metric_Group_Collection> Map of URL to its OD_URL_Metric_Group_Collection. | ||
*/ | ||
function od_get_metrics_by_post_title( array $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.
Since WP sitemaps only provide post URLs, a custom WHERE
clause was needed to be added to WP_Query
to filter out URLs that are already primed. This is done based on the post_title
.
|
||
register_rest_route( | ||
OD_REST_API_NAMESPACE, | ||
OD_PRIME_URLS_ROUTE, |
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, the helper function and REST API endpoints has been added to existing files. Should we consider moving it to a new folder named admin
for better organization?
* @param array<string> $urls Array of URLs to filter. | ||
* @return array<int, array{url: string, breakpoints: array<int, array{width: int, height: int}>}> Filtered batch of URLs. | ||
*/ | ||
function od_filter_batch_urls_for_iframe_url_metrics_priming( array $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.
What would be the good naming conventions for these helper function for priming URLs?
const handleMessage = ( event ) => { | ||
if ( event.data === 'OD_PRIME_URL_METRICS_REQUEST_SUCCESS' ) { | ||
cleanup(); | ||
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.
Instead of this, what about listening to the load
event on the iframe
?
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 reason is that the load
event fires when the document has been fully processed. However, load
does not wait for the async
and await
operations used in detect.js
. From my testing, I found that the detect
function gets stuck on await import(webVitalsLibrarySrc)
, waiting for the promise to resolve. However, before this promise is fulfilled, the load
event is triggered, causing the IFRAME
to navigate to the next URL or a breakpoint change. Changing the IFRAME
's src
destroys the JavaScript execution, preventing the code from progressing past await import(webVitalsLibrarySrc)
. Because of this reason I have used the postMessage
for communication.
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.
That makes sense, yes. In fact, the plugin explicitly waits for load
event to even start doing anything:
performance/plugins/optimization-detective/detect.js
Lines 416 to 423 in e845dd8
// Wait until the resources on the page have fully loaded. | |
await new Promise( ( resolve ) => { | |
if ( doc.readyState === 'complete' ) { | |
resolve(); | |
} else { | |
win.addEventListener( 'load', resolve, { once: true } ); | |
} | |
} ); |
So it makes sense that the iframe's load
event would fire before we do any detection.
…Improve UI to show current batch and task
Summary
Fixes #1311
Relevant technical choices
This PR introduces a new mechanism for priming URL metrics across the site. It uses a newly added submenu page in the Tools menu and automatically primes URL metrics when a post is saved in the block editor.
Demos
Settings page with debugging off:
Settings.Page.mp4
Settings page with debugging on:
Settings.Page.With.Debug.mp4
Saving post in Block Editor:
Block.Editor.mp4