Cloud Four Blog

Technical notes, War stories and anecdotes

Preferred solutions for responsive images

Scott Jehl recently tweeted:

Time to shift the responsive imgs discussion to how we'd actually prefer to do things, and then make it happen. Attrs? CSS? JS? New headers?

I concur. A couple of months ago, I asked what you preferred as a solution for responsive images. At the time, I didn’t have a strong opinion. But over the last couple of weeks, I’ve become increasingly comfortable with a direction that Scott Jehl, Ethan Marcotte and I discussed on twitter recently.

Short run solution: preparse attribute

In September, Scott Jehl proposed a solution that might be fairly easy for browser makers to implement in the short run: adding a preparse attribute to the script tag.

@grigs silly: <script preparse> //document.src references raw HTML src document.src = document.src.replace( pattern, replacement ) </script>

Like the defer and async attributes, there is a general use case for document authors to be able to tell the browser that a piece of javascript will impact the loading of assets and thus should be executed before parsing begins. There are probably even use cases that have nothing to do with asset downloading where telling the browser to execute javascript before parsing would be beneficial.

One of the other suggestions for solving this was for browsers to standardize on how they load assets. I don’t like that idea because it prevents browsers from experimenting on ways to parse pages and load assets more quickly.

If authors explicitly tell the browser when javascript loading matters (preparse, defer or async), it allows browser makers to experiment freely in other situations.

Long run solution: improvements to img tag

In the long run, I don’t like the idea that solutions require javascript. Images are content or presentation which means it should be possible to handle it with HTML and CSS alone.

Therefore, I would like to see one of these two improvements to the img tag:

<img alt="butterfly"> <source src="butterfly-small.png" media="max-device-width:480px" /> <source src="butterfly-large.png" media="min-device-width:481px" /> </img>

Modified from Bryan and Stephanie Rieger’s Rethinking the Mobile Web talk.

This is my preferred option, but I’m unclear on how older browsers would handle an image tag that contains child elements.

Isaac Lewis put together a test page using this style of markup. It would be great to collect some feedback on old browser support to see if it works or causes problems.

If that won’t work because of legacy browsers, the following variation proposed by Anselm Hannemann should:

<img src="http://cdn.url.com/img/myimage_xs.jpg" media-xs="(min-device-width:320px and max-device-width:640px)" media-src-xs="http://cdn.url.com/img/myimage_xs.jpg" media-m="(min-device-width:640px and max-device-width:1024px)" media-src-m="http://cdn.url.com/img/myimage_m.jpg" media-xl="(min-device-width:1024px)" media-src-xl="http://cdn.url.com/img/myimage_xsl.jpg" />

Why not headers?

I would love the browser to send more data to the server about the device making the request, but I don’t think servers should be necessary for the image tag to work in a responsive design.

I also think there is a decent chance that screen size is just the first of many headers we’d like the browser to send along. I don’t want to open up pandora’s box, but it would be nice to get something that felt more like a comprehensive solution instead of a bandaid.

Why not a different, progressive image format?

I’d love this, but feel wholly unqualified to judge what it would take. I’m not sure how long something like that would take to implement and what sort of patent minefield might lie there.

So let’s start with the preparse attribute

Unless someone points a major flaw with the preparse idea, I’m going to submit it as a feature request to the appropriate people to get the ball rolling.

Which reminds me, does anyone know where I should start? :-)

When is Mobile Safari not Mobile Safari?

When it is AppleCoreMedia. Let me explain.

I wrote recently about a quiz that we built that included HTML5 audio files. In addition to the volume problems already discussed, we encountered intermittent issues where the audio either would not play or would play much later than it should.

For the quiz, we preloaded the HTML5 audio file via javascript. To make sure that the file was set up for caching, we set far future expires headers as well as making sure the file was sufficiently small.

Whenever we displayed the screen that told the user whether or not they got a question correct, we play the preloaded audio file. Most of the time this worked flawlessly, but occasionally the iPhone would play the sound long after the results screen was displayed or not play the file at all.

That’s when things started getting weird. We watched the log files and started seeing some odd behavior. Here’s what we saw:

"GET /trivia HTTP/1.1" 200 2346 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5"
"GET /media/quiz_wrong.m4a HTTP/1.1" 206 337 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /media/quiz_right.m4a HTTP/1.1" 206 337 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /trivia.css HTTP/1.1" 200 988 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5"

See what happened there? The user agent string changed from the normal Mobile Safari one (AppleWebKit) to one I hadn’t seen before: AppleCoreMedia.

It makes sense when you think about it. All video and audio playback gets handled by a system component. That component is responsible for downloading the asset. Ergo, the user agent string should change accordingly.

Surprisingly, this is true on desktop Safari as well. The equivalent user agent string is “Apple Mac OS X v10.6.6 CoreMedia v1.0.0.10J567.”

AppleCoreMedia Caching

On its own, the change from Mobile Safari’s user agent string to AppleCoreMedia’s user agent string would hardly be worth noting. But we were encountering problems with the sound not playing all of the time.

Because we noticed the sound problems happened more frequently on 3G than on WiFi, we started to suspect network issues and wondered if the audio file that we preloaded was getting cached properly. Here’s what we found in the logs:

"GET /trivia HTTP/1.1" 200 2346 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5"
"GET /media/quiz_wrong.m4a HTTP/1.1" 206 337 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /media/quiz_right.m4a HTTP/1.1" 206 337 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /trivia.css HTTP/1.1" 200 988 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5"
"GET /media/quiz_wrong.m4a HTTP/1.1" 206 5494 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /media/quiz_right.m4a HTTP/1.1" 206 2597 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /js/trivia.js HTTP/1.1" 200 2618 -" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5"
"GET /media/quiz_right.m4a HTTP/1.1" 206 337 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /media/quiz_wrong.m4a HTTP/1.1" 206 337 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /media/quiz_right.m4a HTTP/1.1" 206 2597 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /media/quiz_wrong.m4a HTTP/1.1" 206 5494 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"

All of that occurs before the first quiz answer is submitted so it is only in relation to preloading the audio. The files get downloaded multiple times. Lest you think this is simply http chunking per the 206 responses, here are the file sizes:

-rw-r--r-- 1 cloudfour psacln 2256 Dec 10 17:33 quiz_right.m4a
-rw-r--r-- 1 cloudfour psacln 5153 Dec 10 17:33 quiz_wrong.m4a

Before the audio file is ever played, AppleCoreMedia has downloaded 11,662 bytes for quiz_wrong.m4a. At 5153 bytes, the source file is less than half of the total bytes download.

When we could replicate the audio problems, we would find that the server would return a 304 response to AppleCoreMedia letting it know that the m4a had not been modified, but then AppleCoreMedia would go ahead and download it anyways. Of course, this behavior was inconsistent making it difficult to troubleshoot.

To try to narrow down the behavior, I created a simple page with links to m4a and mp3 files. I didn’t do any HTML5 audio embedding and didn’t include javascript. The page was as vanilla as can be. The test file size was 30,196 bytes. Clicking on the m4a link with a clear cache had the following result:

"GET /examples/applecoremedia/trailer_iphone_sound/ HTTP/1.1" 200 653 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5"
"GET /examples/applecoremedia/trailer_iphone_sound/trailer_iphone_trimmed.m4a HTTP/1.1" 200 30551 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5"
"GET /examples/applecoremedia/trailer_iphone_sound/trailer_iphone_trimmed.m4a HTTP/1.1" 304 233 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /examples/applecoremedia/trailer_iphone_sound/trailer_iphone_trimmed.m4a HTTP/1.1" 206 22410 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /examples/applecoremedia/trailer_iphone_sound/trailer_iphone_trimmed.m4a HTTP/1.1" 304 233 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /examples/applecoremedia/trailer_iphone_sound/trailer_iphone_trimmed.m4a HTTP/1.1" 206 22410 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"

This is even more confusing than the last example. The full m4a file is first downloaded by Mobile Safari. Then AppleCoreMedia asks the server if the file has been modified and is told by the server that it hasn’t. AppleCoreMedia then ignores this information and proceeds to download the file. Then it repeats the process for good measure.

And despite setting far future expires headers for everything, the next time you play the m4a file, the same process is repeated.

Because of the issues we had previously had with short audio files, I decided to try a longer video file to see what it would do. Again, I set up a simple page and linked to the video file. The video file was 3,995,176 bytes (3.8MB).

"GET /examples/applecoremedia/trailer_iphone/ HTTP/1.1" 200 729 "http://www.cloudfour.com/examples/applecoremedia/" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5"
"GET /examples/applecoremedia/trailer_iphone/trailer_iphone%20-%20iPhone.m4v HTTP/1.1" 206 386 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /examples/applecoremedia/trailer_iphone/trailer_iphone%20-%20iPhone.m4v HTTP/1.1" 200 70500 "-" "Mozilla/5.0 (iPhone; U; CPU iPhone OS 4_2_1 like Mac OS X; en-us) AppleWebKit/533.17.9 (KHTML, like Gecko) Version/5.0.2 Mobile/8C148 Safari/6533.18.5"
"GET /examples/applecoremedia/trailer_iphone/trailer_iphone%20-%20iPhone.m4v HTTP/1.1" 206 71910 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /examples/applecoremedia/trailer_iphone/trailer_iphone%20-%20iPhone.m4v HTTP/1.1" 206 386 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"
"GET /examples/applecoremedia/trailer_iphone/trailer_iphone%20-%20iPhone.m4v HTTP/1.1" 206 3995571 "-" "AppleCoreMedia/1.0.0.8C148 (iPhone; U; CPU OS 4_2_1 like Mac OS X; en_us)"

Mobile Safari still downloads some part of the m4v file, but it isn’t the full file size (70500 bytes). It is unclear what it does with this data chunk nor why it appears to only be part of the data, but the server reports a 200 response instead of 206.

The next time the video is played, the same pattern repeats. Nothing appears to be cached. Based on previous research into iPhone 4 cache sizes, I would have expect even the 3.8MB video to get cached.

What I Expected to See

To illustrate what I expected to see, I ran with my simple audio file test in Firefox. The only change I made was to use an mp3 file instead of an m4a.

"GET /examples/applecoremedia/trailer_iphone_sound/ HTTP/1.1" 200 653 "-" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3"
"GET /examples/applecoremedia/trailer_iphone_sound/trailer_iphone_trimmed.mp3 HTTP/1.1" 200 28185 "-" "Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; en-US; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3"

No matter how many times I load the mp3 file and then hit the back button to the web page containing the link, neither the web page nor the mp3 file were downloaded again. They had been successfully cached.

What does all this mean?

I wish I knew. For our project, it meant that we could not consistently ensure that the audio file would play at the right moment when the message was displayed on the screen letting the user know if they had got the question right or not. This issue combined with the volume issues caused us to remove the feature.

For consumers using iPhones, it appears to mean that every time you watch a video or play an audio file, that your phone is going to download it again. This makes for a slower experience and puts a seemingly unnecessary burden on the carrier network.

For developers, it is important to realize that video and audio playback in Mobile Safari is not handled by Mobile Safari even if you’re controlling it via javascript and the system media player is never visible. Everything you’ve learned about how Safari and Mobile Safari handle the downloading of assets doesn’t apply when AppleCoreMedia is doing the work instead of Safari.

Finally, this is just one more clue about how mobile is still frontier land. Even something that seems simple like a quiz that plays sounds can bump up against the boundaries of what we know about how mobile browsers work and what they can reliably do.

JavaScript GZIP Compression in WordPress: What’s Possible and what Hurts

WordPress 2.8 introduced some constants and bits and bots that nominally make certain kinds of compression of JavaScript and CSS possible. This post looks at JavaScript compression and explains what is possible, and what is very, very hard.

In a nutshell, with a bit of tweaking, you can GZIP and concatenate the scripts that come packaged with WordPress. GZipping and concatenating anything else–plugin scripts or scripts in your own theme–is not currently supported in any obviously sane way (using WP’s WP_Scripts management class anyway).

I’m going to give away the ending to save you some time:

add_action('wp_enqueue_scripts', 'lyza_force_compress');
function lyza_force_compress()
{
    global $compress_scripts, $concatenate_scripts;
    $compress_scripts = 1;
    $concatenate_scripts = 1;
    define('ENFORCE_GZIP', true);
 
    /* enqueue your scripts here */
}

Putting the above in your functions.php file may result in insanely good compression on your subsequently-enqueued JavaScript files. I say may because GZIP is one of those fidgety server-side things that varies from hosting provider to hosting provider. I tested this approach on two vastly different environments and had good success, but as always with these things, YMMV.

While this may look rather silly and simple, figuring out exactly which constants and globals to set and when to set them took me just about forever.

The most important thing to note is that this only works on scripts that come pre-packaged with WordPress and are listed in the wp_default_scripts() function in wp-includes/script-loader.php. This is slightly irritating.

When it Does Work

The above will concatenate and GZIP all of the applicable enqueued JavaScript into a single request, served out through the wp-admin/load-scripts.php script.

  • By “applicable”, I mean scripts listed in wp-includes/script-loader.php in the wp_default_scripts() function.
  • If you have applicable JavaScript in both the head and the foot, you will end up with two script src tags. If everything is in the foot (or the head), you’ll get one tag. You not only get GZipping, you get fewer HTTP requests! Yay!
  • JavaScript in plugins and your own theme won’t get concatenated and GZipped in this approach, but it certainly doesn’t break it.

In testing, the following:

    wp_enqueue_script('jquery');
    wp_enqueue_script('thickbox');
    wp_enqueue_script('scriptaculous');
    wp_enqueue_script('editor');

Went from:

10 requests, 310kB

to

2 requests, 90kB

with the activation of the above function. Nice! I got two requests because the above enqueueing puts stuff in both the head and the footer.

That’s a 340% improvement on filesize alone.

Why Won’t It Work for All JavaScript?

Because there are no, nada, zilch, none hooks for making it do so without doing some deep surgery and writing a plugin. It boils down to a couple of places in code:

From wp-includes/class.wp-scripts.php in the do_item() function:

100
101
102
103
104
105
106
107
108
	if ( $this->in_default_dir($srce) ) {
		$this->print_code .= $this->print_scripts_l10n( $handle, false );
		$this->concat .= "$handle,";
		$this->concat_version .= "$handle$ver";
		return true;
	} else {
		$this->ext_handles .= "$handle,";
		$this->ext_version .= "$handle$ver";
	}

To get the built-in concatenation and GZIPped delivery, it is necessary that the if statement here evaluate true. I won’t explain the nitty gritty unless you really want me to.

So, my first approach was to add paths to the $wp_scripts->default_dirs Array to add entries for my theme’s scripts and my plugin dir’s scripts. Big fat fail.

The reason is that the actual file — wp-admin/load-scripts.php — that serves up the concatenated, zipped JavaScript is a standalone script. I imagine that this is both for security and for performance. It does not have the complement of WordPress constants and hooks that we are accustomed to. In fact, it is completely and absolutely unpluggable (unless I missed something). It only includes the files that define the WP_Scripts and WP_Dependencies classes, and utility functions for those. It does not include any theme- or plugin-level items.

And it does something that completely shuts down the game. It instantiates a new, clean WP_Scripts object and then calls wp_default_scripts().

If you’ll recall, the WP_Scripts object in a normal WordPress request is a global singleton that manages all of the enqueued scripts. By creating a new instantiation, the load-scripts.php script has obliterated any notion of any enqueued scripts. Further, the wp_default_scripts() function is a hard-coded list of the JavaScript files that WordPress comes packaged with. While this function nominally triggers the ‘wp_default_scripts’ action, this is not usable here because no files are getting included in which I could take advantage of that hook. Further, it redefines do_action() and other API functions as blank functions that do nothing.

The load-scripts.php file then iterates through the requested JavaScript handles (comma-delimited GET) and sees if it recognizes them from the wp_default_scripts() list. As anything that’s in your own theme or in a plugin won’t be in this list, FAIL.

Though I see some possible options for me in the writing-a-plugin realm, I also sadly see that it would replicate functionality and am highly curious as to what the WordPress developers had in mind here.

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

[toc title="JavaScript in Footer"]
Warning: Technical WordPress post ahead!

Overview: Really Getting JavaScript Into the Footer

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.

WP JavaScript Inclusion: Header vs. Footer

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.

wp_register_script() and wp_enqueue_script()

37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/**
 * 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 );
}
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/**
 * 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 );
}

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');

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 );

Totally psyched, you update your theme and use:

wp_enqueue_script('jquery','','','',true);

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.

Why WordPress Default JavaScripts Won’t Move to the Footer

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.

Dependency Handling and “Groups”

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.

How Groups Handling does Something Possibly Confusing

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

85
86
87
88
if ( 0 === $group && $this->groups[$handle] > 0 ) {
	$this->in_footer[] = $handle;
	return false;
}

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 );

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);

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].

Possible Workaround

I don’t like this but it works:

wp_enqueue_script('jquery','/wp-includes/js/jquery/jquery.js','','',true);

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.

Getting JS into the Footer: Quick Summary

// 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);

For My Next Trick…Compression

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.

Are CSS Sprites A Mobile Web “Worst Practice?”

There’s an interesting article on mobiforge.com that talks about the pitfalls of using CSS sprites for mobile web content. While there are several reasons why sprites may not be a good idea, two stand out for me: CSS2 support is required for the necessary background positioning, and there may be longer term performance penalties associated with using the layout engine for positioning on every page.

That performance aspect is something to think about.  The author states that it may be better to deliver a set of images once, paying a relatively small penalty for extra (cached, with long expiration) HTTP transactions, than to pay a layout engine penalty on every subsequent page rendering.

I can’t say for sure, but I can certainly imagine that some mobile devices are better than others when it comes to page rendering performance.  As one of the commenters in the article states, this “brings us back to the golden rule in mobile: know thy browser…”.  What works best in the desktop world may not be best in the mobile world.

The W3C says that CSS sprites are a best practice for the mobile web so — this would seem to be a odds with that recommendation.  What do you think?