Pixel Whip

All the Cool Kids to the Back of the Bus

Improving Drupal's page load performance by loading javascript at the bottom of the page.

An easy way to improve page load performance of any website is to move all your javascript calls to the bottom of the page, just inside the closing <body> tag.  By default, Drupal loads all its javascript at the top of the page, inside the head tag.  If you want to see what I mean, open up a Drupal site and run it through ySlow.  Chances are, you'll get a big F.

So, how do we fix this? Well, our first inclination might be to move  <?php print $scripts; ?> down, so it sits just before the closing </body> tag in our html.tpl.php file.  This actually works, but has some drawbacks. For one, it essentially prevents any other scripts from being added to the top of the page without either hardcoding them into the template or creating a new javascript scope (More on scope in a bit.)  There are still some cases where scripts need to be loaded at the top. For instance, html5shiv, which brings the awesomeness of HTML5 elements to old school browsers, needs to be loaded in the <head>, in order for it to work.

The other potential side effect happens when we turn on Drupal's built-in JS aggregation (which we always do, right?) By just moving the $scripts variable further down the page, we possibly end up with extra aggregated js files and more HTTP Requests than we need-- something that will degrade page load performance as well.  This has to do with javascript scope in Drupal.

Javascript scope is what Drupal uses to determine where scripts should be added to the page.  It is also the first of many criteria used in determining which scripts should be aggregated together when aggregation is turned on. Drupal has two built-in scopes: Header and Footer. Both are mapped to php variables that can printed in your html.tpl.php template. The header scope is what is printed with the $scripts variable. When you add <?php print $scripts; ?> to your template, Drupal will print out all the scripts scoped to header, nicely formatted in <script> tags.

Somewhat less intuitive, the footer scope doesn't have its own specific variable. Its scripts are slapped on to the end of the $page_bottom region variable. So, if we just move $scripts down to the bottom of our template, we are potentially printing two groups of scripts that could be aggregated into one. There is a better way.

Enter hook_js_alter().  hook_js_alter allows us to modify any scripts added via drupal_add_js().  This hook fires during drupal_get_js(), before scripts are aggregated and printed to the screen.  If the scope is not specified in drupal_add_js(), Drupal will set the scope to header by default. Using hook_js_alter(), we can go through the list of scripts and change the scope of each to footer. The code goes in your theme's template.php file and is actually quite simple:

<?php
/**
 * Implements hook_js_alter()
 */

function MYTHEME_js_alter(&$javascript) {
  // Collect the scripts we want in to remain in the header scope.
  $header_scripts = array(
    'sites/all/libraries/modernizr/modernizr.min.js',
  );

  // Change the default scope of all other scripts to footer.
  // We assume if the script is scoped to header it was done so by default.
  foreach ($javascript as $key => &$script) {
    if ($script['scope'] == 'header' && !in_array($script['data'], $header_scripts)) {
      $script['scope'] = 'footer';
    }
  }
}
?>

So, what is this doing?  First we create an array of scripts we want to stay in the header.  In this example, we are using modernizr (in conjunction with the modernizr module) which includes html5shiv.js.  As mentioned earlier, html5shiv.js needs to be loaded in the <head> tag to function properly.  Note: Creating this array isn't really necessary, being that it only contains one script. However, doing so will make it easier, should we need to move more scripts to the top of the page down the road. Next we cycle through each script checking to see if its scope is currently set to header and it is NOT in our array.  If those conditions are met, we change the scope to footer.  That's it.  No need to muck with any template files.

Now let's see what yslow has to say.

Yeah, that's what I thought, yslow. It's worth noting that some scripts may have been set to header on purpose by their module developer.  There really isn't any way, that I know of, to tell whether or not a script was scoped to header on purpose or if it was set by default. As this is a blanket approach, you'll just need to be mindful of your scripts and their loading order.  

As mentioned earlier, scope is one of many criteria used to determine aggregation and script load order.  To learn more, check out the drupal_add_js() api.