Skip to main content

Getting all JavaScript into the Footer in WordPress? Not so fast, Buster!

By Lyza Gardner

Published on September 17th, 2009

Topics

Warning: Technical WordPress post ahead!

Quoth the WordPress Version 2.8 feature list:

– Improvements to the script loader: allows plugins to queue scripts for the front end head and footer, adds hooks for server side caching of compressed scripts, adds support for ENFORCE_GZIP constant (deflate is used by default since it’s faster)

At the time, I thought Wow, cool. When I have time, I’ll investigate that and then immediately forgot about it for a few months. During RSS-coffee-breaks I read Lester Chan’s post about how to put JavaScript in the footer (sounds easy enough!) and Andrew Ozz’s post, which left me coated with an intense and foolish optimism about compression.

What I aim to do in this post — part one of a series of three if I find time to investigate the second and third pieces — is explain why it’s not as easy as you’d expect to get some scripts (specifically scripts that come with WordPress by default) into the footer, and how you can make it happen.

If you’re in a hurry, you can skip to the Summary section at the end of the post.

What I read had me believe that it is falling-off-log easy to put all WordPress JavaScript in the footer. If you are trying to include a piece of JavaScript, say, in a plugin, that WP has not previously known about, it is that easy. But woe if you try this on certain scripts that come packaged with WordPress.

Let’s take a look at the functions that are involved in letting WordPress know that you want to use a given piece of JavaScript.

These are taken from wp-includes/functions.wp-scripts.php.

/**
 * Register new JavaScript file.
 *
 * @since r16
 * @see WP_Dependencies::add() For parameter information.
 */
function wp_register_script( $handle, $src, $deps = array(), $ver = false, $in_footer = false ) {
    global $wp_scripts;
    if ( !is_a($wp_scripts, 'WP_Scripts') )
        $wp_scripts = new WP_Scripts();

    $wp_scripts->add( $handle, $src, $deps, $ver );
    if ( $in_footer )
        $wp_scripts->add_data( $handle, 'group', 1 );
}
Code language: PHP (php)
/**
 * Enqueues script.
 *
 * Registers the script if src provided (does NOT overwrite) and enqueues.
 *
 * @since r16
 * @see WP_Script::add(), WP_Script::enqueue()
*/
function wp_enqueue_script( $handle, $src = false, $deps = array(), $ver = false, $in_footer = false ) {
    global $wp_scripts;
    if ( !is_a($wp_scripts, 'WP_Scripts') )
        $wp_scripts = new WP_Scripts();

    if ( $src ) {
        $_handle = explode('?', $handle);
        $wp_scripts->add( $_handle[0], $src, $deps, $ver );
        if ( $in_footer )
            $wp_scripts->add_data( $_handle[0], 'group', 1 );
    }
    $wp_scripts->enqueue( $handle );
}
Code language: PHP (php)

As a theme developer, you may only ever have encounters with wp_enqueue_script(). But let’s look at what each of these functions does so I can then explain the issue.

wp_register_script() tells WordPress about a script, but does not actually cause it to be included in a given page request. By default, WordPress registers a gripload of scripts that are then available to you, the theme hacker, when or if you should need them. A list of these can be found in wp-includes/script-loader.php in the wp_defalut_scripts() function. Highlights include jQuery, prototype, scriptaculous, etc., as well as extensions to those frameworks (e.g. jQuery UI). WordPress (via the WP_Scripts class, itself extended from WP_Dependencies) handles dependencies and makes sure that jQuery UI doesn’t get included without its necessary jQuery, if you should forget to enqueue jQuery itself or it gets enqueued after jQuery UI.

wp_enqueue_script() tells WordPress, hey, I actually need this script, in this request. You may have seen or done something like this:

wp_enqueue_script('jquery');
Code language: PHP (php)

This spools up jQuery and spits out a script tag to include the requested script.

In a lot of cases, it seems easy and straightforward to do this simple enqueue request. jQuery comes packaged with WP and is automatically registered for you, so you just have to hand the wp_enqueue_script() function one argument: $handle.

As a clever theme hacker, you may have noticed that the signature for wp_enqueue_script() and wp_register_script() changed in version 2.8 and this seems exciting. An $in_footer parameter was added:

wp_enqueue_script( $handle, $src = false, $deps = array(), $ver = false, $in_footer = false );
Code language: PHP (php)

Totally psyched, you update your theme and use:

wp_enqueue_script('jquery','','','',true);
Code language: PHP (php)

And wait for the magic. RELOAD! Wait, maybe something’s cached. RELOAD! Wait, what? RELOADRELOADRELOAD.

jQuery is still in the head.

So you get irritable and you quit trying, or like me you spend a few hours deactivating all of your plugins and trying to figure out where the problem is. You notice that the same thing happens with other scripts that WP already knows about (that is, the scripts in wp_default_scripts()). Stubbornly, they won’t get out of your page’s head element.

Here’s why.

The crux is: if the script you are enqueueing has anything defined as a dependency in WordPress’ wp_default_scripts() function, it will ignore your request to put it in the footer (I’ll provide a workaround shortly).

The reason for this is twofold.

Part of WordPress’ dependency handling for scripts involves “groups”, the ins and outs of which I’ll leave as an exercise for an intrigued reader. In an oversimplification, it put scripts with dependencies (e.g. jQuery, Scriptaculous) in groups[0]. It explicitly puts scripts that depend on other scripts in groups[1]. The default behavior of both wp_register_script() is to put a script in groups[0] if the $in_footer argument != true.

OK, now check out this snippet from class.wp-scripts.php (do_item() method):

if ( 0 === $group && $this->groups[$handle] > 0 ) {
    $this->in_footer[] = $handle;
    return false;
}
Code language: PHP (php)

This piece of logic puts groups > 0 into the footer. So that’s how WP decides to move things into the footer–it’s based on where the script is in the $wp_scripts->groups Array.

But here’s a potential pitfall in the wp_enqueue_script() function:

if ( $src ) {
        $_handle = explode('?', $handle);
        $wp_scripts->add( $_handle[0], $src, $deps, $ver );
        if ( $in_footer )
            $wp_scripts->add_data( $_handle[0], 'group', 1 );
    }
    $wp_scripts->enqueue( $handle );
Code language: PHP (php)

If you don’t give wp_enqueue_script() a $src (which you don’t have to provide do if the script has already been registered) for your $handle, it is going to enqueue it in whatever group it’s already registered in. So, when you do your little simple enqueue:

wp_enqueue_script('jquery','','','',true);
Code language: PHP (php)

The entire part of the function where it would put it in the footer doesn’t execute because the if($src) test fails. And jQuery has already been registered—in groups[0].

I don’t like this but it works:

wp_enqueue_script('jquery','/wp-includes/js/jquery/jquery.js','','',true);
Code language: PHP (php)

jQuery is now in the footer.

This does still appear to handle dependencies correctly, but I haven’t deeply tested. Also, there are some valid reasons to have some scripts, and possibly jQuery, in the head. This was just a demo.

// This will NOT put jquery in the footer
wp_enqueue_script('jquery','','','',true);

// But this will, if inelegant and circumventing abstraction
wp_enqueue_script('jquery','/wp-includes/js/jquery/jquery.js','','',true);

Code language: PHP (php)

Really, this post was a red herring. I tripped into this oddity while investigating the second part of the WordPress 2.8 feature claim: adds hooks for server side caching of compressed scripts, adds support for ENFORCE_GZIP constant (deflate is used by default since it’s faster). We’ll talk about that next time.

Comments

Josh said:

Thank you for this workaround… I've been beating my head against my desk for the past hour and a half trying to figure out why in the bloody HELL the documentation says it should work, but for some damn reason, it didn't!!!! haha

Just another example of how the WordPress codex sucks the big one.