Use Deadweight in Your Integration Suite to Automatically Find Unused CSS Rules

Jason Morrison

We’re refactoring the CSS in Hoptoad – the app is a few years old, and has been through several rounds of design improvement. We’re looking to combine duplicated rules, make selectors more intention-revealing, and want to reorganize the stylesheets to reflect our current best practices. We’re also switching to Sass, which we’ve found to be a tremendous help. Together, these improvements should cut developer and designer time during maintenance and future design improvements.

Before diving in, we wanted to know what CSS is used or unused.

Deadweight is an excellent tool for identifying unused CSS selectors. It’s flexible, too: you can identify paths in your application for deadweight to scan, or use it as an HTTP proxy and click around in your site, or just pass it static HTML files to analyze. There are a few caveats (for example, selectors inserted by Javascript aren’t tracked) but it’s generally a solid approach for finding unused CSS rules.

I didn’t want to spend a lot of time clicking through the site, and wanted to eliminate the possibility that I would forget a page or two to include. I decided to run the Hoptoad integration tests (Cucumber features, in this case) and capture the response bodies.

Rack middleware to the rescue

I wrote the following piece of Rack middleware:

class ResponseLoggerMiddleware
  RESPONSE_LOG_DIR = Rails.root.join('log', 'responses')
  Dir.mkdir(RESPONSE_LOG_DIR) unless Dir.exist?(RESPONSE_LOG_DIR)

  def initialize(app)
    @app = app
  end

  def call(env)
    response = @app.call(env)
    log(response)
    response
  end

  def log(rack_response)
    code, headers, response = rack_response

    if response.respond_to?(:body)
      html = response.body
      filename = "response_#{Time.now.to_f}.html"
      File.open(RESPONSE_LOG_DIR.join(filename), 'wb') do |file|
        file.puts(html)
      end
    end
  end
end

and included it:

# For Rails 2, include in config/environment.rb
# For Rails 3, include in config/application.rb
# Why Rack::Lock? http://guides.rubyonrails.org/rails_on_rack.html#internal-middleware-stack
config.middleware.insert_after 'Rack::Lock', 'ResponseLoggerMiddleware'

After the tests ran, I processed the output with deadweight:

gem install deadweight
cat public/stylesheets/*.css | deadweight log/responses/*

Ta-da! If you’re curious what Deadweight output looks like, here’s what I got: https://gist.github.com/cf34709887bf63b1ec15

Further reading

  • Ryan Bates’ Railscast on Deadweight will get you familiar with the tool.
  • The Helium tool lets you drop a piece of JavaScript on your site and paste in a list of URLs, and will compute unused CSS selectors.
  • SitePoint’s Dust-Me Selectors is a Firefox plugin that identify unused CSS selectors. It can spider through your site with a sitemap, too.

Extra credit

You could bundle this all up into a rake task, run it on your CI server, and fail the build whenever there are new unused CSS rules.