Files
wp-healthcheck/includes/steps/70-performance.php
Steve Hanlon 6dd050ea1d Rename to ATT Site Healthcheck (private prefix)
Wholesale rename to avoid clashes with generic 'site healthcheck'
plugin names on a target site:

  - Plugin Name:        'Site Healthcheck' → 'ATT Site Healthcheck'
  - Main file:          site-healthcheck.php → att-site-healthcheck.php
  - Plugin folder:      site-healthcheck → att-site-healthcheck
  - Admin menu slug:    site-healthcheck → att-site-healthcheck
  - Settings slug:      site-healthcheck-settings → att-site-healthcheck-settings
  - PHP class prefix:   WPH_ → ATT_HC_
  - Function prefix:    wph_ → att_hc_
  - Option / transient: wph_* → att_hc_*
  - Action/filter:      wph_* → att_hc_*
  - CSS class prefix:   wph- → att-hc-
  - Constants:          WPH_GITEA_* → ATT_HC_GITEA_*
  - Class file names:   class-wph-*.php → class-att-hc-*.php
  - Dev folder:         ~/dev/wp-healthcheck → ~/dev/att-site-healthcheck

Existing in-progress sessions on installs that had the old wph_session
option will not migrate — they were intended for dev use only and the
user has confirmed this is OK for the rename window.

Smoke-tested on testsite: classes load, 14 steps discovered, save/load
round-trip works, admin page renders with new att-hc- CSS classes.

Recovery plugin detection unchanged — that lives in wp-site-recovery
and continues to be detected by Name + Author header.
2026-06-12 11:11:42 +01:00

132 lines
6.1 KiB
PHP

<?php
if (!defined('ABSPATH')) exit;
return new class extends ATT_HC_Step {
public function id(): string { return 'performance'; }
public function title(): string { return 'Step 7 — Performance Check'; }
public function sub_items(): array {
return [
'Run a quick PageSpeed Insights check on the homepage',
'Note the scores (mobile and desktop) in the client record',
'Flag if mobile score has dropped significantly since last check (more than 10 points)',
'Check that caching is active — if using a caching plugin, confirm it is enabled and not throwing errors',
'Check image sizes on the homepage — flag if uncompressed images over 500KB are being served',
];
}
public function autocheck(array $session_state): array {
$f = [];
$home = home_url('/');
// PageSpeed Insights — no API key needed for low volume (25k/day per IP).
// Skip on localhost: PSI can't reach a local URL.
$host = parse_url($home, PHP_URL_HOST) ?: '';
$is_local = preg_match('/(\.local|localhost|^127\.|^192\.168\.)/', $host);
if ($is_local) {
$f[] = $this->finding('psi_skipped', 'info', 'PageSpeed Insights', 'skipped (local host)', 'Run manually from a public URL: https://pagespeed.web.dev/');
} else {
foreach (['mobile', 'desktop'] as $strategy) {
$psi = $this->psi($home, $strategy);
if ($psi === null) {
$f[] = $this->finding('psi_' . $strategy, 'warn', 'PageSpeed (' . $strategy . ')', 'API error', 'Could not reach googleapis.com — retry or run manually.');
continue;
}
$score = (int) round((float) $psi['performance'] * 100);
$level = $score >= 90 ? 'ok' : ($score >= 50 ? 'warn' : 'bad');
$detail_bits = [];
if (isset($psi['lcp'])) $detail_bits[] = 'LCP ' . $psi['lcp'];
if (isset($psi['cls'])) $detail_bits[] = 'CLS ' . $psi['cls'];
if (isset($psi['tbt'])) $detail_bits[] = 'TBT ' . $psi['tbt'];
$f[] = $this->finding(
'psi_' . $strategy,
$level,
'PageSpeed (' . $strategy . ')',
$score . '/100',
implode(' · ', $detail_bits)
);
}
}
// Caching plugin detection
$caching = [
'wp-rocket/wp-rocket.php' => 'WP Rocket',
'wp-super-cache/wp-cache.php' => 'WP Super Cache',
'w3-total-cache/w3-total-cache.php' => 'W3 Total Cache',
'litespeed-cache/litespeed-cache.php' => 'LiteSpeed Cache',
'sg-cachepress/sg-cachepress.php' => 'SG Optimizer',
'wp-fastest-cache/wpFastestCache.php'=> 'WP Fastest Cache',
'cache-enabler/cache-enabler.php' => 'Cache Enabler',
];
$found_caching = null;
foreach ($caching as $file => $label) {
if (is_plugin_active($file)) { $found_caching = $label; break; }
}
$f[] = $this->finding(
'caching',
$found_caching ? 'ok' : 'info',
'Caching plugin',
$found_caching ?: 'none detected',
$found_caching ? '' : 'No common caching plugin active — host-level caching may still apply.'
);
// Heavy images on the homepage
$resp = wp_remote_get($home, ['timeout' => 6]);
if (!is_wp_error($resp)) {
$body = (string) wp_remote_retrieve_body($resp);
preg_match_all('/<img\b[^>]*src=["\']([^"\']+)["\']/i', $body, $m);
$urls = isset($m[1]) ? array_slice(array_unique($m[1]), 0, 8) : [];
$heavy = [];
foreach ($urls as $u) {
if (substr($u, 0, 2) === '//') $u = (parse_url($home, PHP_URL_SCHEME) ?: 'https') . ':' . $u;
if (substr($u, 0, 1) === '/') $u = rtrim($home, '/') . $u;
$head = wp_remote_head($u, ['timeout' => 3, 'redirection' => 2]);
if (is_wp_error($head)) continue;
$size = (int) wp_remote_retrieve_header($head, 'content-length');
if ($size > 500 * 1024) {
$heavy[] = basename(parse_url($u, PHP_URL_PATH) ?: $u) . ' (' . size_format($size) . ')';
}
}
$f[] = $this->finding(
'heavy_images',
$heavy ? 'warn' : 'ok',
'Large homepage images',
count($heavy) . ' over 500KB',
$heavy ? implode(', ', $heavy) : ''
);
}
return $f;
}
private function psi(string $url, string $strategy): ?array {
$cache_key = 'att_hc_psi_' . md5($url . '|' . $strategy);
$cached = get_transient($cache_key);
if ($cached !== false) return $cached;
$api = add_query_arg([
'url' => $url,
'strategy' => $strategy,
'category' => 'performance',
], 'https://www.googleapis.com/pagespeedonline/v5/runPagespeed');
$resp = wp_remote_get($api, ['timeout' => 30]);
if (is_wp_error($resp) || wp_remote_retrieve_response_code($resp) !== 200) {
set_transient($cache_key, null, HOUR_IN_SECONDS);
return null;
}
$data = json_decode((string) wp_remote_retrieve_body($resp), true);
if (!is_array($data) || empty($data['lighthouseResult'])) {
set_transient($cache_key, null, HOUR_IN_SECONDS);
return null;
}
$lr = $data['lighthouseResult'];
$out = [
'performance' => $lr['categories']['performance']['score'] ?? null,
'lcp' => $lr['audits']['largest-contentful-paint']['displayValue'] ?? null,
'cls' => $lr['audits']['cumulative-layout-shift']['displayValue'] ?? null,
'tbt' => $lr['audits']['total-blocking-time']['displayValue'] ?? null,
];
set_transient($cache_key, $out, 12 * HOUR_IN_SECONDS);
return $out;
}
};