Removing whitespace and compressing CSS files with an HTTP Handler
January 31, 2009This is my third article in the series deconstructing some of the cool functionality of BlogEngine.NET.
In my previous article, Using a custom ASP.NET base page, I demonstrated that one of the uses of BE.NET’s base page was to alter the href attribute of any of the css files listed in the page header, replacing it with a reference to css.axd?name=[original href]. This css HTTP handler both removes unnecessary whitespace (and comments) from the css file as well as compresses it. This article demonstrates just what this handler does.
The CSS HTTP Handler
My first article, Using HttpHandlers to serve image files, gives an overview of how to implement an HTTP handler, so I will not be going into that in detail here. The CSS handler is implemented in just the same way, and follows these general steps:
- Retrieve css file path from the querystring (css.axd?name=[stylesheet.css])
- Use a StreamReader to read this css file into a string in memory
- Pass this css string into a method which strips unnecessary whitespace and comments from it
- Configure some Response headers (This will make the browser and server keep the output in its cache and thereby improve performance)
- Write out the css string to the response stream
- Compress the response
One critical function I’ve left out for brevity is caching. You’ll definitely want to implement some sort of caching so your HTTP Handler doesn’t actually have to perform all of these steps each time. BE.NET does implement caching, so refer to the source code to get an idea of how it is done.
The code
Here’s the meat of the css HTTP handler based off of the one in BE.NET. This class is fully functional (and included in my demo/source code) but it does omit a few things from the BE.NET source code. Click “+ expand source” to view the code:
using System;
using System.Web;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
using System.IO.Compression;
/// <summary>
/// Summary description for CssHandler
/// </summary>
public class CssHandler : IHttpHandler
{
public bool IsReusable
{
get { return false; }
}
public void ProcessRequest(HttpContext context)
{
if (!string.IsNullOrEmpty(context.Request.QueryString["name"]))
{
string fileName = context.Request.QueryString["name"];
string css = string.Empty;
// Check if a .css file was requested
if (!fileName.EndsWith("css", StringComparison.OrdinalIgnoreCase))
throw new System.Security.SecurityException("Invalid CSS file extension");
//load up css
css = RetrieveLocalCss(fileName);
// Make sure css isn't empty
if (!string.IsNullOrEmpty(css))
{
// Configure response headers
SetHeaders(css.GetHashCode(), context);
//write out css
context.Response.Write(css);
// compress
Compress(context);
}
else
{
context.Response.Status = "404 Bad Request";
}
}
}
/// <summary>
/// Retrieves the local CSS from the disk
/// </summary>
private static string RetrieveLocalCss(string file)
{
string path = HttpContext.Current.Server.MapPath(file);
string css = string.Empty;
try
{
using (StreamReader reader = new StreamReader(path))
{
// load CSS content
css = reader.ReadToEnd();
// Optimize CSS content
css = StripWhitespace(css);
}
return css;
}
catch
{ return string.Empty; }
}
/// <summary>
/// Strips the whitespace from any .css file.
/// </summary>
private static string StripWhitespace(string body)
{
body = body.Replace(" ", String.Empty);
body = body.Replace(Environment.NewLine, String.Empty);
body = body.Replace("\t", string.Empty);
body = body.Replace(" {", "{");
body = body.Replace(" :", ":");
body = body.Replace(": ", ":");
body = body.Replace(", ", ",");
body = body.Replace("; ", ";");
body = body.Replace(";}", "}");
// sometimes found when retrieving CSS remotely
body = body.Replace(@"?", string.Empty);
//body = Regex.Replace(body, @"/\*[^\*]*\*+([^/\*]*\*+)*/", "$1");
body = Regex.Replace(body, @"(?<=[>])\s{2,}(?=[<])|(?<=[>])\s{2,}(?= )|(?<=&ndsp;)\s{2,}(?=[<])", String.Empty);
//Remove comments from CSS
body = Regex.Replace(body, @"/\*[\d\D]*?\*/", string.Empty);
return body;
}
/// <summary>
/// This will make the browser and server keep the output
/// in its cache and thereby improve performance.
/// </summary>
private static void SetHeaders(int hash, HttpContext context)
{
context.Response.ContentType = "text/css";
context.Response.Cache.VaryByHeaders["Accept-Encoding"] = true;
context.Response.Cache.SetExpires(DateTime.Now.ToUniversalTime().AddDays(7));
context.Response.Cache.SetMaxAge(new TimeSpan(7, 0, 0, 0));
context.Response.Cache.SetRevalidation(HttpCacheRevalidation.AllCaches);
string etag = "\"" + hash.ToString() + "\"";
string incomingEtag = context.Request.Headers["If-None-Match"];
context.Response.Cache.SetETag(etag);
if (String.Compare(incomingEtag, etag) == 0)
{
context.Response.Clear();
context.Response.StatusCode = (int)System.Net.HttpStatusCode.NotModified;
context.Response.SuppressContent = true;
}
}
#region Compression
private const string GZIP = "gzip";
private const string DEFLATE = "deflate";
private static void Compress(HttpContext context)
{
if (context.Request.UserAgent != null && context.Request.UserAgent.Contains("MSIE 6"))
return;
if (IsEncodingAccepted(GZIP))
{
context.Response.Filter = new GZipStream(context.Response.Filter, CompressionMode.Compress);
SetEncoding(GZIP);
}
else if (IsEncodingAccepted(DEFLATE))
{
context.Response.Filter = new DeflateStream(context.Response.Filter, CompressionMode.Compress);
SetEncoding(DEFLATE);
}
}
/// <summary>
/// Checks the request headers to see if the specified
/// encoding is accepted by the client.
/// </summary>
private static bool IsEncodingAccepted(string encoding)
{
return HttpContext.Current.Request.Headers["Accept-encoding"] != null && HttpContext.Current.Request.Headers["Accept-encoding"].Contains(encoding);
}
/// <summary>
/// Adds the specified encoding to the response headers.
/// </summary>
/// <param name="encoding"></param>
private static void SetEncoding(string encoding)
{
HttpContext.Current.Response.AppendHeader("Content-encoding", encoding);
}
#endregion
}
Beyond registering this HTTP handler in your web.config, the only final step is to put a bit of code in your page (or master page, or base page…) to swap the reference to the local css files to the css.axd handler. I’ve just done this in my Default.aspx page for the demo, and it looks like so:
private bool bCompressCss = true;
protected void Page_Load(object sender, EventArgs e)
{
if (bCompressCss)
CompressCss();
}
/// <summary>
/// Finds all stylesheets in the header and changes the
/// path so it points to css.axd which removes the whitespace.
/// </summary>
protected virtual void CompressCss()
{
foreach (Control control in Page.Header.Controls)
{
HtmlControl c = control as HtmlControl;
if (c != null && c.Attributes["type"] != null && c.Attributes["type"].Equals("text/css", StringComparison.OrdinalIgnoreCase))
{
if (!c.Attributes["href"].StartsWith("http://"))
{
c.Attributes["href"] = "css.axd?name=" + c.Attributes["href"];
c.EnableViewState = false;
}
}
}
}
The result
The end result is a css file that originally looked like this:
/*This is my css file */
body {
margin:5px;
}h1 {
color: Blue;
}h2 {
color:Black;
margin: 10px 0 2px 0;
/* border:solid 1px #000; */
}
To this: (and then compressed to boot)
body{margin:5px}h1{color:Blue}h2{color:Black;margin:10px 0 2px 0;}
Review
I’ve omitted a lot of explanation of the inner workings of the HTTP module. Hopefully most of the concepts are straightforward or covered in the previous BE.NET related articles. One thing that was newer to me was the method of compressing the output. Rick Strahl has a pretty good overview of GZip compression with ASP.NET Contenton his web log, you may want to review.
Tags: css, compression, whitespace removal, http handler
Categories: ASP.NET, BlogEngine.NET, C#, CSS

View Comments & Reactions >>