Make Your Site Faster with Async and Deferred JavaScript: Introducing script_loader_tag

Today I’m going to discuss a new, and very nice, site speed improvement that became possible in WordPress 4.1. What changed? The introduction of a new filter, script_loader_tag. This filter lets us easily change the HTML markup of enqueued script elements—that is, of JavaScript files that were correctly added into a WordPress site using WordPress’s wp_enqueue_script function.

With script_loader_tag, we can now easily fix a problem that can significantly impact page speed: lots of render-blocking JavaScript.

The Problem: Render-Blocking JavaScript

Long JavaScript files in your head can delay your browser from displaying page content, because its default behavior is first to interpret the JS files themselves.

Properly enqueued JavaScript shows up in the head section of your HTML document. On the internet as in nature, the main thing about a head is that it’s above a body—and this means something fairly serious for site speed, because JavaScript can be render-blocking.

“Render-blocking” comes from a web browser’s default behavior: It wants to completely receive and process everything that’s come higher up in the page, before it moves any further down.

This means that long JavaScript files in your head can actually delay your browser from displaying the page content in the body, because its default behavior is first to interpret the JS files themselves. In other words, JS is blocking the browser’s crucial function of rendering the page out for the user to actually see. The result can be slow sites and frustrated users.

Google’s Pagespeed Insights has been pointing out this issue for a while:

blocking_js_before

Click to enlarge

However, prior to 4.1 and script_loader_tag, the only solution I knew of was to move scripts to the site’s footer. This is difficult to do by hand, and the plugins that claim to do it automatically didn’t work in our case.

Let’s move straight to our success story:

The Goal: Much Less Render-Blocking JS

Here’s what we got down to with the solution we’ll present below:

blocking_js_after

Click to enlarge

Now we’ve only got three JavaScript files that can possibly slow down a page’s rendering. The rest are still there, but they load in parallel with the page content, rather than before it.

The Fix: Defer and Async your JavaScript

The first thing to understand is the alternatives to render-blocking JS: defer and async. We’ll explain the difference, but both work similarly: They let the browser load a JS resource “as time permits,” while attending to other things (like page rendering) as well. This means that you can’t rely on a deferred or asynced JavaScript file being in place prior to page render, as you could without these attributes—but the advantage is that the file won’t slow the speed at which the page becomes visible to users.

Those are concepts—now for code. (The full code is available on GitHub.)

1. Getting Your Script Handles

Every properly enqueued WordPress script has a handle: a “nickname” that the site knows to call it by. We’re going to need these handles for all scripts, and getting them isn’t dead-simple, unfortunately.

It is possible, though:

/*	
* Getting script tags
* Thanks http://wordpress.stackexchange.com/questions/54064/how-do-i-get-the-handle-for-all-enqueued-scripts
*/

add_action( 'wp_print_scripts', 'wsds_detect_enqueued_scripts' );
function wsds_detect_enqueued_scripts() {
	global $wp_scripts;
	foreach( $wp_scripts->queue as $handle ) :
		echo $handle . ' | ';
	endforeach;
}

This code prints out a list of enqueued handles, separated by | , right into the head of every page:

You’ll only do this once, then use “View Page Source” to copy and paste the handles themselves.

Once you’ve done this, deactivate this section of the code: we’ve got our handles, so let’s not clog up our head with them anymore. That’s why this section is commented out in the code on GitHub—I don’t want it to run every time!

2. Deferring and Asyncing Render-Blocking JavaScript

We found that we needed to use defer and not async for WPShout, so I’ll walk through the defer code. Most of the heavy lifting here is from an article by Scott Nelle; thanks, Scott!

add_filter( 'script_loader_tag', 'wsds_defer_scripts', 10, 3 );
function wsds_defer_scripts( $tag, $handle, $src ) {

	// The handles of the enqueued scripts we want to defer
	$defer_scripts = array( 
		'prismjs',
		'admin-bar',
		'et_monarch-ouibounce',
		'et_monarch-custom-js',
		'wpshout-js-cookie-demo',
		'cookie',
		'wpshout-no-broken-image',
		'goodbye-captcha-public-script',
		'devicepx',
		'search-box-value',
		'page-min-height',
		'kamn-js-widget-easy-twitter-feed-widget',
		'__ytprefs__',
		'__ytprefsfitvids__',
		'jquery-migrate',
		'icegram',
		'disqus',
	);

    if ( in_array( $handle, $defer_scripts ) ) {
        return '<script src="' . $src . '" defer="defer" type="text/javascript"></script>' . "\n";
    }
    
    return $tag;
} 

The add_filter line tells us that this code should run anytime an enqueued JavaScript file is about to be printed onto the page as an HTML script element. Letting us filter that HTML is what script_loader_tag is for. (If you need an update on filters and WordPress’s hooks system in general, start here!)

The biggest check in this code is doing a single thing defining the $defer_scripts array. This array lists out the handles of all the elements we want to defer—the handles we found in step 1. (Your handles will, of course, vary!)

The logic below the array definition (beginning with if ( in_array( ) searches for the current script’s handle attribute in the array we’ve just defined. If the handle matches an element in the array, then we modify the script to have the same source, but with a new property: defer="defer", which will cause the script not to block rendering. With this change made, we return the HTML back, and we’re good to go!

(And finally, if the handle isn’t found, we just return the original tag itself, unaltered.)

You’ll know this plugin is working when you view page source and see something like this in the head:

deferred_scripts

When to Use Async Instead of Defer

You use async when you’re linking directly to an external JavaScript library. That link would look something like: <script src="https://oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js"></script>. Notice how it’s a link to the full URL, and the JavaScript will get pulled in

enqueueing external JS is a lot less common, at least for us, since most of our enqueued JS is in themes and plugins that host their own code. At any rate, the code for async is precisely the same as the code for defer—but with the two words switched out. So if you do happen to have a lot of externally hosted enqueued scripts, getting them asynced is a very similar technical process to the one we’ve just covered.

Which Scripts to Defer and Async

You’ll notice that we didn’t defer everything—a few scripts are still render-blocking. Here are rules of thumb on that:

  • Don’t do anything to jQuery. jQuery (handle jQuery) is a key dependency for many other JS files, and you want to let it load early.
  • Any file that’s wrapped in a jQuery( document ).ready( function() { }) call should be fine to defer. That code basically says “Wait until the entire document object model (DOM) loads,” so racing to get the JavaScript file loaded in the head doesn’t serve much purpose.
  • In general, you can defer JavaScript files that rely on user interactions, like clicks and mouse hovers—and files that fix layout details, like center or hide a set of element. Again, these rely on a loaded page to work anyway (which is why they’re almost all going to be wrapped in jQuery( document ).ready( function() { }), or else they’re liable not to work), so you should be safe to get the page out beforehand.
  • It’s, unfortunately, impossible to use this method for JavaScript files that have been added some way other than the generally correct method of enqueueing them. This is another reason to prefer that method over other ways of loading scripts that may appear to work fine at first glance.

Summing Up

This turned into a longish post, but at its core is a very cool and rather quick way to improve your site speed and user satisfaction. I hope you now know enough to defer and async your own JavaScript files, and thanks for reading! I’d love to hear comments or questions below.

Image credit: Celine Nadeau


10 Responses

Comments

  • Miles Fink says:

    Nice post! Just noticing the filter example doesn’t show the modified script tag for async or defer, just an empty string.

    • fredclaymeyer says:

      Thanks, Miles! Should be fixed. That’s the page parsing rather than displaying HTML tags I accidentally left in our syntax highlighter. :)

  • Thanks for the nice post. Next: a plugin that does some or all of the steps?

  • Great article and real world demonstration. I cannot seem to get the ASYNC to work, I tried using the full url without the ?ver=1.5.2 and with the ?ver=1.5.2 at the end of the url…as it shows up on view page source. Neither works. Google Page Speed is still giving me a very bad report. Not sure what to try next…

  • tourismpics says:

    Great article Fred, thanks!

  • Blake says:

    So I defer and async all my scripts including the jquery, and I get an 88/100 on mobile page speed. But I have issues due to deferring jquery. So when I exclude that one, my score goes all the way from 88/100 to 67/100.

    There has to be a way to get better scores and that is such a huge hit.

Pingbacks