Combine multiple stylesheets at runtime
I often use multiple stylesheets in a single website to keep things nicely separated. The only problem is that the client has to make multiple HTTP requests to get them all. Today, I thought it was about time I did something about it. The idea is to take all the referenced stylesheet in the <head> tag and combine them into a single reference at runtime. The only rule I had was that I couldn’t touch the way stylesheets are referenced. In other words, it had to be done only by writing C# and leave the HTML alone.
That means I should be able to import multiple stylesheets the way I normally would like so:
<head runat="server">
<link rel="stylesheet" type="text/css" href="~/css/master.css" />
<link rel="stylesheet" type="text/css" href="~/css/menu.css" />
</head>
Luckily, if the head tag has a runat=”server” attribute, all stylesheets are treated as HtmlControls we can reference in the code-behind. That's a prerequisite for this to work. What I needed was to things:
- Code to remove the stylesheets from the head tag
- An HttpHandler to combine all the reference stylesheets
The code
The following code removes all stylesheets from the head tag and adds a new one that points to the HttpHandler and passes the original stylesheet file names as URL parameters. It looks like this:
<link rel="stylesheet" type="text/css" href="~/stylesheet.ashx?stylesheets=~css/master.css,~/css/menu.css" />
Note that the file names are being URL encoded but that looked too messy for this example, so I just leaved them in clear text so it's easier to see what's going on. I use a custom base page so I put the following code in there. You could also place it in your master page or in every .aspx page you want this feature.
[code:c#]
protected void Page_PreRender(object sender, EventArgs e)
{
CombineCss();
}
protected virtual void CombineCss()
{
Collection<HtmlControl> stylesheets = new Collection<HtmlControl>();
foreach (Control control in Page.Header.Controls)
{
HtmlControl c = control as HtmlControl;
if (c != null && c.Attributes["rel"] != null && c.Attributes["rel"].Equals("stylesheet", StringComparison.OrdinalIgnoreCase))
{
if (!c.Attributes["href"].StartsWith("http://"))
stylesheets.Add(c);
}
}
string[] paths = new string[stylesheets.Count];
for (int i = 0; i < stylesheets.Count; i++)
{
Page.Header.Controls.Remove(stylesheets[i]);
paths[i] = stylesheets[i].Attributes["href"];
}
AddStylesheetsToHeader(paths);
}
private void AddStylesheetsToHeader(string[] paths)
{
HtmlLink link = new HtmlLink();
link.Attributes["rel"] = "stylesheet";
link.Attributes["type"] = "text/css";
link.Href = "~/stylesheet.ashx?stylesheets=" + Server.UrlEncode(string.Join(",", paths));
Page.Header.Controls.Add(link);
}
[/code]
The HttpHandler
I’ve created an .ashx file that takes all the stylesheet references as a URL parameter separated by commas. It then iterates through them all, reads the .css file from disk, removes all whitespace and writes it to the response stream.
There is a serious IO overhead in this, so the HttpHandler caches the final response server-side so IO operations only take place the first time it is requested. It also adds a cache file dependency which means that whenever you change one of the stylesheet files, it reloads the cache. It also makes sure that the browsers will correctly cache the combined stylesheet by sending the correct cache headers.
Performance gains
I’ve done a test on two of my stylesheets I used for an old project. One is 7.11kb and the other is 20.2kb. That totals to 27.31kb and two HTTP requests. After I’ve implemented this feature I only have a single HTTP request and the total file size is only 13.08 because the whitespace is stripped. That’s more than half the size in kilobytes and just a single HTTP request was needed.
Implementation
Download the stylesheet.ashx file below and place it at the root of your application. If your website isn't located at the root then remember to update the references in the code above. You might also need to use the "~" when you reference the stylesheets in the head tag like you can see I did.