Web Design

Performance: Making js sit at the back of the bus

Error message

Warning: Undefined array key "password" in DatabaseConnection_mysql->__construct() (line 349 of /var/www/d7/includes/database/mysql/database.inc).

It's a common recommendation: move javascript to the bottom of the document.  That lets the actual content load faster.  Here's how to do it in Drupal.

Background

I'm currently in the middle of a big project to improve Drupal performance on a site I'm developing.  The true solution would be to use a performant host like Pantheon, Acquia, or Aberdeen.  But this is the Airport, and we haven't built the political will to outsource that yet.  (Maybe that's a good thing; my job is predicated on the idea that we don't want to outsource certain things.)

Most Drupal performance recommendations have to do with caching: varnish, memcache, etc.  Unfortunately that won't work on this site.  This particular site will be low traffic, but every user will be authenticated and there are a ton of Views on every page.  So, performance matters, but the standard solutions won't work.  So we dig deeper.

Javascript placement

A popular suggestion is to move js to the bottom of the page.  This is recommended by both Yslow and Google Pagespeed.  This is a good idea because when javascripts load and execute, they can freeze up loading & rendering on the rest of the page.  The consequence of moving js to the bottom is your page will momentarily load without it, and then "change" when the js runs.  In most cases this is a minor cosmetic issue and is worth the performance improvement.  (If you really need a particular js to load at the top, keep reading.  We can do that.)

There is no explicit way of telling Drupal to put js at the bottom, and there is no contrib module for it either.  But the good news: there's a relatively simple snippet you can add:

function YOURTHEME_js_alter(&$javascript) {
  foreach ($javascript as $key => &$script) {
    if ($script['scope'] == 'header') {
      $script['scope'] = 'footer';
    }
  } 
}

Okay great, but there are problems.  Some scripts have to run in the header, like Modernizr or the scripts from the Timeline module.  So let's make our script a little better:

function YOURTHEME_js_alter(&$javascript) {
  $header_scripts = array(
    'sites/all/libraries/modernizr/modernizr.min.js',
    'misc/drupal.js',
    'sites/all/modules/jquery_update/replace/jquery/1.5/jquery.min.js',
  );

  foreach ($javascript as $key => &$script) {
    if ($script['scope'] == 'header' && !in_array($script['data'], $header_scripts)) {
      $script['scope'] = 'footer';
    }
  } 
}

This is better.  Every single javascript will sink to the bottom of your page, except the ones you designate.  Just one problem: cdata.

CDATA

CDATA is an old xml standard for passing variables into html, where they can be read by javascript.  Some people will tell you that you can get rid of the cdata altogether, as long as your page is rendering in html5 (browser compatibility notwithstanding).  Unfortunately that's wrong; cdata is still necessary for some modules to work properly (like admin menu).

Also, depending on your aggregation settings, you may see css files referenced in your cdata.  This seems to be a behavior of the Advanced Aggregation module.  This will create a false positive in Google Pagespeed, because it recommends that css be placed in the page head, not in the footer with the javascript.

Our function discriminates between javascripts based on the script "data," which is really just a pointer to where the actual script lives.  CDATA is rendered as javascript, but there is no "data" in the array.  What to do?

if ($script['scope'] == 'header' && !in_array($script['data'], $header_scripts) && $script['type'] == 'file') {

Our new "if" statement checks for three things:

  • Is the javascript currently assigned to the header?  (They all are by default.)
  • Is the javascript in our list of things to keep in the header?
  • Is our javascript an actual script, and not cdata?

If all those things are true, then it will change the js scope to "footer," and Drupal will automagically render the js at the bottom of the page.

Conclusion

Let's put it all together:

/**
 * Implementaion of hook_js_alter()
 * Move most javascripts to bottom of page, but allow overrides to keep certain js in <head>
 */

function YOURTHEME_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',
    'sites/all/modules/timeline/js/timeline.js',
    'sites/all/libraries/simile_timeline/timeline_js/scripts/l10n/en/labellers.js',
    'sites/all/libraries/simile_timeline/timeline_js/timeline-bundle.js',
    'sites/all/libraries/simile_timeline/timeline_js/timeline-api.js',
    'sites/all/libraries/simile_timeline/timeline_ajax/simile-ajax-api.js',
    'sites/all/libraries/simile_timeline/timeline_ajax/simile-ajax-bundle.js',
    'misc/drupal.js',
    'sites/all/modules/jquery_update/replace/jquery/1.5/jquery.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['type'] == 'file') {
      $script['scope'] = 'footer';
    }
  } 
}

I recommend you put this in your theme's template.php.  Hope it helps!

(Is that a bad reference on the 50th anniversary of the March on Washington?)