Make WordPress Core


Ignore:
Timestamp:
08/31/2022 10:44:04 PM (4 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.