. Nothing else in the page changes: the slider
* still decides when to show it, but the file is already there.
*
* A hero drawn from a stylesheet file or built by a script leaves nothing in
* the HTML to find. Its address can be entered per page type on the Page
* Speed page; otherwise the page says so, and that site needs hand work.
*
* Settings: astsp_performance['hero_image'] (bool, default true) and the
* option astsp_performance_hero (page type => image URL).
*
* @package Astute_Speed
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/** Page types the hero can be set for. */
const ASTSP_HERO_TYPES = array( 'front', 'single', 'page', 'archive' );
/** An image narrower than this (by its width attribute) is not the hero. */
const ASTSP_HERO_MIN_WIDTH = 300;
/** Attributes lazy loaders and sliders keep the real address in, most specific first. */
const ASTSP_HERO_LAZY_ATTRIBUTES = array( 'data-lazyload', 'data-lazy-src', 'data-src', 'data-original', 'data-bg-src' );
/**
* Whether a URL is a loader's stand-in rather than a picture.
*
* @param string $url Image URL.
* @return bool
*/
function astsp_hero_is_placeholder( $url ) {
$url = strtolower( trim( (string) $url ) );
return '' === $url || str_starts_with( $url, 'data:' )
|| (bool) preg_match( '#/(dummy|transparent|blank|placeholder|spacer|pixel|lazy[-_]?placeholder|1x1)\.(png|gif|svg|jpe?g|webp)(\?|$)#', $url );
}
/**
* Whether a URL is in the media library. Heroes are uploaded; a theme's or
* plugin's own images (a default thumbnail in a related-posts list, a
* decoration) are not taken, so the wrong file is never preloaded.
*
* @param string $url Image URL.
* @return bool
*/
function astsp_hero_is_upload( $url ) {
return str_contains( strtolower( (string) wp_parse_url( $url, PHP_URL_PATH ) ), '/uploads/' );
}
/**
* One attribute's value from a tag.
*
* @param string $tag The tag's HTML.
* @param string $name Attribute name.
* @return string|null
*/
function astsp_hero_attribute( $tag, $name ) {
if ( preg_match( '/\s' . preg_quote( $name, '/' ) . '\s*=\s*("([^"]*)"|\'([^\']*)\')/i', $tag, $m ) ) {
return html_entity_decode( '' !== ( $m[2] ?? '' ) ? $m[2] : ( $m[3] ?? '' ), ENT_QUOTES );
}
return null;
}
/**
* The first large image of the page body, and what holds its address.
*
* Pure so it can be unit-tested. Reads from the end of the first
* (so the logo isn't taken), or from
when there is none; an image
* already shown in the header (the logo again in a menu or footer) is
* skipped too.
*
* @param string $html The page.
* @return array{url: string, kind: string, srcset?: string, sizes?: string}|null
* kind: img, lazy, slider or background; srcset and sizes when the image has them.
*/
function astsp_hero_find( $html ) {
$body = stripos( $html, '', $body );
$in_hdr = array();
if ( false !== $header && preg_match_all( '#]*>#i', substr( $html, $body, $header - $body ), $logos ) ) {
foreach ( $logos[0] as $logo ) {
$in_hdr[] = (string) astsp_hero_attribute( $logo, 'src' );
}
}
$html = substr( $html, false !== $header ? $header : $body );
// Scripts, styles and templates can hold markup that isn't on the page.
$html = preg_replace( '#<(script|style|template|noscript)\b.*?\1>#is', '', $html );
if ( ! preg_match_all( '#]*>|\sstyle\s*=\s*("[^"]*background(?:-image)?\s*:[^"]*url\([^"]*"|\'[^\']*background(?:-image)?\s*:[^\']*url\([^\']*\')#i', $html, $tags ) ) {
return null;
}
foreach ( $tags[0] as $tag ) {
if ( ! str_starts_with( strtolower( ltrim( $tag ) ), ' trim( $m[1] ),
'kind' => 'background',
);
}
continue;
}
$class = strtolower( (string) astsp_hero_attribute( $tag, 'class' ) );
$alt = strtolower( (string) astsp_hero_attribute( $tag, 'alt' ) );
$width = astsp_hero_attribute( $tag, 'width' );
if ( str_contains( $class, 'logo' ) || str_contains( $alt, 'logo' ) || ( null !== $width && (int) $width > 0 && (int) $width < ASTSP_HERO_MIN_WIDTH ) ) {
continue;
}
$url = null;
$lazy = false;
foreach ( ASTSP_HERO_LAZY_ATTRIBUTES as $attr ) {
$value = astsp_hero_attribute( $tag, $attr );
if ( null !== $value && ! astsp_hero_is_placeholder( $value ) ) {
$url = $value;
$lazy = true;
break;
}
}
if ( null === $url ) {
$src = (string) astsp_hero_attribute( $tag, 'src' );
if ( astsp_hero_is_placeholder( $src ) ) {
continue;
}
$url = $src;
}
if ( ! astsp_hero_is_upload( $url ) || preg_match( '/\.svg(\?|$)/i', $url ) || str_contains( strtolower( $url ), 'logo' ) || in_array( $url, $in_hdr, true ) ) {
continue;
}
$slider = str_contains( $class, 'rs-lazyload' ) || str_contains( $class, 'tp-rs-img' ) || str_contains( $class, 'swiper-lazy' );
$hero = array(
'url' => trim( $url ),
'kind' => $slider ? 'slider' : ( $lazy ? 'lazy' : 'img' ),
);
// The browser picks from srcset; the preload must offer the same choice or it downloads twice.
$srcset = astsp_hero_attribute( $tag, $lazy ? 'data-srcset' : 'srcset' ) ?? astsp_hero_attribute( $tag, 'srcset' );
if ( null !== $srcset && '' !== trim( $srcset ) && ! astsp_hero_is_placeholder( strtok( trim( $srcset ), ' ' ) ) ) {
$hero['srcset'] = trim( $srcset );
// "auto" is worked out by the lazy loader's script, so it is no use to a preload.
foreach ( $lazy ? array( 'data-sizes', 'sizes' ) : array( 'sizes' ) as $attr ) {
$sizes = trim( (string) astsp_hero_attribute( $tag, $attr ) );
if ( '' !== $sizes && 'auto' !== strtolower( $sizes ) ) {
$hero['sizes'] = $sizes;
break;
}
}
}
return $hero;
}
return null;
}
/**
* Whether the page's already preloads an image (the featured-image
* preload, or the theme's own).
*
* @param string $html The page.
* @return bool
*/
function astsp_hero_head_preloads_image( $html ) {
$end = stripos( $html, '' );
$head = false !== $end ? substr( $html, 0, $end ) : $html;
return (bool) preg_match( '#]*\brel\s*=\s*["\']?preload)(?=[^>]*\bas\s*=\s*["\']?image)[^>]*>#i', $head );
}
/**
* The page with a preload for its hero image added to .
*
* Pure so it can be unit-tested. Left as it is when already preloads
* an image or no hero is found. The tag goes straight after
* (or ), ahead of the stylesheets, so the download starts first.
*
* @param string $html The page.
* @param string|null $fixed The hero's address set by hand for this page type, if any.
* @return string
*/
function astsp_hero_rewrite( $html, $fixed = null ) {
$lead = astsp_hero_lead_background_preload( $html );
if ( null !== $lead ) {
return $lead;
}
if ( astsp_hero_head_preloads_image( $html ) ) {
return $html;
}
$hero = '' !== (string) $fixed ? array( 'url' => (string) $fixed ) : astsp_hero_find( $html );
if ( empty( $hero['url'] ) ) {
return $html;
}
$link = '';
if ( preg_match( '#]*>#i', $html, $m, PREG_OFFSET_CAPTURE ) ) {
$at = $m[0][1] + strlen( $m[0][0] );
} elseif ( preg_match( '#]*>#i', $html, $m, PREG_OFFSET_CAPTURE ) ) {
$at = $m[0][1] + strlen( $m[0][0] );
} else {
return $html;
}
return substr( $html, 0, $at ) . "\n" . $link . substr( $html, $at );
}
/**
* The page with Astute's featured-image preload pointed at the file the theme
* actually shows, when the theme shows it as a CSS background; null when
* there is nothing to change.
*
* The featured-image preload is printed before the theme runs, so it offers
* the image's srcset. A theme that paints the featured image as a background
* (the Softlab theme's blog-post_bg_media) always loads the full
* file, so a phone preloaded the 768 px copy and then fetched the 744 KB
* original late: an 11 s LCP. Only the same image is matched (its address is
* the preload's href or one of its srcset entries), so a background further
* down the page never takes the preload's place.
*
* @param string $html The page.
* @return string|null
*/
function astsp_hero_lead_background_preload( $html ) {
$end = stripos( $html, '' );
$head = false !== $end ? substr( $html, 0, $end ) : $html;
if ( ! preg_match( '#]*data-astsp=["\']lead["\'][^>]*>#i', $head, $m, PREG_OFFSET_CAPTURE ) ) {
return null;
}
$hero = astsp_hero_find( $html );
if ( null === $hero || 'background' !== $hero['kind'] ) {
return null;
}
$tag = $m[0][0];
$href = (string) astsp_hero_attribute( $tag, 'href' );
$srcset = (string) astsp_hero_attribute( $tag, 'imagesrcset' );
$same = static function ( $a, $b ) {
return preg_replace( '#^(https?:)?//[^/]+#i', '', html_entity_decode( $a, ENT_QUOTES ) ) === preg_replace( '#^(https?:)?//[^/]+#i', '', $b );
};
$listed = $same( $href, $hero['url'] );
foreach ( '' === $srcset ? array() : explode( ',', html_entity_decode( $srcset, ENT_QUOTES ) ) as $candidate ) {
$listed = $listed || $same( strtok( trim( $candidate ), ' ' ), $hero['url'] );
}
if ( ! $listed || ( $same( $href, $hero['url'] ) && '' === $srcset ) ) {
return null;
}
$link = '';
return substr( $html, 0, $m[0][1] ) . $link . substr( $html, $m[0][1] + strlen( $tag ) );
}
/**
* The page type of the current request.
*
* @return string front|single|page|archive
*/
function astsp_hero_page_type() {
if ( is_front_page() || is_home() ) {
return 'front';
}
if ( is_single() ) {
return 'single';
}
return is_page() ? 'page' : 'archive';
}
/**
* The hero addresses set by hand, per page type.
*
* @return array
*/
function astsp_hero_fixed_urls() {
$saved = (array) get_option( 'astsp_performance_hero', array() );
return array_intersect_key( array_map( 'strval', $saved ), array_flip( ASTSP_HERO_TYPES ) );
}
/**
* Hooks the rewrite into the page output, like critical CSS (see
* astsp_rewrite_page(): WP Rocket's buffer, so the cached copy carries it, or
* WordPress's own).
*
* @return void
*/
function astsp_hero_start() {
if ( ! astsp_performance_setting( 'hero_image' ) || is_admin() || is_feed() || is_preview() || is_customize_preview()
|| wp_doing_ajax() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) || is_404() ) {
return;
}
$fixed = astsp_hero_fixed_urls()[ astsp_hero_page_type() ] ?? '';
$rewrite = function ( $html ) use ( $fixed ) {
return is_string( $html ) && '' !== $html ? astsp_hero_rewrite( $html, $fixed ) : $html;
};
astsp_rewrite_page( $rewrite, PHP_INT_MAX - 1 );
}
add_action( 'template_redirect', 'astsp_hero_start', 98 );
/**
* What Astute can do for one page's first screen, read from the page as
* visitors get it.
*
* Pure so it can be unit-tested.
*
* @param string $html The page as served.
* @return array{status: string, url: ?string, kind: ?string} status:
* featured — an image preload was already in (the featured image, or the theme's);
* preloaded — Astute found the hero and preloads it;
* found — a hero is there but isn't preloaded yet (the setting is off, or the page was cached before);
* none — no hero image in the page's HTML.
*/
function astsp_hero_diagnose( $html ) {
if ( preg_match( '#]*data-astsp=["\']hero["\'][^>]*>#i', $html, $m ) ) {
return array(
'status' => 'preloaded',
'url' => astsp_hero_attribute( $m[0], 'href' ),
'kind' => astsp_hero_find( $html )['kind'] ?? null,
);
}
if ( astsp_hero_head_preloads_image( $html ) ) {
return array(
'status' => 'featured',
'url' => null,
'kind' => null,
);
}
$hero = astsp_hero_find( $html );
return null === $hero
? array(
'status' => 'none',
'url' => null,
'kind' => null,
)
: array(
'status' => 'found',
'url' => $hero['url'],
'kind' => $hero['kind'],
);
}
/**
* A sample address of each page type on this site, for the check.
*
* @return array Page type => URL.
*/
function astsp_hero_sample_urls() {
$urls = array( 'front' => home_url( '/' ) );
$post = get_posts(
array(
'numberposts' => 1,
'post_status' => 'publish',
)
);
$page = get_posts(
array(
'numberposts' => 1,
'post_type' => 'page',
'post_status' => 'publish',
'exclude' => array_filter( array( (int) get_option( 'page_on_front' ), (int) get_option( 'page_for_posts' ) ) ), // phpcs:ignore WordPressVIPMinimum.Performance.WPQueryParams.PostNotIn_exclude -- at most two IDs, one page fetched, admin check only.
'orderby' => 'menu_order',
'order' => 'ASC',
)
);
$terms = get_terms(
array(
'taxonomy' => 'category',
'number' => 1,
'orderby' => 'count',
'order' => 'DESC',
'hide_empty' => true,
)
);
if ( $post ) {
$urls['single'] = get_permalink( $post[0] );
}
if ( $page ) {
$urls['page'] = get_permalink( $page[0] );
}
if ( $terms && ! is_wp_error( $terms ) ) {
$urls['archive'] = get_term_link( $terms[0] );
}
return array_filter( $urls, 'is_string' );
}
/**
* Fetches each sample page as a visitor would and diagnoses its first screen.
*
* @return array Page type => diagnosis plus url_checked (and error when the fetch failed).
*/
function astsp_hero_check_site() {
$out = array();
foreach ( astsp_hero_sample_urls() as $type => $url ) {
$response = wp_remote_get(
add_query_arg( 'astsp_nocache', time(), $url ),
array(
'timeout' => 20,
'user-agent' => 'Mozilla/5.0 (Linux; Android 14) AstuteSpeedCheck',
'cookies' => array(),
// A request to the site itself, as WordPress's own loopback checks make it.
'sslverify' => apply_filters( 'https_local_ssl_verify', false ), // phpcs:ignore WordPress.NamingConventions.PrefixAllGlobals.NonPrefixedHooknameFound -- core's hook.
)
);
if ( is_wp_error( $response ) || 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
$out[ $type ] = array(
'status' => 'error',
'url_checked' => $url,
'error' => is_wp_error( $response ) ? $response->get_error_message() : 'HTTP ' . wp_remote_retrieve_response_code( $response ),
);
continue;
}
$out[ $type ] = astsp_hero_diagnose( (string) wp_remote_retrieve_body( $response ) ) + array( 'url_checked' => $url );
}
update_option(
'astsp_performance_hero_check',
array(
'at' => gmdate( 'Y-m-d H:i:s' ),
'results' => $out,
),
false
);
return $out;
}
/**
* Where a site owner reaches the Astute team for hand work; empty until set.
*
* @return string URL or mailto: link.
*/
function astsp_support_url() {
return (string) apply_filters( 'astsp_support_url', (string) get_option( 'astsp_support_url', 'https://writeandgrow.com/contact/' ) );
}