Skip to content
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 Site Health test to verify that static assets are served with far-future expires #1727

Open
wants to merge 13 commits into
base: trunk
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
<?php
/**
* Helper function to detect if static assets have far-future expiration headers.
*
* @package performance-lab
* @since n.e.x.t
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

/**
* Callback for the far-future caching test.
*
* @since n.e.x.t
*
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
*/
function perflab_ffh_assets_test(): array {
$result = array(
'label' => __( 'Your site serves static assets with far-future expiration headers', 'performance-lab' ),
'status' => 'good',
'badge' => array(
'label' => __( 'Performance', 'performance-lab' ),
'color' => 'blue',
),
'description' => sprintf(
'<p>%s</p>',
esc_html__(
'Serving static assets with far-future expiration headers improves performance by allowing browsers to cache files for a long time, reducing repeated requests.',
'performance-lab'
)
),
'actions' => '',
'test' => 'is_far_future_headers_enabled',
);

// List of assets to check.
$assets = array(
plugins_url( 'far-future-headers/assets/test.css', __DIR__ ),
);
westonruter marked this conversation as resolved.
Show resolved Hide resolved

// Check if far-future headers are enabled for all assets.
$status = perflab_ffh_check_assets( $assets );

if ( 'good' !== $status ) {
$result['status'] = $status;
$result['label'] = __( 'Your site does not serve static assets with recommended far-future expiration headers', 'performance-lab' );
$result['actions'] = sprintf(
'<p>%s</p>',
esc_html__( 'Far-future Cache-Control or Expires headers can be added or adjusted with a small configuration change by your hosting provider.', 'performance-lab' )
);
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are two different checks which are getting performed one with Cache-Control, Expires and other with Etag, Last-Modified should there be different messages shown based on the checks?

Copy link
Contributor Author

@b1ink0 b1ink0 Dec 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the new 3db5886 commit a table is used to display reason for different failure cases.


return $result;
}

/**
* Checks if far-future expiration headers are enabled for a list of assets.
*
* @since n.e.x.t
*
* @param string[] $assets List of asset URLs to check.
* @return string 'good' if far-future headers are enabled for all assets, 'recommended' if some assets are missing headers.
*/
function perflab_ffh_check_assets( array $assets ): string {
$final_status = 'good';

foreach ( $assets as $asset ) {
$response = wp_remote_get( $asset, array( 'sslverify' => false ) );

if ( is_wp_error( $response ) ) {
continue;
}

$headers = wp_remote_retrieve_headers( $response );

if ( is_array( $headers ) ) {
continue;
}

if ( perflab_ffh_check_headers( $headers ) ) {
continue;
}

// If far-future headers are not enabled, attempt a conditional request.
if ( perflab_ffh_try_conditional_request( $asset, $headers ) ) {
$final_status = 'recommended';
continue;
}

$final_status = 'recommended';
}

return $final_status;
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently even if one asset fails the header check then this test will be show in the site health. Would it be better to show a table with mime type which will specifically tell for which mime type needs to add the Cache-Control headers?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A table makes sense to me.


/**
* Checks if far-future expiration headers are enabled.
*
* @since n.e.x.t
*
* @param WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers Response headers.
* @return bool True if far-future headers are enabled, false otherwise.
*/
function perflab_ffh_check_headers( WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers ): bool {
/**
* Filters the threshold for far-future headers.
*
* @since n.e.x.t
*
* @param int $threshold Threshold in seconds.
*/
$threshold = apply_filters( 'perflab_far_future_headers_threshold', YEAR_IN_SECONDS );

$cache_control = isset( $headers['cache-control'] ) ? $headers['cache-control'] : '';
$expires = isset( $headers['expires'] ) ? $headers['expires'] : '';

// Check Cache-Control header for max-age.
$max_age = 0;
if ( '' !== $cache_control ) {
// Cache-Control can have multiple directives; we only care about max-age.
$controls = is_array( $cache_control ) ? $cache_control : array( $cache_control );
foreach ( $controls as $control ) {
if ( (bool) preg_match( '/max-age\s*=\s*(\d+)/', $control, $matches ) ) {
$max_age = (int) $matches[1];
break;
}
}
}

// If max-age meets or exceeds the threshold, we consider it good.
if ( $max_age >= $threshold ) {
return true;
}

// If max-age is not sufficient, check Expires.
// Expires is a date; we want to ensure it's far in the future.
if ( is_string( $expires ) && '' !== $expires ) {
$expires_time = strtotime( $expires );
if ( (bool) $expires_time && ( $expires_time - time() ) >= $threshold ) {
return true;
}
}

return false;
}

/**
* Attempt a conditional request with ETag/Last-Modified.
*
* @param string $url The asset URL.
* @param WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers The initial response headers.
* @return bool True if a 304 response was received.
*/
function perflab_ffh_try_conditional_request( string $url, WpOrg\Requests\Utility\CaseInsensitiveDictionary $headers ): bool {
$etag = isset( $headers['etag'] ) ? $headers['etag'] : '';
$last_modified = isset( $headers['last-modified'] ) ? $headers['last-modified'] : '';

$conditional_headers = array();
if ( '' !== $etag ) {
$conditional_headers['If-None-Match'] = $etag;
}
if ( '' !== $last_modified ) {
$conditional_headers['If-Modified-Since'] = $last_modified;
}

$response = wp_remote_get(
$url,
array(
'sslverify' => false,
'headers' => $conditional_headers,
)
);

if ( is_wp_error( $response ) ) {
return false;
}

$status_code = wp_remote_retrieve_response_code( $response );
return ( 304 === $status_code );
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
/**
* Hook callbacks used for far-future headers.
*
* @package performance-lab
* @since n.e.x.t
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

/**
* Adds tests to site health.
*
* @since n.e.x.t
*
* @param array{direct: array<string, array{label: string, test: string}>} $tests Site Health Tests.
* @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
*/
function perflab_ffh_add_test( array $tests ): array {
$tests['direct']['far_future_headers'] = array(
'label' => __( 'Far-Future Caching Headers', 'performance-lab' ),
'test' => 'perflab_ffh_assets_test',
);
return $tests;
}
add_filter( 'site_status_tests', 'perflab_ffh_add_test' );
4 changes: 4 additions & 0 deletions plugins/performance-lab/includes/site-health/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,7 @@
// AVIF headers site health check.
require_once __DIR__ . '/avif-headers/helper.php';
require_once __DIR__ . '/avif-headers/hooks.php';

// Far-Future Headers site health check.
require_once __DIR__ . '/far-future-headers/helper.php';
require_once __DIR__ . '/far-future-headers/hooks.php';
Loading