A tool to help you identifying unused CSS rules

It's very common to have unused CSS rules on a website. Having unused CSS rules is not a good thing. It increases the size of the CSS file, so it increases the loading time. Also, it increases the time needed to parse the file by the browser, it increases the time needed to find matching rules for an element, and it increases the RAM necessary to keep the data in memory. Don't forget about green computing! Last but not least, the more unused rules the more code you have to maintain (for nothing).

It's not always easy to detect which rule is used, because a website doesn't often have only one page. To help me identifying unused rules on this website and reduce the size of my CSS files, I've created a tool. Yep, I'm a developer 😃 You cannot detect every unused rules because some rules are used only when the user interact with the page. For instance, a click can trigger a JavaScript function that will add a class to an element. This rule cannot be detected by an automated tool that parses an html page. However, it works very well on a website like this one that doesn't have a lot of JS. At the end there is only five rules that are detected as unused, because there are added by a JavaScript script.

The tool uses AngleSharp to parse the pages and stylesheets of a website. AngleSharp works like a browser except it doesn't execute JavaScript code. So, it parses a page as describe in the W3C specification and provides methods to find elements that match a CSS rule.

static void Main(string[] args)
{
    var rules = new ConcurrentDictionary<Rule, Rule>();

    var config = Configuration.Default
        // Download page resources such as CSS files
        .WithDefaultLoader(conf =>
        {
            conf.IsResourceLoadingEnabled = true;
            conf.IsNavigationEnabled = true;
        })
        // Parse CSS files and compute the style of every elements
        .WithCss()
        .WithLocaleBasedEncoding();

    // Process the urls set in the command line
    foreach(var url in args)
    {
        // Create a new BrowsingContext to get the page
        var browsingContext = BrowsingContext.New(config);
        var u = Url.Create(url);

        // Open the page
        var openTask = browsingContext.OpenAsync(u);
        var document = openTask.Result;

        // Get all stylesheets of the page
        foreach (var stylesheet in document.StyleSheets.OfType<ICssStyleSheet>())
        {
            // Get all style rules of the stylesheet
            foreach (var cssRule in stylesheet.Rules.OfType<ICssStyleRule>())
            {
                // Get all selectors of a rule (split ".a, .b, .c" into 3 selectors)
                foreach (var selector in GetAllSelectors(cssRule.Selector))
                {
                    var r = new Rule(stylesheet.Href, selector.Text, cssRule.SelectorText);
                    r = rules.GetOrAdd(r, r);
                    if (r.Used) // Don't reprocess a rule that already match an element
                        continue;

                    // Check if the rule match an element of the document
                    var match = document.QuerySelector(r.Selector);
                    if (match != null) // The selector matches an element, so it's used
                    {
                        if (!r.Used)
                        {
                            r.UsageUrl = url;
                        }
                    }
                }
            }
        }
    });

    // Display the list of unused rules
    foreach (var rule in rules.Keys.Where(r => !r.Used).OrderBy(r => r.Url).ThenBy(r => r.Selector))
    {
        Console.WriteLine(rule.Url + ": " + rule.Selector);
    }
}

// Get all selectors of a CSS rule
// ".a, .b, .c" contains 3 selectors ".a", ".b" and ".c"
private static IEnumerable<ISelector> GetAllSelectors(ISelector selector)
{
    if (selector is IEnumerable<ISelector> selectors)
    {
        foreach (var innerSelector in selectors)
            foreach (var s in GetAllSelectors(innerSelector))
                yield return s;
    }
    else
    {
        yield return selector;
    }
}

class Rule : IEquatable<Rule>
{
    public string StylesheetUrl { get; }
    public string Selector { get; }
    public string RuleText { get; }
    public string UsageUrl { get; set; }
    public bool Used => UsageUrl != null;

    public Rule(string stylesheetUrl, string selector, string ruleText)
    {
        StylesheetUrl = stylesheetUrl ?? throw new ArgumentNullException(nameof(stylesheetUrl));
        Selector = selector ?? throw new ArgumentNullException(nameof(selector));
        RuleText = ruleText ?? throw new ArgumentNullException(nameof(ruleText));
    }

    public override bool Equals(object obj) => Equals(obj as Rule);

    public bool Equals(Rule other) => other != null && StylesheetUrl == other.StylesheetUrl && Selector == other.Selector;

    public override int GetHashCode() => HashCode.Combine(StylesheetUrl, Selector);
}

The latest version of this code is available on GitHub: https://github.com/meziantou/Uncss.Net.

Uncss result

Conclusion

AngleSharp is a good library to work with html document. It follows the W3C specification, so it should parse your pages as a browser. There are multiple tools on the web to find unused CSS like uncss. However, I found it nice to develop my own tool that matches exactly my use case. I'm not a big fan of rewriting tools, but this one is very small so it was not time consuming, and it was a good project to work with AngleSharp.

Leave a reply