I’ll build this as a drop-in performance/defer-scripts.php module for your RX theme, with safe defaults, admin exclusions, critical script protection, defer/async controls, preload helpers, and compatibility guards.
Below is an advanced rx theme / performance/defer-scripts.php file.
Create this file:
rx-theme/
└── performance/
└── defer-scripts.php
Then include it in your functions.php:
require_once get_template_directory() . '/performance/defer-scripts.php';
performance/defer-scripts.php
<?php
/**
* RX Theme - Advanced Script Defer / Async / Preload Manager
*
* File: performance/defer-scripts.php
*
* Features:
* - Adds defer to safe frontend scripts
* - Optional async support by handle list
* - Excludes jQuery, admin, login, customizer, REST, AMP, WooCommerce checkout/cart
* - Protects critical WordPress and plugin scripts
* - Supports per-handle defer, async, module, nomodule, preload, preconnect, dns-prefetch
* - Adds fetchpriority where useful
* - Supports script loading strategy for modern WordPress
* - Adds safe filters for child theme/plugin extension
* - Avoids breaking inline dependent scripts
* - Can remove query strings from static assets
* - Can move selected footer scripts
* - Can delay selected third-party scripts
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'RX_Theme_Defer_Scripts' ) ) :
final class RX_Theme_Defer_Scripts {
/**
* Enable or disable the whole module.
*/
private static bool $enabled = true;
/**
* Default handles that should never be deferred or async.
*/
private static array $never_defer_handles = array(
'jquery',
'jquery-core',
'jquery-migrate',
'wp-polyfill',
'wp-hooks',
'wp-i18n',
'wp-element',
'wp-components',
'wp-blocks',
'wp-block-library',
'wp-dom-ready',
'wp-api-fetch',
'wp-data',
'wp-date',
'wp-edit-post',
'wp-editor',
'wp-rich-text',
'wp-compose',
'wp-url',
'wp-util',
'underscore',
'backbone',
'mediaelement',
'mediaelement-core',
'mediaelement-migrate',
'wp-mediaelement',
'comment-reply',
'admin-bar',
'customize-preview',
'customize-controls',
'customize-selective-refresh',
);
/**
* Handles that are safe to defer by default.
* Empty means auto-detect and defer most safe frontend scripts.
*/
private static array $defer_handles = array();
/**
* Handles that should use async instead of defer.
* Use async carefully only for independent third-party scripts.
*/
private static array $async_handles = array(
'google-analytics',
'google-tag-manager',
'gtag',
'facebook-pixel',
'fb-pixel',
);
/**
* Handles that should be treated as JavaScript modules.
*/
private static array $module_handles = array();
/**
* Handles that should have nomodule.
*/
private static array $nomodule_handles = array();
/**
* Handles to preload.
*/
private static array $preload_handles = array();
/**
* Handles to delay until user interaction.
*/
private static array $delay_handles = array();
/**
* URL keyword exclusions.
*/
private static array $excluded_url_keywords = array(
'/wp-admin/',
'/wp-includes/js/jquery/',
'jquery.min.js',
'jquery-migrate',
'customize-preview',
'customize-controls',
'wp-emoji',
'recaptcha',
'google.com/recaptcha',
'gstatic.com/recaptcha',
'stripe.com',
'paypal.com',
'checkout',
'cart-fragments',
);
/**
* Third-party hosts that may be delayed if handle is in delay list.
*/
private static array $delay_url_keywords = array(
'googletagmanager.com',
'google-analytics.com',
'connect.facebook.net',
'facebook.com/tr',
'hotjar.com',
'clarity.ms',
'doubleclick.net',
'tiktok.com',
'pinimg.com',
);
/**
* Preconnect URLs.
*/
private static array $preconnect_urls = array(
'https://fonts.googleapis.com',
'https://fonts.gstatic.com',
);
/**
* DNS prefetch URLs.
*/
private static array $dns_prefetch_urls = array(
'//fonts.googleapis.com',
'//fonts.gstatic.com',
'//www.googletagmanager.com',
'//www.google-analytics.com',
);
/**
* Initialize module.
*/
public static function init(): void {
self::$enabled = (bool) apply_filters( 'rx_defer_scripts_enabled', self::$enabled );
if ( ! self::$enabled ) {
return;
}
add_filter( 'script_loader_tag', array( __CLASS__, 'filter_script_loader_tag' ), 20, 3 );
add_filter( 'script_loader_src', array( __CLASS__, 'maybe_remove_script_query_strings' ), 20, 2 );
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'apply_script_loading_strategy' ), 999 );
add_action( 'wp_head', array( __CLASS__, 'print_resource_hints' ), 1 );
add_action( 'wp_head', array( __CLASS__, 'print_script_preloads' ), 2 );
add_action( 'wp_footer', array( __CLASS__, 'print_delay_script_runtime' ), 1 );
add_filter( 'wp_resource_hints', array( __CLASS__, 'add_wp_resource_hints' ), 10, 2 );
/*
* Optional footer movement.
* This can improve render speed but may break badly coded scripts.
*/
add_action( 'wp_enqueue_scripts', array( __CLASS__, 'move_selected_scripts_to_footer' ), 1000 );
}
/**
* Check whether current request is frontend-safe.
*/
private static function is_safe_frontend_context(): bool {
if ( is_admin() ) {
return false;
}
if ( wp_doing_ajax() ) {
return false;
}
if ( wp_doing_cron() ) {
return false;
}
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
return false;
}
if ( is_customize_preview() ) {
return false;
}
if ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() ) {
return false;
}
if ( function_exists( 'is_cart' ) && is_cart() ) {
return false;
}
if ( function_exists( 'is_checkout' ) && is_checkout() ) {
return false;
}
if ( function_exists( 'is_account_page' ) && is_account_page() ) {
return false;
}
return true;
}
/**
* Main script tag filter.
*/
public static function filter_script_loader_tag( string $tag, string $handle, string $src ): string {
if ( ! self::is_safe_frontend_context() ) {
return $tag;
}
if ( empty( $src ) ) {
return $tag;
}
if ( self::has_attribute( $tag, 'defer' ) || self::has_attribute( $tag, 'async' ) ) {
return $tag;
}
if ( self::should_never_modify( $handle, $src ) ) {
return $tag;
}
$tag = self::maybe_add_module_attribute( $tag, $handle );
$tag = self::maybe_add_nomodule_attribute( $tag, $handle );
$tag = self::maybe_add_fetchpriority_attribute( $tag, $handle, $src );
if ( self::should_delay_script( $handle, $src ) ) {
return self::build_delayed_script_tag( $tag, $handle, $src );
}
if ( self::should_async_script( $handle, $src ) ) {
return self::add_attribute_to_script_tag( $tag, 'async', 'async' );
}
if ( self::should_defer_script( $handle, $src ) ) {
return self::add_attribute_to_script_tag( $tag, 'defer', 'defer' );
}
return $tag;
}
/**
* Apply WordPress native loading strategy where possible.
*/
public static function apply_script_loading_strategy(): void {
if ( ! self::is_safe_frontend_context() ) {
return;
}
global $wp_scripts;
if ( ! $wp_scripts instanceof WP_Scripts ) {
return;
}
foreach ( (array) $wp_scripts->registered as $handle => $script ) {
if ( empty( $script->src ) ) {
continue;
}
$src = self::normalize_script_src( $script->src );
if ( self::should_never_modify( $handle, $src ) ) {
continue;
}
if ( self::should_async_script( $handle, $src ) ) {
wp_script_add_data( $handle, 'strategy', 'async' );
continue;
}
if ( self::should_defer_script( $handle, $src ) ) {
wp_script_add_data( $handle, 'strategy', 'defer' );
}
}
}
/**
* Move selected safe scripts to footer.
*/
public static function move_selected_scripts_to_footer(): void {
if ( ! self::is_safe_frontend_context() ) {
return;
}
$footer_handles = apply_filters( 'rx_footer_script_handles', array() );
if ( empty( $footer_handles ) || ! is_array( $footer_handles ) ) {
return;
}
global $wp_scripts;
if ( ! $wp_scripts instanceof WP_Scripts ) {
return;
}
foreach ( $footer_handles as $handle ) {
if ( isset( $wp_scripts->registered[ $handle ] ) ) {
$wp_scripts->registered[ $handle ]->args = 1;
}
}
}
/**
* Should this script never be changed?
*/
private static function should_never_modify( string $handle, string $src ): bool {
$never = apply_filters(
'rx_never_defer_script_handles',
self::$never_defer_handles
);
if ( in_array( $handle, $never, true ) ) {
return true;
}
$excluded_keywords = apply_filters(
'rx_defer_excluded_url_keywords',
self::$excluded_url_keywords
);
foreach ( $excluded_keywords as $keyword ) {
if ( false !== stripos( $src, $keyword ) ) {
return true;
}
}
/*
* WooCommerce important scripts.
*/
if ( false !== stripos( $handle, 'wc-' ) ) {
$wc_safe = apply_filters( 'rx_allow_defer_woocommerce_scripts', false, $handle, $src );
if ( ! $wc_safe ) {
return true;
}
}
/*
* Elementor editor/frontend protections.
*/
if ( false !== stripos( $handle, 'elementor' ) ) {
$elementor_safe = apply_filters( 'rx_allow_defer_elementor_scripts', false, $handle, $src );
if ( ! $elementor_safe ) {
return true;
}
}
/*
* Divi/Builder protections.
*/
if (
false !== stripos( $handle, 'et-' ) ||
false !== stripos( $handle, 'divi' )
) {
$builder_safe = apply_filters( 'rx_allow_defer_builder_scripts', false, $handle, $src );
if ( ! $builder_safe ) {
return true;
}
}
return (bool) apply_filters( 'rx_should_never_modify_script', false, $handle, $src );
}
/**
* Should defer?
*/
private static function should_defer_script( string $handle, string $src ): bool {
$explicit_defer = apply_filters( 'rx_defer_script_handles', self::$defer_handles );
if ( ! empty( $explicit_defer ) && is_array( $explicit_defer ) ) {
return in_array( $handle, $explicit_defer, true );
}
/*
* Auto mode:
* Defer theme/plugin/frontend scripts except protected ones.
*/
if ( self::is_local_theme_or_plugin_script( $src ) ) {
return true;
}
/*
* Defer common CDN libraries, but only when not excluded.
*/
if ( self::is_common_safe_cdn_script( $src ) ) {
return true;
}
return (bool) apply_filters( 'rx_should_defer_script', false, $handle, $src );
}
/**
* Should async?
*/
private static function should_async_script( string $handle, string $src ): bool {
$async_handles = apply_filters( 'rx_async_script_handles', self::$async_handles );
if ( in_array( $handle, $async_handles, true ) ) {
return true;
}
$async_keywords = apply_filters(
'rx_async_script_url_keywords',
array(
'googletagmanager.com/gtag/js',
'google-analytics.com/analytics.js',
'connect.facebook.net',
)
);
foreach ( $async_keywords as $keyword ) {
if ( false !== stripos( $src, $keyword ) ) {
return true;
}
}
return (bool) apply_filters( 'rx_should_async_script', false, $handle, $src );
}
/**
* Should delay until interaction?
*/
private static function should_delay_script( string $handle, string $src ): bool {
$delay_handles = apply_filters( 'rx_delay_script_handles', self::$delay_handles );
if ( in_array( $handle, $delay_handles, true ) ) {
return true;
}
$delay_keywords = apply_filters( 'rx_delay_script_url_keywords', self::$delay_url_keywords );
foreach ( $delay_keywords as $keyword ) {
if ( false !== stripos( $src, $keyword ) ) {
return (bool) apply_filters( 'rx_auto_delay_third_party_scripts', false, $handle, $src );
}
}
return (bool) apply_filters( 'rx_should_delay_script', false, $handle, $src );
}
/**
* Convert script tag into delayed script tag.
*/
private static function build_delayed_script_tag( string $tag, string $handle, string $src ): string {
$src_escaped = esc_url( $src );
$handle_escaped = esc_attr( $handle );
return sprintf(
'<script type="text/rx-delayed-script" data-rx-handle="%1$s" data-src="%2$s"></script>' . "\n",
$handle_escaped,
$src_escaped
);
}
/**
* Runtime loader for delayed scripts.
*/
public static function print_delay_script_runtime(): void {
if ( ! self::is_safe_frontend_context() ) {
return;
}
$enabled = apply_filters( 'rx_delay_script_runtime_enabled', true );
if ( ! $enabled ) {
return;
}
?>
<script id="rx-delay-script-runtime">
(function() {
'use strict';
var loaded = false;
var events = ['mousemove', 'mousedown', 'keydown', 'touchstart', 'scroll', 'wheel'];
function loadDelayedScripts() {
if (loaded) {
return;
}
loaded = true;
var scripts = document.querySelectorAll('script[type="text/rx-delayed-script"][data-src]');
scripts.forEach(function(oldScript) {
var newScript = document.createElement('script');
var src = oldScript.getAttribute('data-src');
if (!src) {
return;
}
newScript.src = src;
newScript.defer = true;
if (oldScript.dataset.rxHandle) {
newScript.setAttribute('data-rx-handle', oldScript.dataset.rxHandle);
}
oldScript.parentNode.insertBefore(newScript, oldScript.nextSibling);
oldScript.remove();
});
removeEvents();
}
function removeEvents() {
events.forEach(function(eventName) {
window.removeEventListener(eventName, loadDelayedScripts, passiveOptions());
});
}
function passiveOptions() {
try {
return { passive: true };
} catch (e) {
return false;
}
}
events.forEach(function(eventName) {
window.addEventListener(eventName, loadDelayedScripts, passiveOptions());
});
window.addEventListener('load', function() {
setTimeout(loadDelayedScripts, 3500);
});
})();
</script>
<?php
}
/**
* Preconnect and DNS prefetch.
*/
public static function print_resource_hints(): void {
if ( ! self::is_safe_frontend_context() ) {
return;
}
$preconnect_urls = apply_filters( 'rx_preconnect_urls', self::$preconnect_urls );
$dns_urls = apply_filters( 'rx_dns_prefetch_urls', self::$dns_prefetch_urls );
foreach ( $preconnect_urls as $url ) {
printf(
'<link rel="preconnect" href="%s" crossorigin>' . "\n",
esc_url( $url )
);
}
foreach ( $dns_urls as $url ) {
printf(
'<link rel="dns-prefetch" href="%s">' . "\n",
esc_url( $url )
);
}
}
/**
* Add WP resource hints.
*/
public static function add_wp_resource_hints( array $urls, string $relation_type ): array {
if ( 'preconnect' === $relation_type ) {
foreach ( apply_filters( 'rx_preconnect_urls', self::$preconnect_urls ) as $url ) {
$urls[] = array(
'href' => esc_url_raw( $url ),
'crossorigin' => 'anonymous',
);
}
}
if ( 'dns-prefetch' === $relation_type ) {
foreach ( apply_filters( 'rx_dns_prefetch_urls', self::$dns_prefetch_urls ) as $url ) {
$urls[] = esc_url_raw( $url );
}
}
return array_unique( $urls, SORT_REGULAR );
}
/**
* Print preload links for selected handles.
*/
public static function print_script_preloads(): void {
if ( ! self::is_safe_frontend_context() ) {
return;
}
$preload_handles = apply_filters( 'rx_preload_script_handles', self::$preload_handles );
if ( empty( $preload_handles ) || ! is_array( $preload_handles ) ) {
return;
}
global $wp_scripts;
if ( ! $wp_scripts instanceof WP_Scripts ) {
return;
}
foreach ( $preload_handles as $handle ) {
if ( empty( $wp_scripts->registered[ $handle ] ) ) {
continue;
}
$script = $wp_scripts->registered[ $handle ];
$src = self::normalize_script_src( $script->src );
if ( empty( $src ) ) {
continue;
}
printf(
'<link rel="preload" href="%1$s" as="script" crossorigin="anonymous">' . "\n",
esc_url( $src )
);
}
}
/**
* Remove query strings from static script files.
*/
public static function maybe_remove_script_query_strings( string $src, string $handle ): string {
$remove = apply_filters( 'rx_remove_script_query_strings', false, $handle, $src );
if ( ! $remove ) {
return $src;
}
if ( false === strpos( $src, '.js?' ) ) {
return $src;
}
return remove_query_arg( array( 'ver', 'v', 'version' ), $src );
}
/**
* Add module attribute.
*/
private static function maybe_add_module_attribute( string $tag, string $handle ): string {
$module_handles = apply_filters( 'rx_module_script_handles', self::$module_handles );
if ( in_array( $handle, $module_handles, true ) ) {
$tag = self::replace_script_type( $tag, 'module' );
}
return $tag;
}
/**
* Add nomodule attribute.
*/
private static function maybe_add_nomodule_attribute( string $tag, string $handle ): string {
$nomodule_handles = apply_filters( 'rx_nomodule_script_handles', self::$nomodule_handles );
if ( in_array( $handle, $nomodule_handles, true ) ) {
$tag = self::add_attribute_to_script_tag( $tag, 'nomodule', 'nomodule' );
}
return $tag;
}
/**
* Add fetchpriority attribute.
*/
private static function maybe_add_fetchpriority_attribute( string $tag, string $handle, string $src ): string {
$high_priority_handles = apply_filters( 'rx_high_priority_script_handles', array() );
$low_priority_handles = apply_filters( 'rx_low_priority_script_handles', array() );
if ( in_array( $handle, $high_priority_handles, true ) ) {
return self::add_attribute_to_script_tag( $tag, 'fetchpriority', 'high' );
}
if ( in_array( $handle, $low_priority_handles, true ) ) {
return self::add_attribute_to_script_tag( $tag, 'fetchpriority', 'low' );
}
return $tag;
}
/**
* Add attribute to script tag safely.
*/
private static function add_attribute_to_script_tag( string $tag, string $attribute, string $value ): string {
if ( self::has_attribute( $tag, $attribute ) ) {
return $tag;
}
return str_replace(
'<script ',
sprintf( '<script %s="%s" ', esc_attr( $attribute ), esc_attr( $value ) ),
$tag
);
}
/**
* Replace script type.
*/
private static function replace_script_type( string $tag, string $type ): string {
if ( preg_match( '/type=[\'"][^\'"]+[\'"]/', $tag ) ) {
return preg_replace(
'/type=[\'"][^\'"]+[\'"]/',
'type="' . esc_attr( $type ) . '"',
$tag
);
}
return str_replace(
'<script ',
'<script type="' . esc_attr( $type ) . '" ',
$tag
);
}
/**
* Check attribute exists.
*/
private static function has_attribute( string $tag, string $attribute ): bool {
return (bool) preg_match( '/\s' . preg_quote( $attribute, '/' ) . '(\s|=|>)/i', $tag );
}
/**
* Normalize script source to full URL.
*/
private static function normalize_script_src( string $src ): string {
if ( empty( $src ) ) {
return '';
}
if ( 0 === strpos( $src, '//' ) ) {
return is_ssl() ? 'https:' . $src : 'http:' . $src;
}
if ( 0 === strpos( $src, '/' ) ) {
return home_url( $src );
}
return $src;
}
/**
* Is local theme/plugin script?
*/
private static function is_local_theme_or_plugin_script( string $src ): bool {
$site_url = home_url();
$content_url = content_url();
if ( false !== strpos( $src, $site_url ) || false !== strpos( $src, $content_url ) ) {
if (
false !== strpos( $src, '/wp-content/themes/' ) ||
false !== strpos( $src, '/wp-content/plugins/' ) ||
false !== strpos( $src, '/wp-content/uploads/' )
) {
return true;
}
}
return false;
}
/**
* Detect common safe CDN scripts.
*/
private static function is_common_safe_cdn_script( string $src ): bool {
$safe_cdn_keywords = apply_filters(
'rx_safe_cdn_script_keywords',
array(
'cdn.jsdelivr.net',
'unpkg.com',
'cdnjs.cloudflare.com',
'static.cloudflareinsights.com',
)
);
foreach ( $safe_cdn_keywords as $keyword ) {
if ( false !== stripos( $src, $keyword ) ) {
return true;
}
}
return false;
}
}
RX_Theme_Defer_Scripts::init();
endif;
Optional: add custom controls in functions.php
Use these only when needed.
1. Force defer specific handles
add_filter( 'rx_defer_script_handles', function( $handles ) {
$handles[] = 'rx-main-js';
$handles[] = 'rx-theme-js';
$handles[] = 'rx-navigation-js';
return $handles;
} );
2. Force async specific handles
add_filter( 'rx_async_script_handles', function( $handles ) {
$handles[] = 'gtag';
$handles[] = 'google-tag-manager';
return $handles;
} );
3. Never defer important handles
add_filter( 'rx_never_defer_script_handles', function( $handles ) {
$handles[] = 'my-critical-header-script';
$handles[] = 'my-slider-before-load';
return $handles;
} );
4. Preload important scripts
add_filter( 'rx_preload_script_handles', function( $handles ) {
$handles[] = 'rx-main-js';
return $handles;
} );
5. Delay third-party scripts after user interaction
add_filter( 'rx_auto_delay_third_party_scripts', '__return_true' );
Use this carefully. It can improve PageSpeed but may delay analytics, ads, chat widgets, tracking pixels, and other third-party scripts.
6. Remove query strings from JS files
add_filter( 'rx_remove_script_query_strings', '__return_true' );
7. Move selected scripts to footer
add_filter( 'rx_footer_script_handles', function( $handles ) {
$handles[] = 'rx-main-js';
$handles[] = 'rx-extra-js';
return $handles;
} );
Important safe rule
Do not defer these unless you test carefully:
jquery
jquery-core
jquery-migrate
wp-polyfill
wp-element
wp-i18n
comment-reply
WooCommerce checkout/cart scripts
reCAPTCHA
Stripe
PayPal
Elementor editor scripts
Customizer scripts
For your RX theme, the safest strategy is:
Critical header script = normal
Theme scripts = defer
Analytics / independent third-party = async
Heavy third-party scripts = delay after interaction
jQuery-dependent old scripts = keep normal unless tested