. 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.*?#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/' ) ); }