Skip to content

Commit ed4f1e6

Browse files
authored
Merge branch 'trunk' into add/admin-bar-skipping
2 parents 4618d11 + 762e52a commit ed4f1e6

File tree

4 files changed

+680
-0
lines changed

4 files changed

+680
-0
lines changed
Lines changed: 331 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,331 @@
1+
<?php
2+
/**
3+
* Helper function to detect if static assets have effective caching headers.
4+
*
5+
* @package performance-lab
6+
* @since n.e.x.t
7+
*/
8+
9+
// @codeCoverageIgnoreStart
10+
if ( ! defined( 'ABSPATH' ) ) {
11+
exit; // Exit if accessed directly.
12+
}
13+
// @codeCoverageIgnoreEnd
14+
15+
/**
16+
* Callback for the effective caching headers test.
17+
*
18+
* @since n.e.x.t
19+
* @access private
20+
*
21+
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
22+
*/
23+
function perflab_effective_asset_cache_headers_assets_test(): array {
24+
$result = array(
25+
'label' => __( 'Your site serves static assets with an effective caching strategy', 'performance-lab' ),
26+
'status' => 'good',
27+
'badge' => array(
28+
'label' => __( 'Performance', 'performance-lab' ),
29+
'color' => 'blue',
30+
),
31+
'description' => sprintf(
32+
'<p>%s</p>',
33+
esc_html__(
34+
'Serving static assets with far-future expiration headers improves performance by allowing browsers to cache files for a long time, reducing repeated requests.',
35+
'performance-lab'
36+
)
37+
),
38+
'actions' => '',
39+
'test' => 'is_effective_asset_cache_headers_enabled',
40+
);
41+
42+
// List of assets to check.
43+
$assets = array(
44+
includes_url( 'js/wp-embed.min.js' ),
45+
includes_url( 'css/buttons.min.css' ),
46+
includes_url( 'fonts/dashicons.woff2' ),
47+
includes_url( 'images/media/video.png' ),
48+
);
49+
50+
/**
51+
* Filters the list of assets to check for effective caching headers.
52+
*
53+
* @since n.e.x.t
54+
*
55+
* @param string[] $assets List of asset URLs to check.
56+
*/
57+
$assets = apply_filters( 'perflab_effective_asset_cache_headers_assets_to_check', $assets );
58+
$assets = array_filter( (array) $assets, 'is_string' );
59+
60+
// Check if effective caching headers are enabled for all assets.
61+
$results = perflab_effective_asset_cache_headers_check_assets( $assets );
62+
63+
if ( 'good' !== $results['final_status'] ) {
64+
$result['status'] = $results['final_status'];
65+
$result['label'] = __( 'Your site does not serve static assets with an effective caching strategy', 'performance-lab' );
66+
67+
if ( count( $results['details'] ) > 0 ) {
68+
$result['actions'] = sprintf(
69+
'<p>%s</p>%s<p>%s</p>',
70+
esc_html__( 'The following file types do not have the recommended effective Cache-Control or Expires headers. Consider adding or adjusting Cache-Control or Expires headers for these asset types.', 'performance-lab' ),
71+
perflab_effective_asset_cache_headers_get_status_table( $results['details'] ),
72+
esc_html__( 'Note: "Conditionally cached" means that the browser can re-validate the resource using ETag or Last-Modified headers. This results in fewer full downloads but still requires the browser to make requests, unlike far-future expiration headers that allow the browser to fully rely on its local cache for a longer duration.', 'performance-lab' )
73+
);
74+
}
75+
$result['actions'] .= sprintf(
76+
'<p>%s</p>',
77+
esc_html__( 'Effective Cache-Control or Expires headers can be added or adjusted with a small configuration change by your hosting provider.', 'performance-lab' )
78+
);
79+
}
80+
81+
return $result;
82+
}
83+
84+
/**
85+
* Checks if effective caching headers are enabled for a list of assets.
86+
*
87+
* @since n.e.x.t
88+
* @access private
89+
*
90+
* @param string[] $assets List of asset URLs to check.
91+
* @return array{final_status: string, details: array{filename: string, reason: string}[]} Final status and details.
92+
*/
93+
function perflab_effective_asset_cache_headers_check_assets( array $assets ): array {
94+
$final_status = 'good';
95+
$fail_details = array(); // Array of arrays with 'filename' and 'reason'.
96+
97+
foreach ( $assets as $asset ) {
98+
$response = wp_remote_get( $asset, array( 'sslverify' => false ) );
99+
100+
// Extract filename from the URL.
101+
$path_info = pathinfo( (string) wp_parse_url( $asset, PHP_URL_PATH ) );
102+
$filename = $path_info['basename'] ?? basename( $asset );
103+
104+
if ( is_wp_error( $response ) ) {
105+
// Can't determine headers if request failed, consider it a fail.
106+
$final_status = 'recommended';
107+
$fail_details[] = array(
108+
'filename' => $filename,
109+
'reason' => __( 'Could not retrieve headers', 'performance-lab' ),
110+
);
111+
continue;
112+
}
113+
114+
$headers = wp_remote_retrieve_headers( $response );
115+
if ( ! is_object( $headers ) && 0 === count( $headers ) ) {
116+
// No valid headers retrieved.
117+
$final_status = 'recommended';
118+
$fail_details[] = array(
119+
'filename' => $filename,
120+
'reason' => __( 'No valid headers retrieved', 'performance-lab' ),
121+
);
122+
continue;
123+
}
124+
125+
$check = perflab_effective_asset_cache_headers_check_headers( $headers );
126+
if ( $check['passed'] ) {
127+
// This asset passed effective caching headers test, no action needed.
128+
continue;
129+
}
130+
131+
// If not passed, decide whether to try conditional request.
132+
if ( $check['missing_max_age'] ) {
133+
// Only if no effective caching headers at all, we try conditional request.
134+
$conditional_pass = perflab_effective_asset_cache_headers_try_conditional_request( $asset, $headers );
135+
$final_status = 'recommended';
136+
if ( ! $conditional_pass ) {
137+
$fail_details[] = array(
138+
'filename' => $filename,
139+
'reason' => __( 'No effective caching headers and no conditional caching', 'performance-lab' ),
140+
);
141+
} else {
142+
$fail_details[] = array(
143+
'filename' => $filename,
144+
'reason' => __( 'No effective caching headers but conditionally cached', 'performance-lab' ),
145+
);
146+
}
147+
} else {
148+
// If there's a max-age or expires but below threshold, we skip conditional.
149+
$final_status = 'recommended';
150+
$fail_details[] = array(
151+
'filename' => $filename,
152+
'reason' => $check['reason'],
153+
);
154+
}
155+
}
156+
157+
return array(
158+
'final_status' => $final_status,
159+
'details' => $fail_details,
160+
);
161+
}
162+
163+
/**
164+
* Checks if effective caching headers are enabled.
165+
*
166+
* @since n.e.x.t
167+
* @access private
168+
*
169+
* @param WpOrg\Requests\Utility\CaseInsensitiveDictionary|array<string, string|array<string>> $headers Response headers.
170+
* @return array{passed: bool, reason: string, missing_max_age: bool} Detailed result of the check.
171+
*/
172+
function perflab_effective_asset_cache_headers_check_headers( $headers ): array {
173+
/**
174+
* Filters the threshold for effective caching headers.
175+
*
176+
* @since n.e.x.t
177+
*
178+
* @param int $threshold Threshold in seconds.
179+
*/
180+
$threshold = apply_filters( 'perflab_effective_asset_cache_headers_expiration_threshold', YEAR_IN_SECONDS );
181+
182+
$cache_control = $headers['cache-control'] ?? '';
183+
$expires = $headers['expires'] ?? '';
184+
185+
// Check Cache-Control header for max-age.
186+
$max_age = 0;
187+
if ( '' !== $cache_control ) {
188+
// There can be multiple cache-control headers, we only care about max-age.
189+
foreach ( (array) $cache_control as $control ) {
190+
if ( 1 === preg_match( '/max-age\s*=\s*(\d+)/', $control, $matches ) ) {
191+
$max_age = (int) $matches[1];
192+
break;
193+
}
194+
}
195+
}
196+
197+
// If max-age meets or exceeds the threshold, we consider it good.
198+
if ( $max_age >= $threshold ) {
199+
return array(
200+
'passed' => true,
201+
'reason' => '',
202+
'missing_max_age' => false,
203+
);
204+
}
205+
206+
// If max-age is too low or not present, check Expires.
207+
if ( is_string( $expires ) && '' !== $expires ) {
208+
$expires_time = strtotime( $expires );
209+
$remaining_time = is_int( $expires_time ) ? $expires_time - time() : 0;
210+
if ( $remaining_time >= $threshold ) {
211+
// Good - Expires far in the future.
212+
return array(
213+
'passed' => true,
214+
'reason' => '',
215+
'missing_max_age' => false,
216+
);
217+
}
218+
219+
// Expires header exists but not far enough in the future.
220+
if ( $max_age > 0 ) {
221+
return array(
222+
'passed' => false,
223+
'reason' => sprintf(
224+
/* translators: 1: actual max-age value in seconds, 2: threshold in seconds */
225+
__( 'max-age below threshold (actual: %1$s seconds, threshold: %2$s seconds)', 'performance-lab' ),
226+
number_format_i18n( $max_age ),
227+
number_format_i18n( $threshold )
228+
),
229+
'missing_max_age' => false,
230+
);
231+
}
232+
return array(
233+
'passed' => false,
234+
'reason' => sprintf(
235+
/* translators: 1: actual Expires header value in seconds, 2: threshold in seconds */
236+
__( 'expires below threshold (actual: %1$s seconds, threshold: %2$s seconds)', 'performance-lab' ),
237+
number_format_i18n( $remaining_time ),
238+
number_format_i18n( $threshold )
239+
),
240+
'missing_max_age' => false,
241+
);
242+
}
243+
244+
// No max-age or expires found at all or max-age < threshold and no expires.
245+
if ( 0 === $max_age ) {
246+
return array(
247+
'passed' => false,
248+
'reason' => '',
249+
'missing_max_age' => true,
250+
);
251+
} else {
252+
// max-age was present but below threshold and no expires.
253+
return array(
254+
'passed' => false,
255+
'reason' => sprintf(
256+
/* translators: 1: actual max-age value in seconds, 2: threshold in seconds */
257+
__( 'max-age below threshold (actual: %1$s seconds, threshold: %2$s seconds)', 'performance-lab' ),
258+
number_format_i18n( $max_age ),
259+
number_format_i18n( $threshold )
260+
),
261+
'missing_max_age' => false,
262+
);
263+
}
264+
}
265+
266+
/**
267+
* Attempt a conditional request with ETag/Last-Modified.
268+
*
269+
* @since n.e.x.t
270+
* @access private
271+
*
272+
* @param string $url The asset URL.
273+
* @param WpOrg\Requests\Utility\CaseInsensitiveDictionary|array<string, string|array<string>> $headers The initial response headers.
274+
* @return bool True if a 304 response was received.
275+
*/
276+
function perflab_effective_asset_cache_headers_try_conditional_request( string $url, $headers ): bool {
277+
$etag = $headers['etag'] ?? '';
278+
$last_modified = $headers['last-modified'] ?? '';
279+
280+
$conditional_headers = array();
281+
if ( '' !== $etag ) {
282+
$conditional_headers['If-None-Match'] = $etag;
283+
}
284+
if ( '' !== $last_modified ) {
285+
$conditional_headers['If-Modified-Since'] = $last_modified;
286+
}
287+
288+
$response = wp_remote_get(
289+
$url,
290+
array(
291+
'sslverify' => false,
292+
'headers' => $conditional_headers,
293+
)
294+
);
295+
296+
if ( is_wp_error( $response ) ) {
297+
return false;
298+
}
299+
300+
$status_code = wp_remote_retrieve_response_code( $response );
301+
return ( 304 === $status_code );
302+
}
303+
304+
/**
305+
* Generate a table listing files that need effective caching headers, including reasons.
306+
*
307+
* @since n.e.x.t
308+
* @access private
309+
*
310+
* @param array<array{filename: string, reason: string}> $fail_details Array of arrays with 'filename' and 'reason'.
311+
* @return string HTML formatted table.
312+
*/
313+
function perflab_effective_asset_cache_headers_get_status_table( array $fail_details ): string {
314+
$html_table = sprintf(
315+
'<table class="widefat striped"><thead><tr><th scope="col">%s</th><th scope="col">%s</th></tr></thead><tbody>',
316+
esc_html__( 'File', 'performance-lab' ),
317+
esc_html__( 'Status', 'performance-lab' )
318+
);
319+
320+
foreach ( $fail_details as $detail ) {
321+
$html_table .= sprintf(
322+
'<tr><td>%s</td><td>%s</td></tr>',
323+
esc_html( $detail['filename'] ),
324+
esc_html( $detail['reason'] )
325+
);
326+
}
327+
328+
$html_table .= '</tbody></table>';
329+
330+
return $html_table;
331+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<?php
2+
/**
3+
* Hook callbacks used for effective caching headers.
4+
*
5+
* @package performance-lab
6+
* @since n.e.x.t
7+
*/
8+
9+
// @codeCoverageIgnoreStart
10+
if ( ! defined( 'ABSPATH' ) ) {
11+
exit; // Exit if accessed directly.
12+
}
13+
// @codeCoverageIgnoreEnd
14+
15+
/**
16+
* Adds tests to site health.
17+
*
18+
* @since n.e.x.t
19+
* @access private
20+
*
21+
* @param array{direct: array<string, array{label: string, test: string}>} $tests Site Health Tests.
22+
* @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
23+
*/
24+
function perflab_effective_asset_cache_headers_add_test( array $tests ): array {
25+
$tests['direct']['effective_asset_cache_headers'] = array(
26+
'label' => __( 'Effective Caching Headers', 'performance-lab' ),
27+
'test' => 'perflab_effective_asset_cache_headers_assets_test',
28+
);
29+
return $tests;
30+
}
31+
add_filter( 'site_status_tests', 'perflab_effective_asset_cache_headers_add_test' );

plugins/performance-lab/includes/site-health/load.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,7 @@
3131
// AVIF headers site health check.
3232
require_once __DIR__ . '/avif-headers/helper.php';
3333
require_once __DIR__ . '/avif-headers/hooks.php';
34+
35+
// Effective Asset Cache Headers site health check.
36+
require_once __DIR__ . '/effective-asset-cache-headers/helper.php';
37+
require_once __DIR__ . '/effective-asset-cache-headers/hooks.php';

0 commit comments

Comments
 (0)