Skip to content

Commit 7b57489

Browse files
authored
Merge pull request #1641 from WordPress/fix/od-with-page-caching-3
Send post ID of queried object or first post in loop in URL Metric storage request to schedule page cache validation
2 parents df29967 + b76d1d3 commit 7b57489

File tree

10 files changed

+384
-36
lines changed

10 files changed

+384
-36
lines changed

plugins/optimization-detective/class-od-url-metric.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,8 @@ public function set_group( OD_URL_Metric_Group $group ): void {
154154
/**
155155
* Gets JSON schema for URL Metric.
156156
*
157+
* @since 0.1.0
158+
*
157159
* @todo Cache the return value?
158160
*
159161
* @return array<string, mixed> Schema.
@@ -407,6 +409,8 @@ public function get( string $key ) {
407409
/**
408410
* Gets UUID.
409411
*
412+
* @since 0.6.0
413+
*
410414
* @return string UUID.
411415
*/
412416
public function get_uuid(): string {
@@ -416,6 +420,8 @@ public function get_uuid(): string {
416420
/**
417421
* Gets URL.
418422
*
423+
* @since 0.1.0
424+
*
419425
* @return string URL.
420426
*/
421427
public function get_url(): string {
@@ -425,6 +431,8 @@ public function get_url(): string {
425431
/**
426432
* Gets viewport data.
427433
*
434+
* @since 0.1.0
435+
*
428436
* @return ViewportRect Viewport data.
429437
*/
430438
public function get_viewport(): array {
@@ -434,6 +442,8 @@ public function get_viewport(): array {
434442
/**
435443
* Gets viewport width.
436444
*
445+
* @since 0.1.0
446+
*
437447
* @return int Viewport width.
438448
*/
439449
public function get_viewport_width(): int {
@@ -443,6 +453,8 @@ public function get_viewport_width(): int {
443453
/**
444454
* Gets timestamp.
445455
*
456+
* @since 0.1.0
457+
*
446458
* @return float Timestamp.
447459
*/
448460
public function get_timestamp(): float {
@@ -452,6 +464,8 @@ public function get_timestamp(): float {
452464
/**
453465
* Gets elements.
454466
*
467+
* @since 0.1.0
468+
*
455469
* @return OD_Element[] Elements.
456470
*/
457471
public function get_elements(): array {
@@ -469,6 +483,8 @@ function ( array $element ): OD_Element {
469483
/**
470484
* Specifies data which should be serialized to JSON.
471485
*
486+
* @since 0.1.0
487+
*
472488
* @return Data Exports to be serialized by json_encode().
473489
*/
474490
public function jsonSerialize(): array {

plugins/optimization-detective/detect.js

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ function extendElementData( xpath, properties ) {
239239
* @param {string} args.restApiEndpoint URL for where to send the detection data.
240240
* @param {string} args.currentUrl Current URL.
241241
* @param {string} args.urlMetricSlug Slug for URL Metric.
242+
* @param {number|null} args.cachePurgePostId Cache purge post ID.
242243
* @param {string} args.urlMetricHMAC HMAC for URL Metric storage.
243244
* @param {URLMetricGroupStatus[]} args.urlMetricGroupStatuses URL Metric group statuses.
244245
* @param {number} args.storageLockTTL The TTL (in seconds) for the URL Metric storage lock.
@@ -253,6 +254,7 @@ export default async function detect( {
253254
restApiEndpoint,
254255
currentUrl,
255256
urlMetricSlug,
257+
cachePurgePostId,
256258
urlMetricHMAC,
257259
urlMetricGroupStatuses,
258260
storageLockTTL,
@@ -457,16 +459,21 @@ export default async function detect( {
457459
continue;
458460
}
459461

460-
const isLCP =
461-
elementIntersection.target === lcpMetric?.entries[ 0 ]?.element;
462+
const element = /** @type {Element|null} */ (
463+
lcpMetric?.entries[ 0 ]?.element
464+
);
465+
const isLCP = elementIntersection.target === element;
462466

463467
/** @type {ElementData} */
464468
const elementData = {
465469
isLCP,
466470
isLCPCandidate: !! lcpMetricCandidates.find(
467-
( lcpMetricCandidate ) =>
468-
lcpMetricCandidate.entries[ 0 ]?.element ===
469-
elementIntersection.target
471+
( lcpMetricCandidate ) => {
472+
const candidateElement = /** @type {Element|null} */ (
473+
lcpMetricCandidate.entries[ 0 ]?.element
474+
);
475+
return candidateElement === elementIntersection.target;
476+
}
470477
),
471478
xpath,
472479
intersectionRatio: elementIntersection.intersectionRatio,
@@ -532,6 +539,12 @@ export default async function detect( {
532539

533540
const url = new URL( restApiEndpoint );
534541
url.searchParams.set( 'slug', urlMetricSlug );
542+
if ( typeof cachePurgePostId === 'number' ) {
543+
url.searchParams.set(
544+
'cache_purge_post_id',
545+
cachePurgePostId.toString()
546+
);
547+
}
535548
url.searchParams.set( 'hmac', urlMetricHMAC );
536549
navigator.sendBeacon(
537550
url,

plugins/optimization-detective/detection.php

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,54 @@
1010
exit; // Exit if accessed directly.
1111
}
1212

13+
/**
14+
* Obtains the ID for a post related to this response so that page caches can be told to invalidate their cache.
15+
*
16+
* If the queried object for the response is a post, then that post's ID is used. Otherwise, it uses the ID of the first
17+
* post in The Loop.
18+
*
19+
* When the queried object is a post (e.g. is_singular, is_posts_page, is_front_page w/ show_on_front=page), then this
20+
* is the perfect match. A page caching plugin will be able to most reliably invalidate the cache for a URL via
21+
* this ID if the relevant actions are triggered for the post (e.g. clean_post_cache, save_post, transition_post_status).
22+
*
23+
* Otherwise, if the response is an archive page or the front page where show_on_front=posts (i.e. is_home), then
24+
* there is no singular post object that represents the URL. In this case, we obtain the first post in the main
25+
* loop. By triggering the relevant actions for this post ID, page caches will have their best shot at invalidating
26+
* the related URLs. Page caching plugins which leverage surrogate keys will be the most reliable here. Otherwise,
27+
* caching plugins may just resort to automatically purging the cache for the homepage whenever any post is edited,
28+
* which is better than nothing.
29+
*
30+
* There should not be any situation by default in which a page optimized with Optimization Detective does not have such
31+
* a post available for cache purging. As seen in {@see od_can_optimize_response()}, when such a post ID is not
32+
* available for cache purging then it returns false, as it also does in another case like if is_404().
33+
*
34+
* @since n.e.x.t
35+
* @access private
36+
*
37+
* @return int|null Post ID or null if none found.
38+
*/
39+
function od_get_cache_purge_post_id(): ?int {
40+
$queried_object = get_queried_object();
41+
if ( $queried_object instanceof WP_Post ) {
42+
return $queried_object->ID;
43+
}
44+
45+
global $wp_query;
46+
if (
47+
$wp_query instanceof WP_Query
48+
&&
49+
$wp_query->post_count > 0
50+
&&
51+
isset( $wp_query->posts[0] )
52+
&&
53+
$wp_query->posts[0] instanceof WP_Post
54+
) {
55+
return $wp_query->posts[0]->ID;
56+
}
57+
58+
return null;
59+
}
60+
1361
/**
1462
* Prints the script for detecting loaded images and the LCP element.
1563
*
@@ -32,6 +80,8 @@ function od_get_detection_script( string $slug, OD_URL_Metric_Group_Collection $
3280
*/
3381
$extension_module_urls = (array) apply_filters( 'od_extension_module_urls', array() );
3482

83+
$cache_purge_post_id = od_get_cache_purge_post_id();
84+
3585
$current_url = od_get_current_url();
3686
$detect_args = array(
3787
'minViewportAspectRatio' => od_get_minimum_viewport_aspect_ratio(),
@@ -41,7 +91,8 @@ function od_get_detection_script( string $slug, OD_URL_Metric_Group_Collection $
4191
'restApiEndpoint' => rest_url( OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ),
4292
'currentUrl' => $current_url,
4393
'urlMetricSlug' => $slug,
44-
'urlMetricHMAC' => od_get_url_metrics_storage_hmac( $slug, $current_url ),
94+
'cachePurgePostId' => od_get_cache_purge_post_id(),
95+
'urlMetricHMAC' => od_get_url_metrics_storage_hmac( $slug, $current_url, $cache_purge_post_id ),
4596
'urlMetricGroupStatuses' => array_map(
4697
static function ( OD_URL_Metric_Group $group ): array {
4798
return array(

plugins/optimization-detective/optimization.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,10 @@ function od_can_optimize_response(): bool {
120120
// users, additional elements will be present like the script from wp_customize_support_script() which will
121121
// interfere with the XPath indices. Note that od_get_normalized_query_vars() is varied by is_user_logged_in()
122122
// so membership sites and e-commerce sites will still be able to be optimized for their normal visitors.
123-
current_user_can( 'customize' )
123+
current_user_can( 'customize' ) ||
124+
// Page caching plugins can only reliably be told to invalidate a cached page when a post is available to trigger
125+
// the relevant actions on.
126+
null !== od_get_cache_purge_post_id()
124127
);
125128

126129
/**

plugins/optimization-detective/readme.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,18 @@ add_filter(
219219

220220
See also [example usage](https://github.com/WordPress/performance/blob/6bb8405c5c446e3b66c2bfa3ae03ba61b188bca2/plugins/embed-optimizer/hooks.php#L128-L144) in Embed Optimizer. Note in particular the structure of the plugin’s [detect.js](https://github.com/WordPress/performance/blob/trunk/plugins/embed-optimizer/detect.js) script module, how it exports `initialize` and `finalize` functions which Optimization Detective then calls when the page loads and when the page unloads, at which time the URL Metric is constructed and sent to the server for storage. Refer also to the [TypeScript type definitions](https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/types.ts).
221221

222+
**Action:** `od_url_metric_stored` (argument: `OD_URL_Metric_Store_Request_Context`)
223+
224+
Fires whenever a URL Metric was successfully stored.
225+
226+
The supplied context object includes these properties:
227+
228+
* `$request`: The `WP_REST_Request` for storing the URL Metric.
229+
* `$post_id`: The post ID for the `od_url_metric` post.
230+
* `$url_metric`: The newly-stored URL Metric.
231+
* `$url_metric_group`: The viewport group that the URL Metric was added to.
232+
* `$url_metric_group_collection`: The `OD_URL_Metric_Group_Collection` instance to which the URL Metric was added.
233+
222234
== Installation ==
223235

224236
= Installation from within WordPress =

plugins/optimization-detective/storage/data.php

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,15 @@ function od_get_url_metrics_slug( array $query_vars ): string {
150150
*
151151
* @see od_verify_url_metrics_storage_hmac()
152152
* @see od_get_url_metrics_slug()
153+
* @todo This should also include an ETag as a parameter. See <https://github.com/WordPress/performance/issues/1466>.
153154
*
154-
* @param string $slug Slug (hash of normalized query vars).
155-
* @param string $url URL.
156-
*
155+
* @param string $slug Slug (hash of normalized query vars).
156+
* @param string $url URL.
157+
* @param int|null $cache_purge_post_id Cache purge post ID.
157158
* @return string HMAC.
158159
*/
159-
function od_get_url_metrics_storage_hmac( string $slug, string $url ): string {
160-
$action = "store_url_metric:$slug:$url";
160+
function od_get_url_metrics_storage_hmac( string $slug, string $url, ?int $cache_purge_post_id = null ): string {
161+
$action = "store_url_metric:$slug:$url:$cache_purge_post_id";
161162
return wp_hash( $action, 'nonce' );
162163
}
163164

@@ -170,14 +171,14 @@ function od_get_url_metrics_storage_hmac( string $slug, string $url ): string {
170171
* @see od_get_url_metrics_storage_hmac()
171172
* @see od_get_url_metrics_slug()
172173
*
173-
* @param string $hmac HMAC.
174-
* @param string $slug Slug (hash of normalized query vars).
175-
* @param String $url URL.
176-
*
174+
* @param string $hmac HMAC.
175+
* @param string $slug Slug (hash of normalized query vars).
176+
* @param String $url URL.
177+
* @param int|null $cache_purge_post_id Cache purge post ID.
177178
* @return bool Whether the HMAC is valid.
178179
*/
179-
function od_verify_url_metrics_storage_hmac( string $hmac, string $slug, string $url ): bool {
180-
return hash_equals( od_get_url_metrics_storage_hmac( $slug, $url ), $hmac );
180+
function od_verify_url_metrics_storage_hmac( string $hmac, string $slug, string $url, ?int $cache_purge_post_id = null ): bool {
181+
return hash_equals( od_get_url_metrics_storage_hmac( $slug, $url, $cache_purge_post_id ), $hmac );
181182
}
182183

183184
/**

plugins/optimization-detective/storage/rest-api.php

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,22 +37,28 @@
3737
*/
3838
function od_register_endpoint(): void {
3939

40+
// The slug and cache_purge_post_id args are further validated via the validate_callback for the 'hmac' parameter,
41+
// they are provided as input with the 'url' argument to create the HMAC by the server.
4042
$args = array(
41-
'slug' => array(
43+
'slug' => array(
4244
'type' => 'string',
4345
'description' => __( 'An MD5 hash of the query args.', 'optimization-detective' ),
4446
'required' => true,
4547
'pattern' => '^[0-9a-f]{32}$',
46-
// This is further validated via the validate_callback for the 'hmac' parameter, as it is provided as input
47-
// with the 'url' argument to create the HMAC by the server. which then is verified to match in the REST API request.
4848
),
49-
'hmac' => array(
49+
'cache_purge_post_id' => array(
50+
'type' => 'integer',
51+
'description' => __( 'Cache purge post ID.', 'optimization-detective' ),
52+
'required' => false,
53+
'minimum' => 1,
54+
),
55+
'hmac' => array(
5056
'type' => 'string',
5157
'description' => __( 'HMAC originally computed by server required to authorize the request.', 'optimization-detective' ),
5258
'required' => true,
5359
'pattern' => '^[0-9a-f]+$',
5460
'validate_callback' => static function ( string $hmac, WP_REST_Request $request ) {
55-
if ( ! od_verify_url_metrics_storage_hmac( $hmac, $request->get_param( 'slug' ), $request->get_param( 'url' ) ) ) {
61+
if ( ! od_verify_url_metrics_storage_hmac( $hmac, $request['slug'], $request['url'], $request['cache_purge_post_id'] ?? null ) ) {
5662
return new WP_Error( 'invalid_hmac', __( 'URL Metrics HMAC verification failure.', 'optimization-detective' ) );
5763
}
5864
return true;
@@ -202,6 +208,16 @@ function od_handle_rest_request( WP_REST_Request $request ) {
202208
}
203209
$post_id = $result;
204210

211+
// Schedule an event in 10 minutes to trigger an invalidation of the page cache (hopefully).
212+
$cache_purge_post_id = $request->get_param( 'cache_purge_post_id' );
213+
if ( is_int( $cache_purge_post_id ) && false === wp_next_scheduled( 'od_trigger_page_cache_invalidation', array( $cache_purge_post_id ) ) ) {
214+
wp_schedule_single_event(
215+
time() + 10 * MINUTE_IN_SECONDS,
216+
'od_trigger_page_cache_invalidation',
217+
array( $cache_purge_post_id )
218+
);
219+
}
220+
205221
/**
206222
* Fires whenever a URL Metric was successfully stored.
207223
*
@@ -226,3 +242,49 @@ function od_handle_rest_request( WP_REST_Request $request ) {
226242
)
227243
);
228244
}
245+
246+
/**
247+
* Triggers actions for page caches to invalidate their caches related to the supplied cache purge post ID.
248+
*
249+
* This is intended to flush any page cache for the URL after the new URL Metric was submitted so that the optimizations
250+
* which depend on that URL Metric can start to take effect.
251+
*
252+
* @since n.e.x.t
253+
* @access private
254+
*
255+
* @param int $cache_purge_post_id Cache purge post ID.
256+
*/
257+
function od_trigger_page_cache_invalidation( int $cache_purge_post_id ): void {
258+
$post = get_post( $cache_purge_post_id );
259+
if ( ! ( $post instanceof WP_Post ) ) {
260+
return;
261+
}
262+
263+
// Fire actions that page caching plugins listen to flush caches.
264+
265+
/*
266+
* The clean_post_cache action is used to flush page caches by:
267+
* - Pantheon Advanced Cache <https://github.com/pantheon-systems/pantheon-advanced-page-cache/blob/e3b5552b0cb9268d9b696cb200af56cc044920d9/pantheon-advanced-page-cache.php#L185>
268+
* - WP Super Cache <https://github.com/Automattic/wp-super-cache/blob/73b428d2fce397fd874b3056ad3120c343bc1a0c/wp-cache-phase2.php#L1615>
269+
* - Batcache <https://github.com/Automattic/batcache/blob/ed0e6b2d9bcbab3924c49a6c3247646fb87a0957/batcache.php#L18>
270+
*/
271+
/** This action is documented in wp-includes/post.php. */
272+
do_action( 'clean_post_cache', $post->ID, $post );
273+
274+
/*
275+
* The transition_post_status action is used to flush page caches by:
276+
* - Jetpack Boost <https://github.com/Automattic/jetpack-boost-production/blob/4090a3f9414c2171cd52d8a397f00b0d1151475f/app/modules/optimizations/page-cache/pre-wordpress/Boost_Cache.php#L76>
277+
* - WP Super Cache <https://github.com/Automattic/wp-super-cache/blob/73b428d2fce397fd874b3056ad3120c343bc1a0c/wp-cache-phase2.php#L1616>
278+
* - LightSpeed Cache <https://github.com/litespeedtech/lscache_wp/blob/7c707469b3c88b4f45d9955593b92f9aeaed54c3/src/purge.cls.php#L68>
279+
*/
280+
/** This action is documented in wp-includes/post.php. */
281+
do_action( 'transition_post_status', $post->post_status, $post->post_status, $post );
282+
283+
/*
284+
* The clean_post_cache action is used to flush page caches by:
285+
* - W3 Total Cache <https://github.com/BoldGrid/w3-total-cache/blob/ab08f104294c6a8dcb00f1c66aaacd0615c42850/Util_AttachToActions.php#L32>
286+
* - WP Rocket <https://github.com/wp-media/wp-rocket/blob/e5bca6673a3669827f3998edebc0c785210fe561/inc/common/purge.php#L283>
287+
*/
288+
/** This action is documented in wp-includes/post.php. */
289+
do_action( 'save_post', $post->ID, $post, /* $update */ true );
290+
}

0 commit comments

Comments
 (0)