. 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