488 lines
15 KiB
PHP
488 lines
15 KiB
PHP
<?php
|
|
/**
|
|
* File: PageSpeed_Data.php
|
|
*
|
|
* Processes PageSpeed API data return into usable format.
|
|
*
|
|
* @since 2.3.0 Update to utilize OAuth2.0 and overhaul of feature.
|
|
*
|
|
* @package W3TC
|
|
*/
|
|
|
|
namespace W3TC;
|
|
|
|
/**
|
|
* PageSpeed Data Config.
|
|
*
|
|
* @since 2.3.0
|
|
*/
|
|
class PageSpeed_Data {
|
|
|
|
/**
|
|
* Prepare PageSpeed Data Config.
|
|
*
|
|
* @since 2.3.0
|
|
*
|
|
* @param array $data PageSpeed analysis data.
|
|
*
|
|
* @return array
|
|
*/
|
|
public static function prepare_pagespeed_data( $data ) {
|
|
$score = Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'categories', 'performance', 'score' ) );
|
|
|
|
$pagespeed_data = array(
|
|
'score' => self::normalize_score( $score ),
|
|
'first-contentful-paint' => self::collect_core_metric( $data, 'first-contentful-paint' ),
|
|
'largest-contentful-paint' => self::collect_core_metric( $data, 'largest-contentful-paint' ),
|
|
'interactive' => self::collect_core_metric( $data, 'interactive' ),
|
|
'cumulative-layout-shift' => self::collect_core_metric( $data, 'cumulative-layout-shift' ),
|
|
'total-blocking-time' => self::collect_core_metric( $data, 'total-blocking-time' ),
|
|
'speed-index' => self::collect_core_metric( $data, 'speed-index' ),
|
|
'screenshots' => array(
|
|
'final' => array(
|
|
'title' => Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'audits', 'final-screenshot', 'title' ) ),
|
|
'screenshot' => Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'audits', 'final-screenshot', 'details', 'data' ) ),
|
|
),
|
|
'other' => array(
|
|
'title' => Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'audits', 'screenshot-thumbnails', 'title' ) ),
|
|
'screenshots' => Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'audits', 'screenshot-thumbnails', 'details', 'items' ) ),
|
|
),
|
|
),
|
|
'insights' => self::collect_audits_by_group( $data, 'insights' ),
|
|
'diagnostics' => self::collect_audits_by_group( $data, 'diagnostics' ),
|
|
);
|
|
|
|
$pagespeed_data['insights'] = self::filter_metrics_by_title( $pagespeed_data['insights'] );
|
|
$pagespeed_data['diagnostics'] = self::filter_metrics_by_title( $pagespeed_data['diagnostics'] );
|
|
|
|
if ( defined( 'W3TC_GPS_KEYS_DEBUG' ) ) {
|
|
self::debug_metric_keys( $pagespeed_data );
|
|
}
|
|
|
|
return self::merge_instructions( $pagespeed_data );
|
|
}
|
|
|
|
/**
|
|
* Collect core web vital metrics in a consistent format.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param array $data PageSpeed data payload.
|
|
* @param string $metric Lighthouse audit identifier.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function collect_core_metric( $data, $metric ) {
|
|
return array(
|
|
'score' => Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'audits', $metric, 'score' ) ),
|
|
'scoreDisplayMode' => Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'audits', $metric, 'scoreDisplayMode' ) ),
|
|
'displayValue' => Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'audits', $metric, 'displayValue' ) ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Log the raw metric keys and configured instruction keys when debugging is enabled.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param array $pagespeed_data Prepared PageSpeed data.
|
|
*
|
|
* @return void
|
|
*/
|
|
private static function debug_metric_keys( $pagespeed_data ) {
|
|
$gps_insight_ids = array_keys( $pagespeed_data['insights'] ?? array() );
|
|
$gps_diagnostic_ids = array_keys( $pagespeed_data['diagnostics'] ?? array() );
|
|
|
|
$instruction_config = PageSpeed_Instructions::get_pagespeed_instructions();
|
|
|
|
$w3tc_insight_ids = ! empty( $instruction_config['insights'] ) ? array_keys( $instruction_config['insights'] ) : array();
|
|
$w3tc_diagnostic_ids = ! empty( $instruction_config['diagnostics'] ) ? array_keys( $instruction_config['diagnostics'] ) : array();
|
|
|
|
\sort( $gps_insight_ids );
|
|
\sort( $gps_diagnostic_ids );
|
|
\sort( $w3tc_insight_ids );
|
|
\sort( $w3tc_diagnostic_ids );
|
|
|
|
Util_Debug::debug(
|
|
'pagespeed_metric_keys',
|
|
array(
|
|
'gps' => array(
|
|
'insights' => $gps_insight_ids,
|
|
'diagnostics' => $gps_diagnostic_ids,
|
|
),
|
|
'w3tc' => array(
|
|
'insights' => $w3tc_insight_ids,
|
|
'diagnostics' => $w3tc_diagnostic_ids,
|
|
),
|
|
)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Collect audits belonging to the given Lighthouse category group.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param array $data Raw Lighthouse API payload.
|
|
* @param string $group Lighthouse category group identifier.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function collect_audits_by_group( $data, $group ) {
|
|
$audit_refs = Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'categories', 'performance', 'auditRefs' ) );
|
|
$audits = Util_PageSpeed::get_value_recursive( $data, array( 'lighthouseResult', 'audits' ) );
|
|
if ( empty( $audit_refs ) || ! \is_array( $audit_refs ) || empty( $audits ) || ! \is_array( $audits ) ) {
|
|
return array();
|
|
}
|
|
|
|
$metrics = array();
|
|
|
|
foreach ( $audit_refs as $audit_ref ) {
|
|
if ( empty( $audit_ref['id'] ) || empty( $audit_ref['group'] ) || $group !== $audit_ref['group'] ) {
|
|
continue;
|
|
}
|
|
|
|
$audit_id = $audit_ref['id'];
|
|
|
|
if ( empty( $audits[ $audit_id ] ) || ! \is_array( $audits[ $audit_id ] ) ) {
|
|
continue;
|
|
}
|
|
|
|
$metrics[ $audit_id ] = self::format_audit_metric( $audit_id, $audits[ $audit_id ] );
|
|
}
|
|
|
|
return $metrics;
|
|
}
|
|
|
|
/**
|
|
* Format a single Lighthouse audit into the structure expected by the UI.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param string $audit_id Lighthouse audit identifier.
|
|
* @param array $audit Lighthouse audit payload.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function format_audit_metric( $audit_id, $audit ) {
|
|
$metric = array(
|
|
'id' => $audit_id,
|
|
'title' => $audit['title'] ?? null,
|
|
'description' => $audit['description'] ?? null,
|
|
'score' => $audit['score'] ?? null,
|
|
'scoreDisplayMode' => $audit['scoreDisplayMode'] ?? null,
|
|
'displayValue' => $audit['displayValue'] ?? null,
|
|
'details' => self::extract_audit_details( $audit['details'] ?? array() ),
|
|
);
|
|
|
|
if ( 'network-dependency-tree-insight' === $audit_id ) {
|
|
$metric['networkDependency'] = self::format_network_dependency_details( $audit['details'] ?? array() );
|
|
$metric['details'] = array();
|
|
}
|
|
|
|
$types = self::resolve_metric_types( $audit_id, $audit );
|
|
if ( ! empty( $types ) ) {
|
|
$metric['type'] = $types;
|
|
}
|
|
|
|
return $metric;
|
|
}
|
|
|
|
/**
|
|
* Normalize Lighthouse audit details to a list structure.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param mixed $details Lighthouse audit details.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function extract_audit_details( $details ) {
|
|
if ( empty( $details ) || ! \is_array( $details ) ) {
|
|
return array();
|
|
}
|
|
|
|
if ( isset( $details['items'] ) && \is_array( $details['items'] ) ) {
|
|
return $details['items'];
|
|
}
|
|
|
|
$alternative_keys = array( 'chains', 'nodes', 'entries', 'timings' );
|
|
foreach ( $alternative_keys as $key ) {
|
|
if ( isset( $details[ $key ] ) && \is_array( $details[ $key ] ) ) {
|
|
return $details[ $key ];
|
|
}
|
|
}
|
|
|
|
return array( $details );
|
|
}
|
|
|
|
/**
|
|
* Determine which Core Web Vitals an audit influences.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param string $audit_id Lighthouse audit identifier.
|
|
* @param array $audit Lighthouse audit payload.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function resolve_metric_types( $audit_id, $audit ) {
|
|
$type_map = self::get_metric_type_map();
|
|
$types = array();
|
|
|
|
if ( isset( $type_map[ $audit_id ] ) ) {
|
|
$types = $type_map[ $audit_id ];
|
|
}
|
|
|
|
if ( empty( $types ) && isset( $audit['metricSavings'] ) && \is_array( $audit['metricSavings'] ) ) {
|
|
foreach ( $audit['metricSavings'] as $metric => $value ) {
|
|
if ( \in_array( $metric, array( 'FCP', 'LCP', 'TBT', 'CLS' ), true ) ) {
|
|
$types[] = $metric;
|
|
}
|
|
}
|
|
}
|
|
|
|
return \array_values( \array_unique( $types ) );
|
|
}
|
|
|
|
/**
|
|
* Normalize the network dependency tree insight payload.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param array $details Lighthouse network dependency tree details payload.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function format_network_dependency_details( $details ) {
|
|
if ( empty( $details ) || ! \is_array( $details ) ) {
|
|
return array();
|
|
}
|
|
|
|
$items = $details['items'] ?? array();
|
|
$tree_section = $items[0]['value'] ?? array();
|
|
$preconnected_section = $items[1] ?? array();
|
|
$candidates_section = $items[2] ?? array();
|
|
$chains = $tree_section['chains'] ?? array();
|
|
$normalized_chains = array();
|
|
|
|
if ( ! empty( $chains ) && \is_array( $chains ) ) {
|
|
foreach ( $chains as $chain ) {
|
|
$normalized_chains[] = self::normalize_network_chain_node( $chain );
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'longestChainDuration' => $tree_section['longestChain']['duration'] ?? null,
|
|
'chains' => $normalized_chains,
|
|
'preconnected' => self::format_preconnect_section( $preconnected_section ),
|
|
'candidates' => self::format_preconnect_section( $candidates_section ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Normalize a network dependency chain node recursively.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param array $node Node payload.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function normalize_network_chain_node( $node ) {
|
|
$children = array();
|
|
|
|
if ( ! empty( $node['children'] ) && \is_array( $node['children'] ) ) {
|
|
foreach ( $node['children'] as $child ) {
|
|
$children[] = self::normalize_network_chain_node( $child );
|
|
}
|
|
}
|
|
|
|
return array(
|
|
'url' => $node['url'] ?? '',
|
|
'duration' => $node['navStartToEndTime'] ?? null,
|
|
'transferSize' => $node['transferSize'] ?? null,
|
|
'isLongest' => (bool) ( $node['isLongest'] ?? false ),
|
|
'children' => $children,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Normalize preconnect insight sections.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param array $section Section payload from Lighthouse.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function format_preconnect_section( $section ) {
|
|
if ( empty( $section ) || ! \is_array( $section ) ) {
|
|
return array();
|
|
}
|
|
|
|
$value = $section['value'] ?? array();
|
|
$data = array(
|
|
'title' => $section['title'] ?? '',
|
|
'description' => $section['description'] ?? '',
|
|
'entries' => array(),
|
|
);
|
|
|
|
if ( isset( $value['value'] ) && ! empty( $value['value'] ) && \is_string( $value['value'] ) ) {
|
|
$data['entries'] = $value['value'];
|
|
return $data;
|
|
}
|
|
|
|
if ( isset( $value['items'] ) && \is_array( $value['items'] ) ) {
|
|
$entries = array();
|
|
foreach ( $value['items'] as $item ) {
|
|
if ( \is_string( $item ) ) {
|
|
$entries[] = $item;
|
|
} elseif ( isset( $item['origin'] ) ) {
|
|
$entries[] = $item['origin'];
|
|
} elseif ( isset( $item['value'] ) && \is_string( $item['value'] ) ) {
|
|
$entries[] = $item['value'];
|
|
}
|
|
}
|
|
|
|
if ( ! empty( $entries ) ) {
|
|
$data['entries'] = $entries;
|
|
} elseif ( ! empty( $value ) ) {
|
|
$data['entries'] = \wp_json_encode( $value );
|
|
}
|
|
} elseif ( ! empty( $value ) && \is_string( $value ) ) {
|
|
$data['entries'] = $value;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
/**
|
|
* Provide a mapping of audit identifiers to Core Web Vital type tags.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function get_metric_type_map() {
|
|
return array(
|
|
'render-blocking-insight' => array( 'FCP', 'LCP' ),
|
|
'render-blocking-resources' => array( 'FCP', 'LCP' ),
|
|
'unused-css-rules' => array( 'FCP', 'LCP' ),
|
|
'unminified-css' => array( 'FCP', 'LCP' ),
|
|
'unminified-javascript' => array( 'FCP', 'LCP' ),
|
|
'unused-javascript' => array( 'LCP' ),
|
|
'uses-text-compression' => array( 'FCP', 'LCP' ),
|
|
'uses-rel-preconnect' => array( 'FCP', 'LCP' ),
|
|
'server-response-time' => array( 'FCP', 'LCP' ),
|
|
'redirects' => array( 'FCP', 'LCP' ),
|
|
'efficient-animated-content' => array( 'LCP' ),
|
|
'duplicated-javascript' => array( 'TBT' ),
|
|
'duplicated-javascript-insight' => array( 'TBT' ),
|
|
'legacy-javascript' => array( 'TBT' ),
|
|
'legacy-javascript-insight' => array( 'TBT' ),
|
|
'total-byte-weight' => array( 'LCP' ),
|
|
'dom-size' => array( 'TBT' ),
|
|
'dom-size-insight' => array( 'TBT' ),
|
|
'bootup-time' => array( 'TBT' ),
|
|
'mainthread-work-breakdown' => array( 'TBT' ),
|
|
'third-party-summary' => array( 'TBT' ),
|
|
'third-parties-insight' => array( 'TBT' ),
|
|
'third-party-facades' => array( 'TBT' ),
|
|
'non-composited-animations' => array( 'CLS' ),
|
|
'unsized-images' => array( 'CLS' ),
|
|
'cls-culprits-insight' => array( 'CLS' ),
|
|
'font-display' => array( 'FCP', 'LCP' ),
|
|
'font-display-insight' => array( 'FCP', 'LCP' ),
|
|
'cache-insight' => array( 'FCP', 'LCP' ),
|
|
'document-latency-insight' => array( 'FCP', 'LCP' ),
|
|
'network-dependency-tree-insight' => array( 'FCP', 'LCP' ),
|
|
'viewport' => array( 'TBT' ),
|
|
'viewport-insight' => array( 'TBT' ),
|
|
'lcp-breakdown-insight' => array( 'LCP' ),
|
|
'lcp-discovery-insight' => array( 'LCP' ),
|
|
'image-delivery-insight' => array( 'LCP' ),
|
|
'forced-reflow-insight' => array( 'TBT' ),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Normalize score values to 0-100 scale while avoiding PHP warnings when score is missing.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param mixed $score Score from the Lighthouse payload.
|
|
*
|
|
* @return int
|
|
*/
|
|
private static function normalize_score( $score ) {
|
|
if ( ! isset( $score ) || ! \is_numeric( $score ) ) {
|
|
return 0;
|
|
}
|
|
|
|
return $score * 100;
|
|
}
|
|
|
|
/**
|
|
* Drop metrics that Google didn't include in the latest payload.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param array $metrics Raw metrics bucket.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function filter_metrics_by_title( $metrics ) {
|
|
if ( empty( $metrics ) || ! \is_array( $metrics ) ) {
|
|
return array();
|
|
}
|
|
|
|
return \array_filter(
|
|
$metrics,
|
|
static function ( $metric ) {
|
|
return \is_array( $metric ) && isset( $metric['title'] ) && '' !== $metric['title'];
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Attach instructions for metrics that survived the filtering step.
|
|
*
|
|
* @since 2.9.1
|
|
*
|
|
* @param array $pagespeed_data Prepared PageSpeed data.
|
|
*
|
|
* @return array
|
|
*/
|
|
private static function merge_instructions( $pagespeed_data ) {
|
|
$instructions = PageSpeed_Instructions::get_pagespeed_instructions();
|
|
$default_instruction = '<p>' . \esc_html__( 'W3 Total Cache does not yet have guidance for this audit.', 'w3-total-cache' ) . '</p>';
|
|
|
|
foreach ( array( 'insights', 'diagnostics' ) as $bucket ) {
|
|
if ( empty( $pagespeed_data[ $bucket ] ) ) {
|
|
continue;
|
|
}
|
|
|
|
if ( empty( $instructions[ $bucket ] ) ) {
|
|
foreach ( $pagespeed_data[ $bucket ] as $key => $metric ) {
|
|
$pagespeed_data[ $bucket ][ $key ]['instructions'] = $default_instruction;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
foreach ( $pagespeed_data[ $bucket ] as $key => $metric ) {
|
|
if ( isset( $instructions[ $bucket ][ $key ] ) ) {
|
|
$pagespeed_data[ $bucket ][ $key ] = \array_merge(
|
|
$metric,
|
|
$instructions[ $bucket ][ $key ]
|
|
);
|
|
} else {
|
|
$pagespeed_data[ $bucket ][ $key ]['instructions'] = $default_instruction;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $pagespeed_data;
|
|
}
|
|
}
|