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 enqueue
d 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:
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:
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
:
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
enqueue
ing 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 enqueue
d scripts, getting them async
ed 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
enqueue
ing 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.