Four Kitchens
Insights

Use Grunt and AdvAgg to inline critical CSS on a Drupal 7 theme

10 Min. ReadDevelopment

Howdy, perfers! Last week I went through the process of generating critical CSS using Gulp and an npm module called critical. The big caveat is that Critical doesn’t currently offer out-of-the-box support for remote URLs, which is the most convenient option for dynamic sites (including the one you’re reading). However, there are tools available for this. I’ll walk you through the complete process of generating and inlining critical CSS, then further modifying your Drupal 7 site to remove all render-blocking assets from the head of your theme.

Generate critical CSS with grunt-criticalcss

This time we’ll use grunt-criticalcss, a different npm module that does the same task as Critical. The feature we’re interested in is the ability to analyze a remote URL. Since pages of a dynamic CMS such as Drupal or WordPress are accessible only via URL, critical requires an extra step involving saving the HTML to disk if you wish to analyze your live site. Not so with grunt-criticalcss.

Read grunt-criticalcss docs

Our blog has been using Grunt for a couple years so it’s a great example for this walkthrough. Here’s the configuration of our Gruntfile.js which is inside our primary theme. If you are new to Grunt, go ahead and consult the getting started guide before continuing with this article.

grunt.initConfig({
  criticalcss: {
    home: {
      options: {
        url: 'http://fourword.dev',
        width: 1400,
        height: 900,
        filename: 'css/style.css',
        outputfile: 'css/critical-home.css'
      }
    },
    article: {
      options: {
        url: 'http://fourword.dev/article/fix-scrolling-performance-css-will-change-property',
        width: 1400,
        height: 900,
        filename: 'css/style.css',
        outputfile: 'css/critical-article.css'
      }
    }
  }
});

We can define as many implementations of the task as we want. In this case I’m creating unique versions of the critical CSS for our homepage and an article. Since this optimization is primarily for first-time visitors, check your analytics to determine your site’s most common entry points. A brief explanation of each option:

  • home/article define each subtask. You can run them all in sequence by calling grunt criticalcss, or specifying a particular subtask by including its name prefixed with a colon: grunt criticalcss:home. When running the task, you will see them in the console output as criticalcss:home or criticalcss:article.
  • url is the URL that will be fetched. You could use your live site, but I have specified a local environment so I can run this against my current branch while developing. Whatever URL you choose, the HTML will be paired with the CSS specified in filename options and then rendered using PhantomJS.
  • width and height define the viewport size that should be considered “critical” — that is, only styles which apply to elements visible within this window will be output in our final file. Although this optimization makes a bigger difference on mobile devices, setting a larger viewport normally works out just fine, producing a small enough file.
  • filename points Grunt to a local copy of your CSS which serves as the original CSS to work off of. The docs don’t mention file globbing and a bit of experimentation indicated that this must be a single, pre-existing file.
  • outputfile defines where Grunt should place the critical CSS. Since this is task is run inside a Drupal theme, a relative path of css/critical-TYPE is all that is needed to put it in a convenient place for inclusion.

As mentioned in the previous article, there are issues you need to watch out for within your inline CSS, or it might actually have a negative impact on page load performance. Read the caveats about critical CSS filesize and render-blocking styles if you’re not familiar already.

Use a Drupal hook to inline your styles

There’s no single method for adding this code to Drupal. It depends on how your site is set up so I leave it to your discretion. Our example will use THEME_preprocess_page():

function fourword_theme_preprocess_page(&$vars) {
  //
  // Inline critical CSS
  // First, define the options for drupal_add_css();
  //
  $critical_css_options = array(
    'type' => 'inline',
    'scope' => 'header',
    'group' => CSS_SYSTEM,
    'weight' => -1000,
    'preprocess' => FALSE,
  );

  // Determine which file to inline.
  if ($vars['is_front'] == TRUE) {
    $critical_css_file = 'critical-home.css';
  }
  else {
    $critical_css_file = 'critical-article.css';
  }

  // Inline the CSS
  drupal_add_css(file_get_contents(drupal_get_path('theme', 'fourword_theme') . '/css/' . $critical_css_file), $critical_css_options);
}

There is great documentation for drupal_add_css() but let me explain the reason for this specific configuration:

  • type should be 'inline' — that is the goal after all!
  • scope determines whether the CSS will appear within the <head> or near the bottom of the closing </body> tag.
  • group is the next sorting mechanism. For all assets with a particular scope, Drupal then looks at their group, which are output from lowest to highest number. CSS_SYSTEM is a Drupal constant for defining CSS aggregates. This constant tells Drupal that you want this CSS to be in the first group, typically reserved for CSS that comes from Drupal core.
  • weight is the next number considered when ordering the CSS aggregates. Within each group, the weight of each file is sorted from lowest to highest. A weight of -1000 is extreme, but normally means you’ll see your inline CSS showing up first above anything else.
  • preprocess instructs Drupal to skip the step where it would check if it should combine this file with any other CSS files within the site. It’s less likely to happen with inline files, but setting this to FALSE ensures that it won’t ever happen.

From there, we do a quick conditional within PHP to see if we’re loading the homepage or not by checking the is_front variable within $vars. The homepage has its own set of inline styles that we defined in the Gruntfile, remember? Finally, the drupal_add_css() command informs Drupal about our CSS.

Be sure to use that file_get_contents() command! If you omit that function, the inline styles will simply be the path to your file. Read more about this technique in the context of one-off JavaScript within Drupal. It lets you manage separate CSS/JS files but still gain the benefits of inlining.

Configure AdvAgg module to optimize CSS/JS

Ok, we’re getting there! These steps are crucial: removing render-blocking assets from the <head> and allowing CSS to be loaded in an asynchronous manner. If you stopped before making these changes, the <link> and <script> tags would remain in the head of your document, and they would block rendering until their contents were fetched.

Luckily the process is straightforward if you use the Advanced CSS/JS Aggregation Drupal module, the go-to solution for serious frontend performance problems on a Drupal 7 site.

Note: we’re using AdvAgg 7.x-2.14 for these instructions and screenshots. Past or future releases might present different configuration options.

Enable AdvAgg modules

AdvAgg, as it’s nicknamed, contains almost all the options you could ever ask for in terms of frontend performance for Drupal 7. It even includes configuration to use Filament Group’s loadCSS script in a safe, automated fashion. It will work with your aggregates, so you can still let Drupal combine your theme’s CSS with other modules’ styles.

First, enable advagg, advagg_bundler, and advagg_mod. These are, respectively, the main module, a submodule to alter how CSS/JS is bundled together, and a submodule to modify how these bundles are positioned within the HTML and loaded.

Log in as a user with appropriate admin permissions and navigate to /admin/config/development/performance/advagg. On the Configure tab, uncheck the following option under Global Options:

  • Ensure that Enable advanced aggregation is checked
  • Ensure that Use cores grouping logic is NOT checked
Screenshot of AdvAgg global configuration

Reduce CSS bundles

Your next stop will be the Bundler tab. Although this step is optional, I find it’s very effective at eliminating the numerous aggregates that Drupal creates in a common site that uses ctools, Views, and so forth. Configure as follows:

  • Ensure the Bundler is Active setting is enabled.
  • Set the Target Number Of CSS Bundles Per Page setting to 1.
Screenshot of AdvAgg bundler configuration

Restricting bundles can be complicated, so be sure to check a few different types of pages and ensure that the same aggregate appears on each page for anonymous users. If the aggregates aren’t consistent across pages, your visitors cannot reuse cached styles. When this happens, increase the CSS bundles until you observe some aggregates which are common to your pages, or simply skip this step and disable the Bundler.

If you really want to troubleshoot it, consider using the Information tab to identify which styles are being added within a given aggregate, and write some rules to suppress them for anonymous users. I’ve written a separate article dedicated to troubleshooting your aggregates using the Detailed Information tool, using this tutorial as context for the process.

Move all JavaScript to the bottom of the page

Moving on to the Modifications tab, we will make a few changes here. Since our goal is to remove all render-blocking assets from the <head> of our HTML, moving JavaScript to the bottom of the page is a necessary step in the process.

Just a heads-up, this is not always a safe operation. If you have written JS within your markup, and that JS expects jQuery to exist, this will break your JavaScript. Hopefully though, you have written your JS so that it can be safely moved to the footer. If you haven’t, it would be a whole different post to describe the reasons behind and process of fixing the JS.

If all of your custom JS is written using IIFEs that accept Drupal and jQuery as parameters, you’re most likely ok. Just make sure it isn’t being added directly into your HTML, and is only being included using drupal_add_js(). Furthermore, check that you aren’t relying on any modules which write JavaScript to the page with the implicit expectation that jQuery exists.

Find the Adjust JavaScript location and execution section and adjust the following setting:

  • Set Move JS to the footer to All (might break things).
Screenshot of AdvAgg configuration to alter location of JavaScript

Load CSS asynchronously

Stay on the Modifications tab; we’ve got a bit more work to do. Scroll down until you see the Adjust CSS location and execution section. This is where we enable the built-in copy of Filament Group’s loadCSS library and ask AdvAgg to use it to load all aggregates. Adjust the following settings:

  • Set Deferred CSS Execution: Use JS to load CSS to All in footer except for JS loading code (If enabled this is recommended).
  • Set How to include the JS loading code to Inline javascript loader library (If enabled this is recommended).
Screenshot of AdvAgg asynchronous CSS configuration

After saving the settings you’ll probably need a cache clear, but if all goes well, you’ll be left with two things in your <head>: the inline CSS generated by Grunt, and a small block of inline JS which will be used to load the remaining assets. Since neither of these make external requests, the browser doesn’t have to wait to render anything when it first consumes the page’s HTML.

Test your fine work

Whew! We made it. Try running PageSpeed Insights or WebPageTest.org on your new configuration to see how it performs. That PageSpeed link is for this site, which went from 79 to 97 based on the optimizations described in this article! o/

Additionally, WebPageTest reported a First Paint time that was 3.5 seconds sooner. That’s 3.5s less time spent staring at a blank, white screen. In the data below you might notice that we actually increased our total payload by a couple KB, but we loaded everything in a manner that lets the browser show content more quickly. That approach is very beneficial to our visitors, so WebPageTest factors that into its scores.

Check out the following data for yourself. I ran the test twice because because Test 1 was not an identical test. I accidentally used a lower-powered device, but despite that it still showed a massive improvement. I ran Test 2 using the exact same device/connection to get a true delta. Luckily, the three tests have a strikingly similar TTFB (Time To First Byte) within 0.03 seconds of each other, so these numbers are indeed a fair comparison across the board.

ConditionsTotal KBTTFBSpeed IndexFirst paintTest data
Moto G – 3G, render-blocking489 KB0.592s46144.511s📊
Moto E – 3G, inline + async491 KB0.599s19021.090s📊
Moto G – 3G, inline + async491 KB0.619s14420.973s📊

Remember, it’s important to test using a remote server. In our case it was a snap because we use Pantheon’s CI workflow to test our code against the live database before deploying. Since the test environment is identical to production, it allows us to easily see how our changes will perform once they’re deployed.

Making further improvements

I’ll be the first to say our implementation isn’t perfect, but it gives you a good idea of how dramatically the initial page load can be improved. Since the majority of our blog’s traffic comes from folks arriving at individual articles, it’s a step in the right direction. Further improvements could be made by eliminating our brief FOIT (Flash of Invisible Text) using a font observer.

Additionally, we could set and check a cookie, so that the asynchronous loading only happens on a fresh session. If a user continues to browse around the site, it can intelligently switch back to regular <link> tags since the CSS files will be cached after the initial page load. For the typical Drupal site this involves tweaking Varnish and a few other Drupal settings, so that will have to wait for another day.

Tell me the trade-offs

You knew it was coming… here are some side-effects. I leave it to the reader to decide whether they are negative or not.

The most obvious is the FOUC (Flash of Unstyled Content) which is manifest most commonly when fonts take a moment to load. If you’re in the camp that gets really irked by the FOUT (Flash of Unstyled Text) before fonts load in, then I would say just undo all your work and go back to render-blocking fonts. However, be aware that sync fonts can cause many problems. This past Monday, TypeKit had downtime which left text invisible on many sites. Progressively enhancing text by async-loading fonts should be considered a core feature of the web platform, not a visual problem that needs to be avoided.

This is a Drupal-specific one, but when you’re logged in as an admin, this technique will sometimes cause the UI styles to load in a noticeably fragmented manner. Again, it’s just a consequence of all the CSS being loaded asynchronously bit by bit. I’m not aware of a configuration option in AdvAgg to restrict optimizations to a specific theme. So if it bothers your editors then perhaps setting up loadCSS manually to affect your public styles only is your best option.

Hopefully this walkthrough was useful to you. If you got a huge boost from following this tutorial drop a link to your site in the comments so we can see your implementation.