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:

  1. Code to remove the stylesheets from the head tag
  2. 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.

stylesheet.ashx (1.14 kb)

While hooking an HttpHandler up in web.config I thought about all the default HttpModules that are hooked up in machine.config. I did some research and found that the following modules are loaded into the ASP.NET pipeline by default:

[code:xml]

<httpModules>
  <add name="OutputCache" type="System.Web.Caching.OutputCacheModule" />
  <add name="Session" type="System.Web.SessionState.SessionStateModule" />
  <add name="WindowsAuthentication" type="System.Web.Security.WindowsAuthenticationModule" />
  <add name="FormsAuthentication" type="System.Web.Security.FormsAuthenticationModule" />
  <add name="PassportAuthentication" type="System.Web.Security.PassportAuthenticationModule" />
  <add name="RoleManager" type="System.Web.Security.RoleManagerModule" />
  <add name="UrlAuthorization" type="System.Web.Security.UrlAuthorizationModule" />
  <add name="FileAuthorization" type="System.Web.Security.FileAuthorizationModule" />
  <add name="AnonymousIdentification" type="System.Web.Security.AnonymousIdentificationModule" />
  <add name="Profile" type="System.Web.Profile.ProfileModule" />
</httpModules>

[/code]

If you don't use all of them, then why not remove them from the pipeline? This is a great question that I immediately had to find the answer to, but after a while I gave up finding anything on the web. Nobody has written about it. What I really wanted to know was how this will affect the performance of the application. The logical conclusion will be that it would boost the performance to have fewer modules in the pipeline, but I want to know how much. It should also reduce the attack surface.

The next question that needs an answer is which of the modules are safe to remove. I found that the UrlAuthorization and FileAuthorization modules act as a safeguard for security reasons, so they must stay. The three authentication modules can be removed if you don’t use them or at least the ones you don’t use. The rest can safely be removed if you don’t need them.

You can remove the modules you don’t need in the web.config like so:

[code:xml]

<httpModules>
  <remove name="PassportAuthentication" />
  <remove name="Profile" />
  <remove name="AnonymousIdentification" />
</httpModules>

[/code]

If you know about the performance impact involved, please let me know.

UPDATE: Scott Guthrie says

This morning I checked my mail and saw one from Joe Kaiser. He had asked Scott Guthrie about this and here is his reply:

In general you can get some very small performance wins using this approach - although I'd probably recommend not doing it.  The reason is that some features of ASP.NET (forms auth, roles, caching, etc) will of course stop working once you remove the modules they depend on. Trying to figure out why this has happened can often be confusing.

So there you have it. Small performance gains but you might be confused later on.