Fixing Render Blocking Scripts from Third Party Sources in WordPress

Reading Time: 3 minutes

I recently installed Easy Digital Downloads and the Stripe Integration extension on my site. It was super easy. If you’re looking to sell digital products on your site I couldn’t recommend Easy Digital Downloads more. And I’m definitely a Stripe fan after dealing with PayPal’s developer portal issues.

One issue I did run into is that Google PageSpeed Insights decided the Stripe JavaScript was render blocking and dropped the score for my site from the 90s to the mid 60s. Not great when you’ve done a presentation on improving your Google PageSpeed Insights score.

Normally I’d use Autoptimize to minify and concatenate the script along with the rest of my JavaScript files, but since the script is actually loaded from Stripe.com that isn’t possible. Stripe does require you server the JavaScript from https://js.stripe.com/v2 as well. So serving it from my server isn’t an option.

Part of the problem is how Google PageSpeed Insights renders the page for it’s test. It isn’t using a real device. From the Google PageSpeed Insights’ FAQ:

PageSpeed Insights’ analysis does not use real devices. PageSpeed Insights fetches a site with a blink renderer (the rendering engine that powers Chrome) that emulates both mobile device and desktop devices.

That leaves me needing to convince Google PageSpeed Insights that Stripe isn’t a render blocking script. Webpagetest.org already seemed to understand that, but Google has the name recognition. And the 30 point ding in PageSpeed Insights was annoying. Jetpack had occasionally been throwing me a render blocking script with the devicepx JS from the Site Stats feature too. Meaning this fix could kill two birds with one stone.

The easiest way would be to use the async tag to let Google know that the Stripe script isn’t blocking the render of my homepage and posts. Of course there is no great way for adding parameters to scripts that are enqueued with wp_enqueue_script.

Adding an async tag to enqueued scripts

I could just dequeue the script on my homepage and Posts and leave it for the checkout, but I wanted to figure out a good way to add the async parameter. Then I stumbled across someone else that had run into the issue with the Jetpack Site Stats script as well. And within there someone had put together a nice function for adding defer to all scripts except those specifically set to exclude.

The function hooks into the script_loader_tag filter and replaces the `src` attribute in the string with defer="defer" src

Here’s the function:

/*Function to defer or asynchronously load scripts not aggregated by Autoptimize*/
function js_async_attr($tag){

# Do not add defer or async attribute to these scripts
$scripts_to_exclude = array('script1.js', 'script2.js', 'script3.js');
 
foreach($scripts_to_exclude as $exclude_script){
    if(true == strpos($tag, $exclude_script ) )
    return $tag;    
}

# Defer or async all remaining scripts not excluded above
return str_replace( ' src', ' defer="defer" src', $tag );
}
add_filter( 'script_loader_tag', 'js_async_attr', 10 );

Let’s break it down a little.

The script_loader_tag hook (Codex) filters the HTML text of all enqueued scripts. Essentially everytime a script is written to the page this filter runs to output the HTML. That gives us easy access to every script the site is outputting.

This particular function is then excluding certain scripts from having defer added to it via the $scripts_to_exclude array. If the string of the script HTML has any of the excluded scripts in it, the function stops running and returns the original $tag. Otherwise it replaces the string ‘ src’ with ‘defer=”defer” src’.

This works, but I already have the rest of my scripts handled so I only want to include specific scripts and I want to use async instead of defer. This can be done with a few small changes.

/*Function to defer or asynchronously load scripts not aggregated by Autoptimize*/
    function aty_js_async_attr($tag){

        # Add defer or async attribute to these scripts
        $scripts_to_include = array('js.stripe.com', 'devicepx-jetpack.js');

        foreach($scripts_to_include as $include_script){
            if(true == strpos($tag, $include_script ))
            # Async the scripts included above
            return str_replace( ' src', ' async="async" src', $tag );
        }

        # Return original tag for all scripts not included
        return $tag;

    }
    add_filter( 'script_loader_tag', 'aty_js_async_attr', 10 );

I’ve reversed the code a bit. Instead of excluding certain scripts, I’m including certain scripts.

If I wanted to make the Stripe JS async everywhere but on the Easy Digital Downloads checkout page I could use Easy Digital Downloads’ edd_is_checkout() function to see if the user is on the checkout page. I could add that as a condition in the if within the foreach statement like so:

if(true == strpos($tag, $include_script ) && ! edd_is_checkout() )

Now when I hop back over to Google PageSpeed Insights I’m back in the 90s.

Async vs Defer

Before we go let’s take a look at the difference between async and defer.

No Attributes

First, let’s go over how a normal script without async or defer attributes loads. Basically, once the browser hits the script tag it stops loading the HTML, and fetches the script then executes the script before resuming parsing the HTML.

Async

With async set when the browser hits the script tag it continues parsing the HTML while it fetches the script, and then pauses the HTML parsing to execute the script. Once it executes the HTML continues parsing.

Defer

Setting defer allows the browser to download the script while the HTML parses, and holds the execution of the script until the HTML has completed parsing.

For a more detailed explanation with nice images check out Growing with the Web.

If you’ve got a different way to handle this I’d love to hear it in the comments.

If you’re interested in getting more tips and articles directly in your inbox join my list.

Pin It on Pinterest