Using UnCSS and grunt-uncss with WordPress

Following on from my last blog post about modernising WordPress development with grunt, we’ve been using the process for a few months at Storm and have been slowly making it better.

This week, while working on pretty heavy project, we noticed that our compiled single CSS file was a huge 6,500 lines and ~180kb. This is still better than loading a bunch of different CSS files, but still less than ideal.

The problem is, we use Zurb Foundation, and not using a bootstrap/foundation/grid isn’t an option for us – You get way too much stuff for free, and I call BS on the usual argument that “all sites look the same” – that just means you’re not using it properly, or you’re throwing something together lazily.

Zurb is awesome – this site is built in it – but, by it’s nature, it contains probably 75% more stuff than you’re actually going to use, and there is really no point in making the browser download and parse the 75% of stuff you’re not actually using. This is where UnCSS comes in.

What is UnCSS?

UnCSS looks at all your html and figures out what selectors you actually use in the finished project, stripping out everything else that isn’t used. If you’re using any framework, or library that requires CSS, the chances are, this will save you a huge percentage of your minified CSS file.

In the case of dynamic or CMS powered websites, there is no complete html that contains all of the selectors that you’re using. UnCSS is aware of this and it supports reading URLs as well as local files – but that doesn’t really work for a CMS website – you could have hundreds of URLs, and they’re going to be changing regularly.

Applying UnCSS to WordPress

Originally, my idea to solve this was to use the W3 Total Cache files to load instead, but this would be messy and evil whilst relying on way too many other things to be in place locally, and the cache to be fully built at the time you run the UnCSS task. After 15 minutes or so it became clear this wasn’t going to work.

The answer to me was to create a WordPress plugin that gave me a list of every single published page on the site, somehow feed that into our grunt process, and then use Addy Osmani’s grunt-uncss.

WordPress’s WP_Query makes it crazy simple for us to get every published page on the site in one line, then we throw every URL into an array and echo it json_encoded. The plugin and instructions on how to make this work is available on my github as a gist, here. Once you’ve got that loaded, you can add the grunt-exec plugin to download the sitemap to your site root (I added sitemap.json to my .gitignore file here too, just to be clean)

Grunt Tasks

Here’s the grunt task which handles downloading the sitemap from our local build URL:

exec: {
  get_grunt_sitemap: {
     command: 'curl --silent --output sitemap.json http://gladdy.local/?show_sitemap'
  }
}

Next, we need to load the sitemap.json file into the Gruntfile in order to pass the URLs into the grunt-uncss task. I played a bunch with this, tried all kinds of config options, but as usual with this kind of stuff, it ended up being way simpler than I thought:

grunt.registerTask('load_sitemap_json', function() {
  var sitemap_urls = grunt.file.readJSON('./sitemap.json');
  grunt.config.set('uncss.dist.options.urls', sitemap_urls);
});

That task overwrites the uncss:dist task’s options.urls setting with our list JSON list of valid page paths, and here is what that UnCSS task looks like:

uncss: {
  dist: {
    options: {
      ignore       : [/expanded/,/js/,/wp-/,/align/,/admin-bar/],
      stylesheets  : ['css/app.css'],
      ignoreSheets : [/fonts.googleapis/],
      urls         : [], //Overwritten in load_sitemap_and_uncss task
    },
    files: {
      'css/app.clean.css': ['**/*.php']
    }
  }
}

We tell UnCSS to process our already compiled css file, css/app.css, and to ignore anything hosted on google fonts. We also get it to load all our .php files – just to give it a good starting point, though actually isn’t necessary. The UnCSS task unminifies any code you pass into it, so you need to run it back through your minifier after you’ve processed it.

Edit: I’ve updated this article here to include an ignore option which excludes a bunch of default styles that are only enabled when you login, (like the wordpress admin bar) or on javascript actions in Zurb, such as expanded for the menu.

Finally, we need a final task that will grab the sitemap, load the sitemap, and run the uncss task…

grunt.registerTask('deploy_build',
  ['exec:get_grunt_sitemap','load_sitemap_json','uncss:dist','sass:dist']);

Conclusions

For this site, containing about 20 pages at the moment, the deploy_build task takes about 3 minutes to run. It’s certainly not something you want as part of your watch task, but should be part of your pre-deploy system. I wouldn’t be surprised to see it take up to 30 minutes if you’ve got a massive WordPress site with hundreds of pages/URLs.

For this site, the UnCSS task reduces the CSS outputted by 81.13%, from 149.29 kB to 28.17 kB – once you pass your newly trimmed file through your minfier, it’ll cut even more off. In this case it goes down to 20.1 kB – that’s a total of 86.53% saved!

UnCSS with WordPress result

Edit: I published the full gruntfile I use to make this blog here

  • MickCreates

    This is brilliant! I’ve been wondering how to use UnCSS in a WordPress build ever since I first read about it.
    I’m going to try this out now on a rather large build and see how it goes!

  • MickCreates

    So in all my luck, I was unable to get this working. I’m not really sure what I’m doing wrong here.
    I’m getting the following error:
    >> Destination (dist/css/compiled.min.css) not written because src files were empty.

    The Sitemap plugin is working fine.

    I hate to be a pain but would you be able to show us your complete Gruntfile?

  • rees_will

    I’m going to second MickCreates request to show us your complete Gruntfile. Thanks!

  • MickCreates

    I don’t think Liam is around, but has anyone else had success with this?

  • lgladdy

    Sorry! I’ve been meaning to build a full gruntfile to give you an idea of how to use it, but kept forgetting. I will get to this in the next 24 hours and post it here 🙂

  • lgladdy

    MickCreates  I published the gruntfile I use for this site here https://gist.github.com/lgladdy/05774c794bedbbde366c

  • lgladdy

    rees_will  I published the gruntfile I use for this site here https://gist.github.com/lgladdy/05774c794bedbbde366c

  • MickCreates

    lgladdy Ahh brilliant! That helped immensely. I was able to go from 701.8 kB → 62.92 kB. That’s huge! Definitely incorporating this in to my workflow now.

    As a side note, it seems I was also having issues due to cURL not being installed correctly (a quick re-install of GIT & a restart sorted that out if anyone else has similar problems).

  • rees_will

    Awesome! I got it working with a few mods. I’ve posted a gist (https://gist.github.com/phoenixMag00/680779f6fa15ba9d28ef) of what ended up working for me. 

    One mod I added is an exec to increase the amount of allowed open files. I’m pretty sure it’s because I’m on a Mac and from what I’ve read it’s pretty conservative on the number allowed opened at the same time. 

    I also had to change the ‘files’ part of uncss from [‘**/*.php’] to [‘**/*.html’]. The php example seemed to cause UNCSS to crawl through every php file in the entire wp-content folder eventually stalling out with errors.

    Anyways, the bootstrap.css file went from 99.96kB to 11.55kB! Thanks again for the code and the idea!

  • rees_will

    Hmmm. I found a snag in the plugin part of this tutorial. It pulls the sitemap perfectly, but it’s missing the homepage, category pages, archive pages, etc.

    I might merge those in when I have a few free minutes. I’ll make sure to post a link to the updated plugin.

    Thanks again!

  • lgladdy

    rees_will  I’ve been meaning to do this for ages but time never allows. It’ll include the homepage if it’s a static page, but not if its just a wordpress default one. I’ll try my best to get this done soon, if you don’t get to it first!

  • rees_will

    lgladdy I found a solid tutorial by Yoast that has solid instructions to create a site map with category pages, author pages and custom post types. I’m pretty sure I have free time today to merge them into what you posted.

    Thanks again!

  • rees_will

    Here is the updated plugin!  https://github.com/phoenixMag00/JSON-Sitemap-Generator-for-Grunt-UnCSS-with-WordPress/blob/master/grunt-plugin.php

    I’m pretty sure I got like every possible WordPress page, but then again I’ve might missed something. Please let me know if anything needs to be added.

  • CFX

    Has anyone attempted this with a site running WooCommerce? My specific concern is the new endpoint structure which eliminated certain pages…

  • AshWhiting

    Nice plugin and process.

    I am having a slight issue with a memory-leak error. It doesn’t stop the job being done, it just generates some rather scary warnings!

  • CFX

    AshWhiting Wow that sounds very scary! 😉

  • rees_will

    AshWhiting Is phantomjs throwing the errors?

  • AshWhiting

    rees_will AshWhiting It appears to be, yes.

    I have looked into it a little and from what I can tell to be a bug in Node itself. The files all compile properly and the task itself runs with no errors
    (node) warning: possible EventEmitter memory leak detected. 11 listeners added. Use emitter.setMaxListeners() to increase limit.

    It works really well apart from the wall of warnings. 😉

  • rees_will

    AshWhiting rees_will Ha! yeah, phantomjs seems to be pretty finicky with grunt-uncss. I get the same errors as well, I’ve also had to reinstall grunt-uncss because phantomjs seems to corrupt out every so often. Still an awesome script though!

  • AshWhiting

    rees_will I can certainly live with a wall of warnings every now and again.

    It’s completely saved my bacon, this script. I have a legacy WordPress site and I’m redeveloping it. The original CSS was 400k – No idea who made it, but it was bad…

    Running this on it got it down to 40k!

    Good work guys on this. 🙂

  • rees_will

    AshWhiting rees_will Yeah, I typically run it over any site built on Bootstrap or Foundation and I normally get about an 80% to 90% size reduction.

    For example, on my current site the Bootstrap css file was around 140kb, now it’s down to 14kb and then with gzip it’s about 3.3kb.

  • Jon

    Has anyone been able to do this with gulp-uncss instead of grunt?

  • lgladdy

    @Jon Hey John – We’ve just switched to gulp instead of grunt, so stand by for an update 🙂

  • timo_burmeister

    lgladdy thx so much! but I really need this to work with gulp  😉

  • Shashank Jaiswal

    it gives me an error “fatal error: uncss/node_modules/css: missing } near line” 17551:1

  • You rock! =)

  • Scottie Watts

    Hey thanks for all of your work. This should help me a lot. I do have a problem. I keep getting the error “fatal error: phantomjs: cannot open about:blank”. I’m new to all this so any suggestions would be welcome.

    • Hey Scottie,

      That’s weird! It would suggest that about:blank is being added to your sitemap.json file — could you search that and confirm and we’ll go from there?

      • Scottie Watts

        Hi Liam, Thanks for your response. I did check the sitemap.json file and “about:blank” is not in the file. I also set up another test site and ran into the same problem. In a Google search I found references to ssl issues and cURl issues that produced the same error but seemed to be unrelated. Any suggestions?
        Once again I thank you for your time.

        • Hey guys. FWIW, I’m getting the same error. I’m running the Grunt task from a folder in “wp-content/themes/” that will eventually contain child theme code. Because the there are no child theme files, I tested the task against the “twentyfifteen directory.

          I assumed that I would have to adjust the code like this…

          uncss: {

          dist: {

          options: {

          stylesheets : [‘../twentyfifteen/style.css’],

          },

          files: {

          ‘css-build/app.clean.css’: [‘../twentyfifteen/**/*.php’]

          }

          ..

          Hoping that I just slightly screwed up the files paths here and I just don’t see it… 🙂

      • wissile sogoyou

        Hi Liam,

        Thank you for the great post, and taking the time to help us optimize our wordpress site. I also run into the same issue as Scottie: Fatal error: PhantomJS: Cannot open about:blank on my local server and on staging server a different error: Fatal error: /path/node_modules/grunt-uncss/node_modules/uncss/node_modules/phridge/node_modules/phantomjs/lib/phantom/bin/phantomjs: /path/node_modules/grunt-uncss/node_modules/uncss/node_modules/phridge/node_modules/phantomjs/lib/phantom/bin/phantomjs: cannot execute binary file

        As Scottie mentioned after doing some researches it looks like it is due to ssl issues from curl. Is there any way we could bypass the curl method and use an other one ?

        Thank you for your help,

    • Zim

      Hey, this seems to be a known issue[1], my temporal fix was to install uncss 0.10.0 – hope it helps you!

      [1]: https://github.com/giakki/uncss/issues/150

  • LCT

    This no longer works – getting “Fatal error: PhantomJS: Cannot open about:blank”

  • Dan Lee

    I’m also getting the: Fatal error: PhantomJS: Cannot open about:blank

    Any progress on figureing out what it might be?

  • Any version for gULP?

  • For the Gulp lovers https://gist.github.com/corysimmons/56e8f6c5c0e58de7facf

    It seems to work on a handful of files just fine, but when I try to run it on my whole site (hundreds of articles) it starts throwing a repeating “select: Invalid argument” error and eventually PhantomJS can’t find a page (that exists…) and the whole task dies.

    Would love if it someone could solve this.

    • ashbryant

      Hi Just used your code (thanks), but I’m having the same issue. Did you get a fix?

  • Nico

    Still no luck with Fatal error: PhantomJS: Cannot open about:blank.

    • Nico

      Ok, a few interesting findings:

      reading the grunt-uncss docs I see that

      files: {
      ‘css/app.clean.css’: [‘**/*.php’]
      }

      could also refer to url instead of files. So I’ve changed that value to the home page url (also tested with any other post/page/cpt url)
      Also I needed to change the stylesheets path to reflect the actual style url like

      ../wp-content/themes/web/css/app.css

      That way it worked perfectly but obviously just for one url.
      I’m now trying to pass the json array to that option and report you back (live debug) …

      • Nico

        latest step was to set
        grunt.config.set(‘uncss.dist.files’, {‘css/app.clean.css’: [sitemap_urls]});

        All pages have been parsed with no errors. Hope it could be useful to anynone facing the same issue ; )

        A tip for large sites: chances are that you do not need to parse 100 posts to find rules in use (unless you’re art directing each post). You could perform different queries and merge them before building the final array, like

        $pt_post = array( ‘post_type’ => ‘post’, ‘posts_per_page’ => 3, ‘orderby’ => ‘rand’, ‘post_status’ => ‘publish’ );
        $pt_post_query = new WP_Query( $pt_post );

        $pt_page = array( ‘post_type’ => ‘page’, ‘posts_per_page’ => -1, ‘post_status’ => ‘publish’ );
        $pt_page_query = new WP_Query( $pt_page );

        $the_query = new WP_Query();

        $the_query->posts = array_merge( $pt_post_query->posts, $pt_page_query->posts );

        $the_query->post_count = count( $the_query->posts );

        the code continues as previously mentioned. The example is limited to two different queries but you could mix and match any combo that make sense for yuor project.

  • Stuart Nelson

    Anyone else come across this error with Foundation — TypeError: Cannot convert undefined or null to object

    After a bit of research seems to be a know issue with Uncss & Foundation due to complex selectors. I tried to chaning my ignore array to get around this to — [/expanded/,/js/,/wp-/,/align/,/admin-bar/,/^meta.foundation/, /f-topbar-fixed/, /contain-to-grid/, /sticky/, /fixed/] — But still no luck. Anyone have a suggestion?