In web development it’s a often a best practice to only load CSS and JavaScript files on pages where you need them. Developing an efficient website is both a matter of site performance and of professional pride. In WordPress though it’s all too often that a carefully maintained site is suddenly filled with extra files on every page load due to inconsiderate plugins.

Conditionally Loading Scripts and Stylesheets in WordPress For Ajax Requests
Heavy Load, by Martin Leonhardt

These plugins take the easy way out and load their scripts and styles on every page just in case they’re needed, instead of intelligently loading them as needed.

This week I was working on a web map library we wrote called leaflet-php. Most people only need a maps on a handful of pages of their site, so we didn’t want to always load the Leaflet.js JavaScript library, but we needed it to be available whenever code required it.

The usual way to conditionally load scripts and styles in WordPress is to call wp_enqueue_script and wp_enqueue_style when you need the script or style and to not call them if you don’t need it. This works well for plugins that use [shortcodes] or otherwise print their content during the normal page building process.

Our map code is are usually built with this normal page rendering process, but in some cases we needed to load a map with ajax and found that the map scripts weren’t available when we needed them.

When content is loaded with ajax scripts and styles enqueued with wp_enqueue_script and wp_enqueue_style aren’t processed. In these cases, the easy option is, as I mentioned, to enqueue the needed scripts and styles on every page. The easy route seemed wasteful, so I came up with an alternative solution.

The code below uses the standard WordPress enqueueing classes. During the ajax call an inline <script> tag is generated. A list of scripts and styles which have been enqueued are written to a JavaScript object.

When the ajax content is added to the DOM, the inline <script> runs. It checks if the required <script> and <link> tags are already present. If they are not, then the scripts are loaded with a jQuery ajax call, and the CSS stylesheets are loaded into the <head> tag.

 

<?php
/**
 * We'll need access to these two objects.
 */
global $wp_scripts, $wp_styles;

/**
 * Loop over all queued styles. If a style is still in the queue it hasn't been printed yet. 
 *
 * Get its info so we can later check if it needs to be loaded.
 *
 * The queue only has the handles of the css files.
 */
$maybe_missing_css = array();
foreach( $wp_styles->queue as $handle ){
	if ( strpos( $handle, 'leafletphp' ) === 0 ) {
		$maybe_missing_css[] = $handle;
	}
}

/**
 * The queued CSS files might depend on other CSS files. Load depenencies, then get info 
 * about each script from the to_do list.
 */
$missing_css = array();
if ( !empty( $maybe_missing_css ) ) {
	$wp_styles->all_deps( $maybe_missing_css, true );
	foreach ( $wp_styles->to_do as $handle ) {
		$src = $wp_styles->registered[$handle]->src;

		// Some styles may be empty if they only have dependencies and no source. Skip these. We'll process their dependencies.
		if ( empty( $src ) ) {
			continue;
		}

		// Append the absolute URL to the stylesheet if it's a relative path.
		if ( ! preg_match( '|^(https?:)?//|', $src ) && ! ( $wp_styles->content_url && 0 === strpos( $src, $wp_styles->content_url ) ) ) {
			$src = $wp_styles->base_url . $src;
		}

		$missing_css[] = add_query_arg( 'ver', $wp_styles->registered[$handle]->ver, $src);
	}
}

/**
 * Now we do the exact same thing for JavaScript
 */
$maybe_missing_js = array();
foreach( $wp_scripts->queue as $handle ){
	if ( strpos( $handle, 'leafletphp' ) === 0 ) {
		$maybe_missing_js[] = $handle;
	}
}

$missing_js = array();
if ( !empty( $maybe_missing_js ) ) {
	$wp_scripts->all_deps( $maybe_missing_js, true );
	foreach( $wp_scripts->to_do as $handle ) {
		$src = $wp_scripts->registered[$handle]->src;

		if ( empty( $src ) ) {
			continue;
		}

		if ( ! preg_match( '|^(https?:)?//|', $src ) && ! ( $wp_scripts->content_url && 0 === strpos( $src, $wp_scripts->content_url ) ) ) {
			$src = $wp_scripts->base_url . $src;
		}

		$missing_js[] = add_query_arg( 'ver', $wp_scripts->registered[$handle]->ver, $src );
	}
}

/**
 * At this point any CSS files which haven't been printed during this request
 * should be in $missing_css and any JS which hasn't been printed should be in
 * $missing_js. Note that since this may be an ajax request we don't know what
 * was loaded on the actual page, or by other ajax calls.
 *
 * Now we print our HTML, including our <script> tag.
 *
 * In reality this might be bundled in a function and return the HTML as a string
 * which would be included in the ajax response.
 */


// Generate a unique ID for our div, so we can reference it in JavaScript later.
$div_id = 'my_div_' . time();

// Set up the div wrapper.
?>
<div id="<?php echo $div_id;?>">
<script>
// The ajax call may occur many times on a page, but we only need to load the JS/CSS once.
// Use a global to keep track of our promises.
window.js_promises = window.js_promises || [];

// Only run after the document is ready. This code may be being printed inline, in which case
// scripts may be being printed in the footer, in which case we don't want to also print ours.
jQuery(document).ready(function(){

	var maybe_missing_css = <?php echo json_encode( $missing_css ); ?>;

	jQuery(maybe_missing_css).each(function(i,css){

		// Search for any missing CSS files.
		if ( jQuery('link[href="'+css+'"]').length === 0 ) {
			// CSS <link> elements can be added to the DOM at any time and they'll be loaded.
			jQuery('head').append('<link rel="stylesheet" href="'+css+'">');
		}
	});

	var maybe_missing_js = <?php echo json_encode( $missing_js ); ?>;

	jQuery(maybe_missing_js).each(function(i,js){
		if ( jQuery('script[src="'+js+'"]').length === 0 ) {

			// Adding a <script> to the DOM doesn't actually load it. We just do this
			// so that if we have multiple blocks of ajax loaded content we don't end
			// up loading a script for each one.
			jQuery('head').append('<link rel="stylesheet" href="'+js+'">');

			// We could use .getScript() instead of .ajax(), but .getScript
			// doesn't cache requested files, and we want caching.
			var promise = jQuery.ajax({
				dataType: 'script',
				cache: true,
				url: js
			});

			// Gather up the promises.
			window.js_promises.push( promise );
		}
	});

	// jQuery.when takes a comma delimited list of promises/deferred objects.
	// Since we have an array of them, use .apply. 
	jQuery.when.apply( jQuery, window.js_promises ).then(function(){ 
		// This function will run when all of the promises have successfully
		// completed, or in other words, when all our required JavaScript is available.
		var div_id='<?php echo $div_id;?>';

		// With the scripts all loaded we're ready to do something awesome with this div!
		jQuery('#'+div_id).doSomethingAwesome();
	});
});
</script>
</div>

The code should work for in both the standard rendering case as well as when content is loaded with ajax. What you’re seeing above is a cleaned up version of our actual code, which you can see on GitHub, if you’re interested. It looks like a lot of code, but it’s mostly comments.

So next time you’re developing a plugin take those few extra minutes to make sure that your plugin is a good citizen on whatever WordPress site it may be installed on.

2024 tim headshot 5 no smile grey

Tim Cimbura – CEO, CFO and Software Engineer

Tim is an expert in creating custom business solutions that make businesses more effective, productive, and profitable. He specializes in rapid application development with the Claris platform including FileMaker, Laravel, and WordPress. He also knows Apple macOS technology inside and out.