A better CSS minifier


Code: VS2008 | VS2005

When you’re designing web applications locally, it’s easy to overlook the payload of your pages when they run so quickly on your development web server, or over the company LAN. But the moment you start taking your projects to the public, you inevitably need to address four challenges for all of the content that you push to a user’s browser: minifying, compression, combining, and caching (both on the browser and on the server). I want to focus on the first of those challenges.

Minification

There are many free tools available for “minifying” javascript and css files. Minification is the process of shortening the number of characters required to express the same functionality, reducing the size of the file before it is compressed. Compression is great, but minification before compression is even better (we end up with less text to compress).

When to apply minification

For good reason, many developers recommend minifying a production version of your scripts and styles to avoid performing the minification at runtime. This recommendation may stem from the fact that if your web site becomes popular, your web server could crash from the load of attempting to dynamically minify, compress, and combine scripts to thousands of users a second (known as the Slashdot effect). I still perform all of my script and style combining at runtime through various handlers and filters, but I ensure that the end result is cached on the server. In the event of a high demand for scripts, the server will dynamically minify, compress, and combine the desired content once, and then serve it out of cache for the others. It’s also easier for me to combine at runtime since I may use third party libraries that inject their own script and styles, so I’m not able to combine all of my content into a single file for release.

Introducing the YUI Compressor

The tools that provide the best combination of safety (they don’t break the intent of your original scripts) and performance (they save more bytes than other alternatives) are the javascript and css minification tools provided by the YUI Compressor. The reason I like this package so much is that the javascript minifier goes beyond comment and whitespace removal, and will refactor internal methods to shorten their names, without breaking the source code. The CSS minification in the YUI compressor, developed by Isaac Schlueter, is outstanding, because it also optimizes redundant CSS code, providing a size savings that you just can’t get from other tools. If you’ve been using comment and white space removal as your minification strategy, I suggest you try these out instead and measure the difference.

Isaac Schlueter’s CSS Minifier for C#

Whether you minify at runtime, or create static production versions of your scripts, it’s handy if you don’t have to rely on executable files to perform the job, so I’ve gone ahead and ported Isaac’s excellent CSS minifier to C#. I used a few extension methods to ease the port from Java to C#, since C# does not have a few of the Regex matching concepts in Java.

License

Keep in mind that this code is subject to the terms of the same BSD license that the YUI Compressor is released under. You can read those licensing terms here. To use, simply import the namespace and run CssMinify on a string instance containing your CSS content.

public static string CssMinify(this string css)
{
    return CssMinify(css, 0);
}

public static string CssMinify(this string css, int columnWidth)
{
    css = css.RemoveCommentBlocks();
    css = css.RegexReplace("s+", " ");
    css = css.RegexReplace("""}""", "___PSEUDOCLASSBMH___");
    css = css.RemovePrecedingSpaces();
    css = css.RegexReplace("([!{}:;>+([,])s+", "$1");
    css = css.RegexReplace("([^;}])}", "$1;}");
    css = css.RegexReplace("([s:])(0)(px|em|%|in|cm|mm|pc|pt|ex)", "$1$2");
    css = css.RegexReplace(":0 0 0 0;", ":0;");
    css = css.RegexReplace(":0 0 0;", ":0;");
    css = css.RegexReplace(":0 0;", ":0;");
    css = css.RegexReplace("background-position:0;", "background-position:0 0;");
    css = css.RegexReplace("(:|s)0+.(d+)", "$1.$2");
    css = css.ShortenRgbColors();
    css = css.ShortenHexColors();
    css = css.RegexReplace("[^}]+{;}", "");

    if (columnWidth > 0)
    {
        css = css.BreakLines(columnWidth);
    }

    css = css.RegexReplace("___PSEUDOCLASSBMH___", """}""");
    css = css.Trim();

    return css;
}

private static string RemoveCommentBlocks(this string input)
{
    var startIndex = 0;
    var endIndex = 0;
    var iemac = false;

    startIndex = input.IndexOf(@"/*", startIndex);
    while (startIndex >= 0)
    {
        endIndex = input.IndexOf(@"*/", startIndex + 2);
        if (endIndex >= startIndex + 2)
        {
            if (input[endIndex - 1] == '')
            {
                startIndex = endIndex + 2;
                iemac = true;
            }
            else if (iemac)
            {
                startIndex = endIndex + 2;
                iemac = false;
            }
            else
            {
                input = input.RemoveRange(startIndex, endIndex + 2);
            }
        }
        startIndex = input.IndexOf(@"/*", startIndex);
    }

    return input;
}

private static string ShortenRgbColors(this string css)
{
    var sb = new StringBuilder();
    Regex p = new Regex("rgbs*(s*([0-9,s]+)s*)");
    Match m = p.Match(css);

    int index = 0;
    while (m.Success)
    {
        string[] colors = m.Groups[1].Value.Split(',');
        StringBuilder hexcolor = new StringBuilder("#");

        foreach (string color in colors)
        {
            int val = Int32.Parse(color);
            if (val < 16)
            {
                hexcolor.Append("0");
            }
            hexcolor.Append(val.ToHexString());
        }

        index = m.AppendReplacement(sb, css, hexcolor.ToString(), index);
        m = m.NextMatch();
    }

    m.AppendTail(sb, css, index);
    return sb.ToString();
}

private static string ShortenHexColors(this string css)
{
    var sb = new StringBuilder();
    Regex p = new Regex("([^"'=s])(s*)#([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])([0-9a-fA-F])");
    Match m = p.Match(css);

    int index = 0;
    while (m.Success)
    {
        if (m.Groups[3].Value.EqualsIgnoreCase(m.Groups[4].Value) &&
            m.Groups[5].Value.EqualsIgnoreCase(m.Groups[6].Value) &&
            m.Groups[7].Value.EqualsIgnoreCase(m.Groups[8].Value))
        {
            var replacement = String.Concat(m.Groups[1].Value, m.Groups[2].Value, "#", m.Groups[3].Value, m.Groups[5].Value, m.Groups[7].Value);
            index = m.AppendReplacement(sb, css, replacement, index);
        }
        else
        {
            index = m.AppendReplacement(sb, css, m.Value, index);
        }

        m = m.NextMatch();
    }

    m.AppendTail(sb, css, index);
    return sb.ToString();
}

private static string RemovePrecedingSpaces(this string css)
{
    var sb = new StringBuilder();
    Regex p = new Regex("(^|})(([^{:])+:)+([^{]*{)");
    Match m = p.Match(css);

    int index = 0;
    while (m.Success)
    {
        var s = m.Value;
        s = s.RegexReplace(":", "___PSEUDOCLASSCOLON___");

        index = m.AppendReplacement(sb, css, s, index);
        m = m.NextMatch();
    }
    m.AppendTail(sb, css, index);

    var result = sb.ToString();
    result = result.RegexReplace("s+([!{};:>+()],])", "$1");
    result = result.RegexReplace("___PSEUDOCLASSCOLON___", ":");

    return result;
}

private static string BreakLines(this string css, int columnWidth)
{
    int i = 0;
    int start = 0;

    var sb = new StringBuilder(css);
    while (i < sb.Length)
    {
        var c = sb[i++];
        if (c == '}' && i - start > columnWidth)
        {
            sb.Insert(i, 'n');
            start = i;
        }
    }
    return sb.ToString();
}
blog comments powered by Disqus