Make WordPress Core


Ignore:
Timestamp:
08/31/2022 10:44:04 PM (3 years ago)
Author:
flixos90
Message:

Site Health: Introduce page cache check.

This changeset adds a new page_cache check which determines whether the site uses a full page cache, and in addition assesses the server response time. If no page cache is present and the server response time is slow, the check will suggest use of a page cache.

A few filters are included for customization of the check:

  • site_status_good_response_time_threshold filters the number of milliseconds below which the server response time is considered good. The default value is based on the server-response-time Lighthouse audit and can be altered using this filter.
  • site_status_page_cache_supported_cache_headers filters the map of supported cache headers and their callback to determine whether it was a cache hit. The default list includes commonly used cache headers, and it is filterable to support e.g. additional cache headers used by specific vendors.

Note that due to the nature of this check it is only run in production environments.

Props furi3r, westonruter, spacedmonkey, swissspidy, Clorith.
Fixes #56041.

File:
1 edited

Legend:

Unmodified
Added
Removed
  • trunk/src/wp-admin/includes/class-wp-site-health.php

    r54042 r54043  
    16681668
    16691669    /**
     1670     * Tests if a full page cache is available.
     1671     *
     1672     * @since 6.1.0
     1673     *
     1674     * @return array The test result.
     1675     */
     1676    public function get_test_page_cache() {
     1677        $description  = '<p>' . __( 'Page cache enhances the speed and performance of your site by saving and serving static pages instead of calling for a page every time a user visits.' ) . '</p>';
     1678        $description .= '<p>' . __( 'Page cache is detected by looking for an active page cache plugin as well as making three requests to the homepage and looking for one or more of the following HTTP client caching response headers:' ) . '</p>';
     1679        $description .= '<code>' . implode( '</code>, <code>', array_keys( $this->get_page_cache_headers() ) ) . '.</code>';
     1680
     1681        $result = array(
     1682            'badge'       => array(
     1683                'label' => __( 'Performance' ),
     1684                'color' => 'blue',
     1685            ),
     1686            'description' => wp_kses_post( $description ),
     1687            'test'        => 'page_cache',
     1688            'status'      => 'good',
     1689            'label'       => '',
     1690            'actions'     => sprintf(
     1691                '<p><a href="%1$s" target="_blank" rel="noopener noreferrer">%2$s <span class="screen-reader-text">%3$s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
     1692                __( 'https://wordpress.org/support/article/optimization/#Caching' ),
     1693                __( 'Learn more about page cache' ),
     1694                /* translators: Accessibility text. */
     1695                __( '(opens in a new tab)' )
     1696            ),
     1697        );
     1698
     1699        $page_cache_detail = $this->get_page_cache_detail();
     1700
     1701        if ( is_wp_error( $page_cache_detail ) ) {
     1702            $result['label']  = __( 'Unable to detect the presence of page cache' );
     1703            $result['status'] = 'recommended';
     1704            $error_info       = sprintf(
     1705            /* translators: 1 is error message, 2 is error code */
     1706                __( 'Unable to detect page cache due to possible loopback request problem. Please verify that the loopback request test is passing. Error: %1$s (Code: %2$s)' ),
     1707                $page_cache_detail->get_error_message(),
     1708                $page_cache_detail->get_error_code()
     1709            );
     1710            $result['description'] = wp_kses_post( "<p>$error_info</p>" ) . $result['description'];
     1711            return $result;
     1712        }
     1713
     1714        $result['status'] = $page_cache_detail['status'];
     1715
     1716        switch ( $page_cache_detail['status'] ) {
     1717            case 'recommended':
     1718                $result['label'] = __( 'Page cache is not detected but the server response time is OK' );
     1719                break;
     1720            case 'good':
     1721                $result['label'] = __( 'Page cache is detected and the server response time is good' );
     1722                break;
     1723            default:
     1724                if ( empty( $page_cache_detail['headers'] ) && ! $page_cache_detail['advanced_cache_present'] ) {
     1725                    $result['label'] = __( 'Page cache is not detected and the server response time is slow' );
     1726                } else {
     1727                    $result['label'] = __( 'Page cache is detected but the server response time is still slow' );
     1728                }
     1729        }
     1730
     1731        $page_cache_test_summary = array();
     1732
     1733        if ( empty( $page_cache_detail['response_time'] ) ) {
     1734            $page_cache_test_summary[] = '<span class="dashicons dashicons-dismiss"></span> ' . __( 'Server response time could not be determined. Verify that loopback requests are working.' );
     1735        } else {
     1736
     1737            $threshold = $this->get_good_response_time_threshold();
     1738            if ( $page_cache_detail['response_time'] < $threshold ) {
     1739                $page_cache_test_summary[] = '<span class="dashicons dashicons-yes-alt"></span> ' . sprintf(
     1740                    /* translators: 1: The response time in milliseconds. 2: The recommended threshold milliseconds. */
     1741                    __( 'Median server response time was %1$s milliseconds. This is less than the recommended %2$s milliseconds threshold.' ),
     1742                    number_format_i18n( $page_cache_detail['response_time'] ),
     1743                    number_format_i18n( $threshold )
     1744                );
     1745            } else {
     1746                $page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . sprintf(
     1747                    /* translators: 1: The response time in milliseconds. 2: The recommended threshold milliseconds. */
     1748                    __( 'Median server response time was %1$s milliseconds. It should be less than the recommended %2$s milliseconds threshold.' ),
     1749                    number_format_i18n( $page_cache_detail['response_time'] ),
     1750                    number_format_i18n( $threshold )
     1751                );
     1752            }
     1753
     1754            if ( empty( $page_cache_detail['headers'] ) ) {
     1755                $page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . __( 'No client caching response headers were detected.' );
     1756            } else {
     1757                $headers_summary  = '<span class="dashicons dashicons-yes-alt"></span>';
     1758                $headers_summary .= sprintf(
     1759                /* translators: Placeholder is number of caching headers */
     1760                    _n(
     1761                        ' There was %d client caching response header detected: ',
     1762                        ' There were %d client caching response headers detected: ',
     1763                        count( $page_cache_detail['headers'] )
     1764                    ),
     1765                    count( $page_cache_detail['headers'] )
     1766                );
     1767                $headers_summary          .= '<code>' . implode( '</code>, <code>', $page_cache_detail['headers'] ) . '</code>.';
     1768                $page_cache_test_summary[] = $headers_summary;
     1769            }
     1770        }
     1771
     1772        if ( $page_cache_detail['advanced_cache_present'] ) {
     1773            $page_cache_test_summary[] = '<span class="dashicons dashicons-yes-alt"></span> ' . __( 'A page cache plugin was detected.' );
     1774        } elseif ( ! ( is_array( $page_cache_detail ) && ! empty( $page_cache_detail['headers'] ) ) ) {
     1775            // Note: This message is not shown if client caching response headers were present since an external caching layer may be employed.
     1776            $page_cache_test_summary[] = '<span class="dashicons dashicons-warning"></span> ' . __( 'A page cache plugin was not detected.' );
     1777        }
     1778
     1779        $result['description'] .= '<ul><li>' . implode( '</li><li>', $page_cache_test_summary ) . '</li></ul>';
     1780        return $result;
     1781    }
     1782
     1783    /**
    16701784     * Check if the HTTP API can handle SSL/TLS requests.
    16711785     *
    16721786     * @since 5.2.0
    16731787     *
    1674      * @return array The test results.
     1788     * @return array The test result.
    16751789     */
    16761790    public function get_test_ssl_support() {
     
    24832597        }
    24842598
    2485         // Only check for a persistent object cache in production environments to not unnecessarily promote complicated setups.
     2599        // Only check for caches in production environments.
    24862600        if ( 'production' === wp_get_environment_type() ) {
     2601            $tests['async']['page_cache'] = array(
     2602                'label'             => __( 'Page cache' ),
     2603                'test'              => rest_url( 'wp-site-health/v1/tests/page-cache' ),
     2604                'has_rest'          => true,
     2605                'async_direct_test' => array( WP_Site_Health::get_instance(), 'get_test_page_cache' ),
     2606            );
     2607
    24872608            $tests['direct']['persistent_object_cache'] = array(
    24882609                'label' => __( 'Persistent object cache' ),
     
    29673088
    29683089    /**
     3090     * Returns a list of headers and its verification callback to verify if page cache is enabled or not.
     3091     *
     3092     * Note: key is header name and value could be callable function to verify header value.
     3093     * Empty value mean existence of header detect page cache is enabled.
     3094     *
     3095     * @since 6.1.0
     3096     *
     3097     * @return array List of client caching headers and their (optional) verification callbacks.
     3098     */
     3099    public function get_page_cache_headers() {
     3100
     3101        $cache_hit_callback = static function ( $header_value ) {
     3102            return false !== strpos( strtolower( $header_value ), 'hit' );
     3103        };
     3104
     3105        $cache_headers = array(
     3106            'cache-control'          => static function ( $header_value ) {
     3107                return (bool) preg_match( '/max-age=[1-9]/', $header_value );
     3108            },
     3109            'expires'                => static function ( $header_value ) {
     3110                return strtotime( $header_value ) > time();
     3111            },
     3112            'age'                    => static function ( $header_value ) {
     3113                return is_numeric( $header_value ) && $header_value > 0;
     3114            },
     3115            'last-modified'          => '',
     3116            'etag'                   => '',
     3117            'x-cache-enabled'        => static function ( $header_value ) {
     3118                return 'true' === strtolower( $header_value );
     3119            },
     3120            'x-cache-disabled'       => static function ( $header_value ) {
     3121                return ( 'on' !== strtolower( $header_value ) );
     3122            },
     3123            'x-srcache-store-status' => $cache_hit_callback,
     3124            'x-srcache-fetch-status' => $cache_hit_callback,
     3125        );
     3126
     3127        /**
     3128         * Filters the list of cache headers supported by core.
     3129         *
     3130         * @since 6.1.0
     3131         *
     3132         * @param int $cache_headers Array of supported cache headers.
     3133         */
     3134        return apply_filters( 'site_status_page_cache_supported_cache_headers', $cache_headers );
     3135    }
     3136
     3137    /**
     3138     * Checks if site has page cache enabled or not.
     3139     *
     3140     * @since 6.1.0
     3141     *
     3142     * @return WP_Error|array {
     3143     *     Page cache detection details or else error information.
     3144     *
     3145     *     @type bool    $advanced_cache_present        Whether a page cache plugin is present.
     3146     *     @type array[] $page_caching_response_headers Sets of client caching headers for the responses.
     3147     *     @type float[] $response_timing               Response timings.
     3148     * }
     3149     */
     3150    private function check_for_page_caching() {
     3151
     3152        /** This filter is documented in wp-includes/class-wp-http-streams.php */
     3153        $sslverify = apply_filters( 'https_local_ssl_verify', false );
     3154
     3155        $headers = array();
     3156
     3157        // Include basic auth in loopback requests. Note that this will only pass along basic auth when user is
     3158        // initiating the test. If a site requires basic auth, the test will fail when it runs in WP Cron as part of
     3159        // wp_site_health_scheduled_check. This logic is copied from WP_Site_Health::can_perform_loopback().
     3160        if ( isset( $_SERVER['PHP_AUTH_USER'] ) && isset( $_SERVER['PHP_AUTH_PW'] ) ) {
     3161            $headers['Authorization'] = 'Basic ' . base64_encode( wp_unslash( $_SERVER['PHP_AUTH_USER'] ) . ':' . wp_unslash( $_SERVER['PHP_AUTH_PW'] ) );
     3162        }
     3163
     3164        $caching_headers               = $this->get_page_cache_headers();
     3165        $page_caching_response_headers = array();
     3166        $response_timing               = array();
     3167
     3168        for ( $i = 1; $i <= 3; $i++ ) {
     3169            $start_time    = microtime( true );
     3170            $http_response = wp_remote_get( home_url( '/' ), compact( 'sslverify', 'headers' ) );
     3171            $end_time      = microtime( true );
     3172
     3173            if ( is_wp_error( $http_response ) ) {
     3174                return $http_response;
     3175            }
     3176            if ( wp_remote_retrieve_response_code( $http_response ) !== 200 ) {
     3177                return new WP_Error(
     3178                    'http_' . wp_remote_retrieve_response_code( $http_response ),
     3179                    wp_remote_retrieve_response_message( $http_response )
     3180                );
     3181            }
     3182
     3183            $response_headers = array();
     3184
     3185            foreach ( $caching_headers as $header => $callback ) {
     3186                $header_values = wp_remote_retrieve_header( $http_response, $header );
     3187                if ( empty( $header_values ) ) {
     3188                    continue;
     3189                }
     3190                $header_values = (array) $header_values;
     3191                if ( empty( $callback ) || ( is_callable( $callback ) && count( array_filter( $header_values, $callback ) ) > 0 ) ) {
     3192                    $response_headers[ $header ] = $header_values;
     3193                }
     3194            }
     3195
     3196            $page_caching_response_headers[] = $response_headers;
     3197            $response_timing[]               = ( $end_time - $start_time ) * 1000;
     3198        }
     3199
     3200        return array(
     3201            'advanced_cache_present'        => (
     3202                file_exists( WP_CONTENT_DIR . '/advanced-cache.php' )
     3203                &&
     3204                ( defined( 'WP_CACHE' ) && WP_CACHE )
     3205                &&
     3206                /** This filter is documented in wp-settings.php */
     3207                apply_filters( 'enable_loading_advanced_cache_dropin', true )
     3208            ),
     3209            'page_caching_response_headers' => $page_caching_response_headers,
     3210            'response_timing'               => $response_timing,
     3211        );
     3212    }
     3213
     3214    /**
     3215     * Get page cache details.
     3216     *
     3217     * @since 6.1.0
     3218     *
     3219     * @return WP_Error|array {
     3220     *    Page cache detail or else a WP_Error if unable to determine.
     3221     *
     3222     *    @type string   $status                 Page cache status. Good, Recommended or Critical.
     3223     *    @type bool     $advanced_cache_present Whether page cache plugin is available or not.
     3224     *    @type string[] $headers                Client caching response headers detected.
     3225     *    @type float    $response_time          Response time of site.
     3226     * }
     3227     */
     3228    private function get_page_cache_detail() {
     3229        $page_cache_detail = $this->check_for_page_caching();
     3230        if ( is_wp_error( $page_cache_detail ) ) {
     3231            return $page_cache_detail;
     3232        }
     3233
     3234        // Use the median server response time.
     3235        $response_timings = $page_cache_detail['response_timing'];
     3236        rsort( $response_timings );
     3237        $page_speed = $response_timings[ floor( count( $response_timings ) / 2 ) ];
     3238
     3239        // Obtain unique set of all client caching response headers.
     3240        $headers = array();
     3241        foreach ( $page_cache_detail['page_caching_response_headers'] as $page_caching_response_headers ) {
     3242            $headers = array_merge( $headers, array_keys( $page_caching_response_headers ) );
     3243        }
     3244        $headers = array_unique( $headers );
     3245
     3246        // Page cache is detected if there are response headers or a page cache plugin is present.
     3247        $has_page_caching = ( count( $headers ) > 0 || $page_cache_detail['advanced_cache_present'] );
     3248
     3249        if ( $page_speed && $page_speed < $this->get_good_response_time_threshold() ) {
     3250            $result = $has_page_caching ? 'good' : 'recommended';
     3251        } else {
     3252            $result = 'critical';
     3253        }
     3254
     3255        return array(
     3256            'status'                 => $result,
     3257            'advanced_cache_present' => $page_cache_detail['advanced_cache_present'],
     3258            'headers'                => $headers,
     3259            'response_time'          => $page_speed,
     3260        );
     3261    }
     3262
     3263    /**
     3264     * Get the threshold below which a response time is considered good.
     3265     *
     3266     * @since 6.1.0
     3267     *
     3268     * @return int Threshold in milliseconds.
     3269     */
     3270    private function get_good_response_time_threshold() {
     3271        /**
     3272         * Filters the threshold below which a response time is considered good.
     3273         *
     3274         * The default is based on https://web.dev/time-to-first-byte/.
     3275         *
     3276         * @param int $threshold Threshold in milliseconds. Default 600.
     3277         *
     3278         * @since 6.1.0
     3279         */
     3280        return (int) apply_filters( 'site_status_good_response_time_threshold', 600 );
     3281    }
     3282
     3283    /**
    29693284     * Determines whether to suggest using a persistent object cache.
    29703285     *
Note: See TracChangeset for help on using the changeset viewer.