Drupal 7: Taking control of CSS and JS aggregation

Drupal 7 includes a big re-factor of the way CSS and Javascript are aggregated. What does this mean for your sites? In short:

  1. You will see a greater number of files compared to D6 - this is normal and not usually cause for alarm. Surprisingly, more files is sometimes better.
  2. To ensure efficient aggregation, the most important thing developers can do is choose the parameters to drupal_add_css and drupal_add_js carefully. And if you encounter contrib modules that are using the wrong parameters, please file patches!

This was a somewhat controversial change, and understanding the new strategy (and how to override it) requires going a bit deeper.

Previously, all files were merged together into one giant file (at least within the same scope or media type). This strategy worked well on simple sites with few or no contrib modules. Unfortunately, many contrib modules in D6 incorrectly add conditional* CSS and Javascript files – resulting in changing aggregates as you browse from page to page. If even a few lines of code differ, the entire aggregate (including large libraries like jQuery and jQuery UI) must be re-downloaded, consuming extra bandwidth.

* "Conditional" files in this context refers to files that are added to some pages but not others - an example would be calling drupal_add_css() in a theme function. In D6 such calls should have the $preprocess value set to FALSE to prevent splitting the aggregate.

The D7 way

D7 uses a new strategy to address this - aggregates are split into three groups:
CSS_SYSTEM, CSS_DEFAULT, and CSS_THEME for styles, and JS_LIBRARY, JS_DEFAULT, and JS_THEME for Javascript. Each group is further subdivided into files that load on every page, and files that load conditionally based on the 'every_page' option. Note this option has some potential for confusion - it doesn't cause the file to be loaded on every page - it's merely a "hint" to the core system to place it in that group.

Ultimately, the intent of these divisions is to group files into functional groups that are smaller and less likely to be split by an errant conditional style or script. We get separate files for core libraries, the theme, and page-specific files. Looking at the results, a stock Drupal 7 outputs eight stylesheets (including browser styles), while D6 only has three in the default install. Is this optimal? The answer will be different for each site.

Custom tuning

D7 adds two new hooks that allow you to alter styles and scripts before they are rendered:
hook_css_alter()
hook_js_alter()

At least one contrib project taking advantage of this functionality has already been created: Core Library. I haven't had a chance to test it yet, but the module's intent is to optimize the groupings by "learning" as visitors browse your site.

One thing to note if you decide to undertake customizing these groupings: In Drupal 7.0, the insertion order of styles and scripts is always preserved, even at the expense of sometimes splitting a group. A patch has been submitted that proposes relaxing this a bit.

Back to D6

If the previous Drupal 6 strategy is a better match for your site, you can drupal_alter your way back with this code in a custom module:

/**
 * Implements hook_js_alter().
 */
function mymodule_js_alter(&$javascript) {
  uasort($javascript, 'drupal_sort_css_js');
  $i = 0;
  foreach ($javascript as $name => $script) {
    $javascript[$name]['weight'] = $i++;
    $javascript[$name]['group'] = JS_DEFAULT;
    $javascript[$name]['every_page'] = FALSE;
  }
}
 
/**
 * Implements hook_css_alter().
 */
function mymodule_css_alter(&$css) {
  uasort($css, 'drupal_sort_css_js');
  $i = 0;
  foreach ($css as $name => $style) {
    $css[$name]['weight'] = $i++;
    $css[$name]['group'] = CSS_DEFAULT;
    $css[$name]['every_page'] = FALSE;
  }
}
Filed under 

Thanks for advertising for my module, I need a lot of testers, it's still an early development stage.
Nice and efficient small article, thanks.

Speed is life, or at least, part of life for our web enterprises. Aggregation is a way to mitigate the drupal penalty.

So my question is "why are there no comments on this?" It seems so important AND there is a controversy. I happen to be mostly on the do it the "big-file D6 way" camp. In my opinion, fewer loads wins.

This page is the first place I found a way to use the D6 method in D7. Way to go Dylan.

I'm with Marcus: often, fewer requests win. I can see the use case for the current implementation though, but not as a general rule. In that case, the old D6 way seemed to work better.

Also suprised at the apparent lack of comments. Important subject.

Something to consider - our old friend IE 7 - 9 has a limitation of being able to process 4095 selectors in a single css include file. By combining all of the site css into a single include file there's a potential of hitting this limit for IE users.

Our work around was to add extra logic to Dylan's example to create two groups. This helps us stay below the 4095 IE limitation:

uasort($css, 'drupal_sort_css_js');
$i = 0;
foreach ($css as $name => $style) {
$css[$name]['weight'] = $i++;

// Break grouping into two sets - CSS_SYSTEM, (-100) and the rest
if ($style['group'] == -100) {
$css[$name]['group'] = CSS_SYSTEM;
}
else {
$css[$name]['group'] = CSS_DEFAULT;
}

$css[$name]['every_page'] = FALSE;
}

The fact that we have over 4095 selectors in our css is another issue :)

I still don't get it... How are JS files grouped? If they're indeed grouped into 1 of 3 groups, there would never be more than 3 files, right? Then why are there 5? AND a 'bunch' of Drupal.settings (with CSS files I'm deliberately not loading?) Me no get it.

Also, as a module devver, how should I add my very specific/conditional JS and CSS?

There a a few ways that a top-level JS group can get split into subgroups. The most common is probably the "every_page" option to drupal_add_js(); files that have this flag are grouped separately from files that do not. Files with 'scope' => 'footer' are also grouped separately from header files.

This is also the most important option to set accurately in modules that you write. If your JS file is always added to every page (for example in hook_init(), or via the .info file) then you should set this to TRUE. If instead the JS file is added based on some conditional logic, then it must be set to FALSE (the default).

The file names you see in the inline settings object have a different purpose. Drupal.settings.ajaxPageState is used to track what files are loaded as part of the AJAX command system.

It should be noted that AdvAgg fully takes advantage of the D7 hooks and provides some nice features like the IE 4095 CSS limit, better grouping than core if configured to do so (out of the box it will mimic core), as well as minification of CSS/JS.

Yes I do confirm that AdvAgg did a long way since the original post, it's now a great module.

I have tried your code. But it giving me two aggregated js files. One file is having scripts from theme, and the other is having rest of things. How can I make it as a single file?

Add new comment

Restricted HTML

  • Allowed HTML tags: <a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type> <li> <dl> <dt> <dd> <h2 id> <h3 id> <h4 id> <h5 id> <h6 id>
  • You can enable syntax highlighting of source code with the following tags: <code>, <blockcode>, <cpp>, <java>, <php>. The supported tag styles are: <foo>, [foo].
  • Web page addresses and email addresses turn into links automatically.
  • Lines and paragraphs break automatically.
By submitting this form, you accept the Mollom privacy policy.

About the Author

Dylan Tack, Director of Technology

Dylan is a software engineer with more than a decade of experience working with a wide variety of clients including the Linux Foundation, PBS, Habitat for Humanity, TV.com and the Emmys. His background includes training as an electrical engineer, but he became passionate about open source through his work with a university genetics lab.

Dylan is a proud member of the Drupal community, a member of the Drupal security team, and has extensive experience with Perl and Java. His other interests include computer security, embedded design, climbing, and brewing.