Is there a method of moving the aggregated JS files to the footer? I've taken a look at hook_js_alter but that just gives me access to the individual files, not the aggregated ones.

I would prefer to not move the $scripts variable to the bottom of html.tpl.php

Any tips would be appreciated.

  • 5
    I would prefer to not move the $scripts variable to the bottom of html.tpl.php...that would be the correct way to do it, why would you prefer not to do it that way? – Clive Oct 10 '12 at 8:26
  • 3
    I'd like to keep the option of having some files in the head tag and others not. This way I can sort/choose files programmatically, not necessarily in the theme layer. – Paul Sheldrake Oct 10 '12 at 12:31

Found this excellent code snippet for Drupal 7: https://gist.github.com/pascalduez/1418121

It offers a way to have $script and $head_scripts so that you can specify which JS files need to go in the head. Example, Modernizr should go into the head scripts.

I'm copy pasting below the solution in the link to future proof the answer.

Cheers.

html.tpl.php

<!DOCTYPE html>
<html<?php print $html_attributes; ?>>
<head>
<?php print $head; ?>
<title><?php print $head_title; ?></title>
<?php print $styles; ?>
<?php print $head_scripts; ?>
</head>

<body<?php print $body_attributes;?>>
<?php print $page_top; ?>
<?php print $page; ?>
<?php print $scripts; ?>
<?php print $page_bottom; ?>
</body>
</html>

template.php

// Used in conjunction with https://gist.github.com/1417914

/**
* Implements hook_preprocess_html().
*/
function THEMENAME_preprocess_html(&$vars) {
// Move JS files "$scripts" to page bottom for perfs/logic.
// Add JS files that *needs* to be loaded in the head in a new "$head_scripts" scope.
// For instance the Modernizr lib.
$path = drupal_get_path('theme', 'THEMENAME');
drupal_add_js($path . '/js/modernizr.min.js', array('scope' => 'head_scripts', 'weight' => -1, 'preprocess' => FALSE));
}

/**
* Implements hook_process_html().
*/
function THEMENAME_process_html(&$vars) {
$vars['head_scripts'] = drupal_get_js('head_scripts');
}

Any method of moving scripts around must respect the way scripts are ordered, and thus loaded and executed, since they may perform tasks which cannot be postponed until the DOM is done loading.

Moving the $scripts variable to just above $page_bottom is one quick way of achieving this, and should be safe, but won't let you easily keep any scripts in the header.

The snippet at http://pixel-whip.com/drupal-load-scripts-last.html may be a popular way, but it shows this flaw very clearly. Any script expecting to run before the page is done loading, but after Drupal.settings has been set, will be unable to function properly because settings will always end up at the bottom of the script pile.

The documentation for drupal_add_js() shows how scripts are ordered. Note that the scope matters more than anything else, but drupal_sort_css_js() does not take scope into consideration. This is because scripts from different scopes are normally requested separately, so there's no need to group by that first. Simply mashing all scripts into the 'footer' scope will make 'group' the most important sorting criteria, but scripts in different scopes can have the same group set while still needing to have one load before the other.

The settings object generated on the server and passed to the client as a JSON object creates a slight problem here, as drupal_get_js() normally "hard codes" it just after the header scripts, and thus ignores things like grouping or weighting for the inline script it creates.

One way to move scripts to the footer, while maintaining their relative order and keeping settings correctly positioned, from within a template.php file is like something like the hook implementation below. the code could probably be optimized a bit, especially if you only have 'header' and 'footer' scopes, but still runs quick enough.

/**
 * Implements hook_js_alter()
 *
 * Moves scripts from the header scope (and others) to the footer, maintaining
 * their relative positions by adjusting script groups while also making
 * sure settings are positioned just after the original header scripts.
 */
function MYTHEME_js_alter(&$javascript) {
  // List the scripts we want in to remain in their original scope.
  $untouched_scripts = array(
//   'sites/all/libraries/modernizr/modernizr.min.js',
  );

  $scopes = array(
    'header' => array(),
    'other' => array(),
    'footer' => array(),
  );
  // Scope group weights.
  $header_max = NULL;
  $other_min = NULL;
  $other_max = NULL;
  $footer_min = NULL;

  foreach ($javascript as $key => &$script) {
    // Categorize scripts by original scope.
    switch ($script['scope']) {
      case 'header':
        // Find the last header group weight.
        if (!isset($header_max) || $script['group'] > $header_max) {
          $header_max = $script['group'];
        }

      case 'footer':
        // Find the first footer group weight.
        if (!isset($footer_min) || $script['group'] < $footer_min) {
          $footer_min = $script['group'];
        }
        $scopes[$script['scope']][] = &$script;
        break;

      // It's impossible for to know in which order scopes defined by a
      // theme will be rendered, so just put them all together for now.
      // Ordering these scopes, if needed, is left as an exercise for the reader.
      default:
        // Find the first other group weight.
        if (!isset($other_min) || $script['group'] < $other_min) {
          $other_min = $script['group'];
        }
        // Find the last other group.
        if (!isset($other_max) || $script['group'] > $other_max) {
          $other_max = $script['group'];
        }
        $scopes['other'][] = &$script;
    }
    // Move scripts to footer.
    if ($script['scope'] !== 'footer' && !in_array($script['data'], $untouched_scripts)) {
      $script['scope'] = 'footer';
    }
  }
  if (!isset($header_max)) {
    // Just in case there were no header scripts.
    $header_max = -1;
  }

  $last_max = $header_max + 1;

  // Add settings as an inline script after the last header group.
  if (isset($javascript['settings'])) {
    $inline_settings = array(
      'type' => 'inline',
      'scope' => 'footer',
      'data' => 'jQuery.extend(Drupal.settings, ' . drupal_json_encode(drupal_array_merge_deep_array($javascript['settings']['data'])) . ");",
      'group' => $last_max,
      'every_page' => TRUE,
      'weight' => 0,
    ) + drupal_js_defaults();
    // No need for drupal_get_js() to do this again.
    unset($javascript['settings']);

    $javascript['inline_settings'] = $inline_settings;
  }

  // If there are other scopes, push them all down below the header scripts.
  if (isset($other_min) && $other_min <= $last_max) {
    $diff = $last_max - $other_min + 1;
    foreach ($scopes['other'] as $key => &$script) {
      $script['group'] += $diff;
    }
    $last_max = $other_max + $diff;
  }
  if (!isset($footer_min)) {
    // Just in case there were no footer scripts.
    $footer_min = $last_max;
  }
  // Finally push footer scripts down below everything else.
  if ($footer_min <= $last_max) {
    $diff = $last_max - $footer_min + 1;
    foreach ($scopes['footer'] as $key => &$script) {
      $script['group'] += $diff;
    }
  }
}

This removes the need for any extra modules, and calling drupal_get_js() from anywhere will return the correctly ordered output.

This also works when Drupal's JavaScript aggregation/optimization setting is enabled.

  • Try AdvAgg. Turn on aggressive caching in AdvAgg for faster than core rendering of the link/script html tags. – mikeytown2 May 28 '14 at 0:08
  • 1
    AdvAgg works great for what it does, but it also does a lot more than what was requested. It's main .module file alone is over 2000 lines longer than this snippet, and also works around drupal_get_js() the way Magic does, making it a bit more difficult to follow. That's not wrong, but may be unexpected. – TwoD May 28 '14 at 4:53

All of these examples are incorrect. Edit: The answer from TwoD posted after mine is also legit, although the easiest thing is still to use AdvAgg or Magic modules.

Do not alter your templates or create new ones. Do not create new variables such as $head_scripts. This is incompatible with contrib modules because they don't know about your customizations. The Drupal API has had has had a solution for placing scripts in the footer scope since Drupal 6 was released in 2008.

The only correct way to do this is by using the scope setting within drupal_add_js(), which you can read about within the Drupal 7 API docs:

scope: The location in which you want to place the script. Possible values are 'header' or 'footer'. If your theme implements different regions, you can also use these. Defaults to 'header'.

To re-iterate, this is the only proper way to move scripts into the footer. Any other way is a hack and will potentially break modules. Do not move the $scripts variable around inside your html.tpl.php template. That is wrong.

For Modernizr in particular, you can use the Modernizr Drupal module and it will properly place the JavaScript library at the beginning of your header JS stack... except if you've moved $scripts to the wrong location within the template.

If you'd like to see a code snippet that details how to properly scope all of your JS to the footer read this blog post:

http://pixel-whip.com/drupal-load-scripts-last.html

This pattern has been adopted by the community and several modules and themes respect these settings. Use a module such as Magic or AdvAgg to safely move the remaining scripts into the footer scope of Drupal's JS output. Major base themes such as Omega and Aurora are built to respect these settings as well.

  • 1
    There is nothing wrong with moving $scripts, as long as you keep it before $page_bottom no module will notice. Themes are allowed add their own scopes too. The method you linked to is actually flawed. It does not respect the original script order and puts everything before Drupal.settings = .... The Magic module re-implements drupal_get_js() as magic_get_js(), which does work, but that feels like cheating. ;) – TwoD May 27 '14 at 22:33

Moving the print $scripts to the end of the page is the proper way to do it. In fact, I seriously doubt there is any other way.

I typically do this on all of my sites now. You need to keep an eye out for modules that add elements to the content and not with the Javascript API. Google Custom Search is one module that comes to mind that does this.

On one Drupal 6 site we did a few years ago, we had to port some legacy JS to Drupal and ended up using drupal_get_js() to get the JS for the page, split it out via regular expressions, build up $scripts1, $scripts2, $scripts3, etc, and then add them to $variables via a template_preprocess_page() for printing in the page template. This was a very custom theme, but it ended up as a messy solution. I don't recall how we handled aggregation.

Something similar to this would be your only real solution, and would require a lot of custom work.

  • 1
    I do this the same way. By the way: Modernizr is another example of scripts that need to be in the head. – Bart Oct 10 '12 at 13:30
  • 2
    Yeah, Modernizr is a good example. I typically bake that directly into the theme and bypass drupal_add_js() for that one. – mpdonadio Oct 10 '12 at 13:37
  • Yep, doing it the same way. – Bart Oct 10 '12 at 14:24
up vote 0 down vote accepted

There is a module called Magic that will take care of all this for you instead of rolling your own approach.

Your Answer

By clicking "Post Your Answer", you acknowledge that you have read our updated terms of service, privacy policy and cookie policy, and that your continued use of the website is subject to these policies.

Not the answer you're looking for? Browse other questions tagged or ask your own question.