. The rest stay lazy. * * 2. Ads and analytics libraries (AdSense, Google Tag Manager) are a few * hundred kilobytes of script competing with the page for the network and * the main thread. When enabled, they are held back until the visitor first * scrolls, taps or types — or a few seconds have passed — so the page * renders first. Their inline set-up code still runs at once and queues * its calls (dataLayer, adsbygoogle) for the library to pick up. Off by * default: it trades a little ad and analytics coverage for speed, which is * the site owner's call. * * 3. The resized copies WordPress makes of an uploaded JPEG or PNG (768, * 1024 px…), which are what phones download, are saved as WebP: about a * third smaller than a JPEG and far smaller than a PNG. The uploaded original * keeps its format, so a social preview that uses the full-size image still * works everywhere (some networks don't read WebP). Applies to new uploads; * `wp media regenerate --skip-delete` converts earlier ones and keeps their * old files for any post that links to them directly. * * Settings live in the astsp_performance option: * lcp_image (bool, default true) * hero_image (bool, default true) — see hero.php * delay_scripts (bool, default false) * webp_sizes (bool, default true) * content_images (bool, default true) — see content-images.php * * @package Astute_Speed */ if ( ! defined( 'ABSPATH' ) ) { exit; } require_once __DIR__ . '/hero.php'; require_once __DIR__ . '/admin.php'; require_once __DIR__ . '/content-images.php'; /** Narrowest image, in pixels, that can be the lead image. */ const ASTSP_PERFORMANCE_LEAD_MIN_WIDTH = 400; /** Seconds after load when held-back scripts load even without interaction. */ const ASTSP_PERFORMANCE_DELAY_TIMEOUT = 6; /** * Script sources held back when delay_scripts is on. * * Matched against scripts another plugin or the theme has already put on the * page; Astute Speed never loads these itself and sends nothing to Google * (see "External services" in readme.txt). * * @return string[] Substrings matched against a script's src. */ function astsp_performance_delayed_sources() { return (array) apply_filters( 'astsp_performance_delayed_sources', array( 'googletagmanager.com/gtag/js', 'googletagmanager.com/gtm.js', 'pagead2.googlesyndication.com/pagead/js/adsbygoogle.js', ) ); } /** * One setting, with its default. * * @param string $key Setting name. * @return bool */ function astsp_performance_setting( $key ) { $defaults = array( 'lcp_image' => true, 'hero_image' => true, 'delay_scripts' => false, 'webp_sizes' => true, 'content_images' => true, ); $settings = (array) get_option( 'astsp_performance', array() ); return (bool) ( $settings[ $key ] ?? $defaults[ $key ] ?? false ); } /** * The post whose featured image leads the page: the post itself on a single * post or page, the first post of the list on the blog, an archive or search. * * @return int Post ID, or 0. */ function astsp_performance_lead_post() { if ( is_singular() ) { return (int) get_queried_object_id(); } if ( is_home() || is_archive() || is_search() ) { global $wp_query; return isset( $wp_query->posts[0] ) ? (int) $wp_query->posts[0]->ID : 0; } return 0; } /* * Featured image: the first one rendered on the page loads at once. * * get_the_post_thumbnail() fires begin_/end_fetch_post_thumbnail_html around * the wp_get_attachment_image() call, so the attribute filter can tell a * featured image from a logo or an image in the content. */ $GLOBALS['astsp_performance_in_thumbnail'] = false; $GLOBALS['astsp_performance_lead_done'] = false; add_action( 'begin_fetch_post_thumbnail_html', function () { $GLOBALS['astsp_performance_in_thumbnail'] = true; } ); add_action( 'end_fetch_post_thumbnail_html', function () { $GLOBALS['astsp_performance_in_thumbnail'] = false; } ); /** * Marks the first featured image on the page as eager and high priority. * * Lazy-load plugins skip an image carrying data-no-lazy, the skip-lazy class * or loading="eager" (WP Rocket, Jetpack, a3 and most themes honour these). * * @param array $attr Image attributes. * @param WP_Post|null $attachment The image. * @param string|array $size Requested size. * @return array */ function astsp_performance_lead_image_attributes( $attr, $attachment = null, $size = null ) { if ( is_admin() || is_feed() || ! astsp_performance_setting( 'lcp_image' ) || ! $GLOBALS['astsp_performance_in_thumbnail'] || $GLOBALS['astsp_performance_lead_done'] ) { return $attr; } // A small thumbnail (a ticker or "latest posts" list printed above the // main slider, as on ChromeNews) is never the largest thing on screen. // Leave it lazy and keep looking. $src = $attachment ? wp_get_attachment_image_src( $attachment->ID, $size ) : false; if ( $src && (int) $src[1] < ASTSP_PERFORMANCE_LEAD_MIN_WIDTH ) { return $attr; } $GLOBALS['astsp_performance_lead_done'] = true; astsp_performance_remember_lead_size( $size, $attr['sizes'] ?? null ); $attr['loading'] = 'eager'; $attr['fetchpriority'] = 'high'; $attr['data-no-lazy'] = '1'; $attr['class'] = trim( ( $attr['class'] ?? '' ) . ' skip-lazy' ); return $attr; } add_filter( 'wp_get_attachment_image_attributes', 'astsp_performance_lead_image_attributes', 20, 3 ); /** * The kind of page the lead image is on: its theme may print a different * image size on each (a full-width hero on an article, a card on a list). * * @return string single|front|archive */ function astsp_performance_lead_context() { if ( is_singular() ) { return 'single'; } return is_front_page() || is_home() ? 'front' : 'archive'; } /** * The stored sizes with $size (and the theme's "sizes" attribute) recorded * for $context, or null when that is what is stored already. * * Pure so it can be unit-tested. * * @param array $stored Context => array{size: string|int[], sizes: ?string}. * @param string $context Page kind. * @param string|array $size The size the theme asked for: a name, or [width, height]. * @param string|null $sizes The img's sizes attribute. * @return array|null */ function astsp_performance_lead_sizes_with( array $stored, $context, $size, $sizes ) { $entry = array( 'size' => is_array( $size ) ? array_values( array_map( 'intval', array_slice( $size, 0, 2 ) ) ) : (string) $size, 'sizes' => null === $sizes || '' === $sizes ? null : (string) $sizes, ); if ( '' === $entry['size'] || array() === $entry['size'] || ( $stored[ $context ] ?? null ) === $entry ) { return null; } $stored[ $context ] = $entry; return $stored; } /** * Remembers the size the theme printed the lead image at on this kind of * page, so the next page's preload asks for the same file. Guessing "large" * preloaded a 1024 px file where Kadence's category list prints 768 px and * TrendyNews prints its own sizes — a second download competing with the * image actually shown (on one test page, 77 KB * beside a 50 KB image). Written only when it changes. * * @param string|array $size The size asked for. * @param string|null $sizes The img's sizes attribute. * @return void */ function astsp_performance_remember_lead_size( $size, $sizes ) { if ( null === $size ) { return; } $next = astsp_performance_lead_sizes_with( (array) get_option( 'astsp_performance_lead_sizes', array() ), astsp_performance_lead_context(), $size, $sizes ); if ( null !== $next ) { update_option( 'astsp_performance_lead_sizes', $next, true ); } } /** * Preloads the lead featured image, so the download starts while the browser * is still reading rather than once it reaches the . * * The size, srcset and sizes are the ones the theme printed the lead image * with on this kind of page last time (astsp_performance_remember_lead_size), * so the browser picks the same file for both and fetches it once. Until a * page of that kind has been seen, it guesses: full on an article, large on * a list. */ function astsp_performance_preload_lead_image() { if ( ! astsp_performance_setting( 'lcp_image' ) ) { return; } $post_id = astsp_performance_lead_post(); $thumb = $post_id ? (int) get_post_thumbnail_id( $post_id ) : 0; if ( ! $thumb ) { return; } $learned = ( (array) get_option( 'astsp_performance_lead_sizes', array() ) )[ astsp_performance_lead_context() ] ?? null; $size = $learned['size'] ?? ( is_singular() ? 'full' : 'large' ); $src = wp_get_attachment_image_url( $thumb, $size ); $srcset = wp_get_attachment_image_srcset( $thumb, $size ); $sizes = $learned['sizes'] ?? wp_get_attachment_image_sizes( $thumb, $size ); if ( ! $src ) { return; } printf( '' . "\n", esc_url( $src ), $srcset ? ' imagesrcset="' . esc_attr( $srcset ) . '"' : '', $sizes ? ' imagesizes="' . esc_attr( $sizes ) . '"' : '' ); } add_action( 'wp_head', 'astsp_performance_preload_lead_image', 1 ); /** * Whether a script source is one to hold back. * * @param string $src Script URL. * @return bool */ function astsp_performance_is_delayed( $src ) { if ( '' === (string) $src || is_admin() || ! astsp_performance_setting( 'delay_scripts' ) ) { return false; } foreach ( astsp_performance_delayed_sources() as $needle ) { if ( false !== strpos( $src, $needle ) ) { return true; } } return false; } /** * Holds back an enqueued script (Site Kit's gtag is one): the browser does not * run a script whose type it does not know, and the loader below swaps it in. * * @param string $tag Script tag HTML. * @param string $handle Script handle. * @param string $src Script URL. * @return string */ function astsp_performance_delay_enqueued( $tag, $handle, $src ) { // Already held back by astsp_performance_delay_printed(): WordPress 6.3+ // builds enqueued tags with wp_get_script_tag(), which runs that filter first. if ( ! astsp_performance_is_delayed( $src ) || false !== strpos( $tag, 'astsp/delayed' ) ) { return $tag; } $tag = preg_replace( '/(]*?)\stype=([\'"])[^\'"]*\2/i', '$1', $tag, 1 ); // Rewrites the tag WordPress already built rather than printing a new one. return preg_replace( '/]*?)\ssrc=/i', '