WordPress – Script and Style Tags – Adding Defer, Async, and Lazy Load

  • infoFull Post Details
    info_outlineClick for Full Post Details
    Date Posted:
    Aug. 11, 2019
    Last Updated:
    Aug. 11, 2019
  • classTags
    classClick for Tags

One of the downsides to WordPress is that they don’t always have the best exposed methods / APIs for modifying how HTML generated by their own system (rather than your content like posts or pages) gets echoed out. For example, WP devs are familiar with ‘wp_enqueue_script()‘ in PHP, which will generate a <script></script> tag output in HTML, but there are no arguments on that method to add attributes or even to simply grab the raw text output of it. To do anything like this requires using WordPress Filter, which is not always straightforward.

I started looking into this since I wanted to be able to add defer or async loading to scripts and styles, which requires modifying attributes of the outputted tags.

Where are the filter calls actually generated

If you start to search for how to modify the HTML that is generated by “wp_enqueue_style” and “wp_enqueue_script”, you will quickly come across posts recommending that you hook into filters, “style_loader_tag” and “script_loader_tag” respectively. But why does this work? How?

Since WordPress is open source, we can find out!

After a little digging, we can see that the function that actually turns queued scripts and styles into echoed out <link> and <script> tags is do_item($handle), which is a public method on the WP_Scripts and WP_Styles classes. Both methods also call a named filter. Here is a summary:

Type HTML Generated by Uses filter: Actual filter call: Arguments passed to filter
Script WP_Scripts::do_item($handle, $group) script_loader_tag Link $tagHtml, $handle, $src
Style WP_Styles::do_item($handle) style_loader_tag Link $tagHtml, $handle, $src, $media

Modifying the output of do_item()

The way both of the do_item() filters work is that it passes a bunch of arguments to a function you hook into the filter with add_filter(), and it expects that your function returns a string that represents the new <link> or <script> tag that it should use. For example, if you wanted to change all non-https scripts to https, just in case a plugin tries to add one, you could do something like this:

<?php
add_filter('script_loader_tag', function($tag, $handle, $src){
    $tag = preg_replace('/^http:\/\//', 'https://', $tag);
    return $tag;
},10,4);

You can do pretty much anything you want to the tag HTML string; this is both beneficial and dangerous. On one hand, this gives a great deal of flexibility, but on the other hand, it is very easy to craft malformed HTML. Plus, like I mentioned, all you can return is the string of HTML for WP to embed – there is not a method for adding or removing a particular attribute, which means most of the time relying on Regex to modify the tag passed by WP.

Putting it together in a reusable way

There are a lot of hackish ways you can add filters all over your functions.php to file modify the output of queued scripts and styles, but there are also some ways to craft reusable methods that will make modifying script and style tags easier. For one, since both filters have almost identical arguments, we can add a single function as the callback if we want to. Here is how I have done that:

<?php
/**
 * Callback for WP to hit before echoing out an enqueued resource
 * @param {string} $tag - Will be the full string of the tag (`<link>` or `<script>`)
 * @param {string} $handle - The handle that was specified for the resource when enqueuing it
 * @param {string} $src - the URI of the resource
 * @param {string|null} $media - if resources is style, should be the target media, else null
 * @param {boolean} $isStyle - If the resource is a stylesheet
 */
function scriptAndStyleTagCallback($tag, $handle, $src, $media, $isStyle){
    // ...
    // Function body omitted
}

add_filter('script_loader_tag',function($tag, $handle, $src){
    return scriptAndStyleTagCallback($tag, $handle, $src, null, false);
},10,4);
add_filter('style_loader_tag',function($tag, $handle, $src, $media){
    return scriptAndStyleTagCallback($tag, $handle, $src, $media, true);
},10,4);

Configurable defer, async, and lazy CSS loading

Next, I wanted to be able to easily enqueue certain scripts and styles in a way that would generate output HTML with async or defer loading. My current approach is as follows:

First, have a variable that holds the handles (unique identifiers) of any scripts or styles that need to be given special treatment:

$specialLoadHnds = (object) array(
    'scripts' => (object) array(
        'async' => array(),
        'defer' => array()
    ),
    'styles' => (object) array(
        'async' => array(),
        'asyncPreload' => array()
    )
);

Items are added to this variable through helper functions I’ve added:

<?php
// Same signature as wp_enqueue_style, + $loadMethod as last arg
function wp_enqueue_style_special($handle, $srcString, $depArray, $version, $media, $loadMethod){
    global $specialLoadHnds;
    array_push($specialLoadHnds->styles->{$loadMethod},$handle);
    wp_enqueue_style($handle, $srcString, $depArray, $version, $media);
}
// Same signature as wp_enqueue_script, + $loadMethod as last arg
// Reminder - $inFooter should probably be false for both async and defer
function wp_enqueue_script_special($handle, $srcString, $depArray, $version, $inFooter, $loadMethod){
    global $specialLoadHnds;
    array_push($specialLoadHnds->scripts->{$loadMethod},$handle);
    wp_enqueue_script($handle, $srcString, $depArray, $version, $inFooter);
}

/**
 * Example: Adding a script with 'async'
 * wp_enqueue_script_special('jquery','https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js',array(),false,false,'async');
 */

Next, within scriptAndStyleTagCallback(), I check the incoming $handle variable and see if it is contained in any of the special arrays. If it is, I apply a treatment as follows:

  • Scripts:
    • ‘async’
      • add ‘async=”true”‘ to <script> tag
    • ‘defer’
      • add ‘defer’ attribute to <script> tag
  • Styles
    • ‘async’
      • set ‘media=”none”‘ and ‘onload=”if(media!=’all’)media=’all'”‘
      • Add <noscript></noscript> block that contains raw $tag, in case user has JS disabled
      • Based on this post
    • ‘asyncPreload’
      • set ‘rel=”preload”‘ and ‘as=”style”‘ and onload=”this.onload=null;this.rel=’stylesheet'”
      • Add <noscript></noscript> block that contains raw $tag, in case user has JS disabled
      • Based on loadCSS approach, and should be used with their preload polyfill for browsers that don’t support the ‘preload’ attribute

Here is everything all put together, into a single file that I require() into my functions.php file:

https://raw.githubusercontent.com/joshuatz/joshuatz-wp-theme/master/inc/special-loader.php

Leave a Reply

Your email address will not be published. Required fields are marked *