Enhancing performance with 1 stylesheet and a custom handler

Including a lot of files in your website can impact the performance of your site. Your browser needs to request all those files from the webserver(s) and download them individually. Luckily this fetching is pretty fast and your browser can do multiple requests at once. However, there is a maximum to the number of requests a browser can make, so if you include 100 external files, will probably be (relatively) slow.

I’ve tested this by creating a new MVC 3 web application, copying the Site.css file 12 times and include all of them in the head-element of the page. Below you can see the FireBug and YSlow reports for this test page.

image

image

I’ve pressed the Refresh-button several times and came to the conclusion each individual file has a loading time between 5ms and 25ms.

Even though 13x25ms still is pretty fast, you probably understand it’s better to minimize the number of requests, because each request has some overhead and some have to wait for the other to be completed.

To minimize the number of files which need to be included in a website, devigners often create one huge CSS file and one huge JavaScript file which contains everything needed for the website to work. This way the browser only needs to make 3 requests to load the page, the HTML, the CSS and the JavaScript. An additional request will be made for the JavaScript framework you are using (if any) and some more additional requests will be made to fetch the images of the page you are loading.

To test if including 1 big file really is faster, I’ve tested it again with FireBug and YSlow. For this test, I’ve concatenated the contents of theSite.css file 13 times in the SiteFull.css file, so the total styling file size will be the same as with 13 independent files.

image

image

As you can see in these results, loading a single CSS-file will only take about 2ms to 19ms. Now let’s say my development computer was super-fast while testing with the SiteFull.css file, even if fetching the file would take up twice as much time, it would still be faster as 13x25ms or even 13x5ms.

The conclusion to this test is: Bundling all styling in 1 huge file will give much faster loading times as separating them in several smaller files.

This conclusion is widely spread and most devigners already know about it, so it would be obsolete for me to point it out again. The thing which bothered me about this approach is the usage of mobile devices and/or having low bandwidth.

Putting all styling for your complete website is a waste of bandwidth if the user will only check out 1 or 2 pages. This user probably doesn’t need most of the style sheet, yet he has downloaded it. Loading the full set of styling on your device is probably useful when caching it locally (or on a proxy server), but if you don’t need it, why load it anyway?

Because I wanted to test if loading style sheets (and maybe JavaScript files) can be done smarter, I’ve created a possible solution for this. I’ve introduced a new HttpHandler, called CSSXHandler. This CSSXHandler will handle all requests which have the cssx-extension.

The implementation is fairly simple. You make a request to let’s say the homepage.cssx file. The handler will pick up this request, load the necessary CSS files in memory and output the necessary contents for this request.

The initial implementation for this handler looks like this.

public void ProcessRequest(HttpContext context)
{
    var currentStylesheet = DetermineRequestedStylesheet(context);
    switch (currentStylesheet.ToLower())
    {
        case "homepage":
            GenerateHomepageStylesheet(context);
            break;
    }
}
private string DetermineRequestedStylesheet(HttpContext context)
{
    int locationOfLastSlash = context.Request.RawUrl.LastIndexOf("/");
    int locationOfExtension = context.Request.RawUrl.LastIndexOf(".cssx");
    int numberOfCharactersBetweenSlashAndExtension = locationOfExtension - locationOfLastSlash;
    return context.Request.RawUrl.Substring(locationOfLastSlash + 1, numberOfCharactersBetweenSlashAndExtension - 1);
}
private void GenerateHomepageStylesheet(HttpContext context)
{
    var fullCssFileStream = new System.IO.StreamReader(context.Server.MapPath("~/Content/SiteFull.css"));
    string fullCssFileBody = fullCssFileStream.ReadToEnd();
    fullCssFileStream.Close();
    context.Response.ClearHeaders();
    context.Response.AddHeader("Pragma", "no-cache");
    context.Response.AddHeader("Content-Type", "text/css");
    context.Response.Write(fullCssFileBody);
}

As you can see, I’m loading the SiteFull.css file and read the contents in a string and output it in a whole. This isn’t production ready code and needs improvement if you want to use it, but it’ll give you an idea how to set it up.

Because I wanted to test the performance penalty of this CSSXHandler compared to loading the single SiteFull.css file in the head, I’ve tested loading the page using this handler (with the homepage.cssx in the head).

image

image

I was pretty surprised by the results. Loading the homepage.cssx file was between 2ms and 9ms, never slower. Compared to loading the SiteFull.css file, which had a maximum loading time of 19ms, that’s almost 50% faster (max). I didn’t believe this at first, but after pressing the refresh button a couple of times more, I couldn’t get it past the 9ms.

Some time ago I’ve read somewhere that when having routing in place, the ASP.NET ISAPI filter(s) first handle static files and they are processed ‘correctly’ afterwards. Too bad I can’t find a decent source for it at the moment. But I figured this is probably the reason for the relative slow loading of the CSS file.

In the above scenario, the full contents of the CSS file are still returned to the browser. The reason for me to write this handler was to minimize the output, so only the necessary styling is returned. To accomplish this, I’ve altered the CSSXHandler a bit. Now it looks like this:

public void ProcessRequest(HttpContext context)
{
    var currentStylesheet = DetermineRequestedStylesheet(context);
    switch (currentStylesheet.ToLower())
    {
        case "homepage":
            GenerateHomepageStylesheetWithOnlyStuffForTheHomePage(context, currentStylesheet);
            break;
    }
}
private string DetermineRequestedStylesheet(HttpContext context)
{
    int locationOfLastSlash = context.Request.RawUrl.LastIndexOf("/");
    int locationOfExtension = context.Request.RawUrl.LastIndexOf(".cssx");
    int numberOfCharactersBetweenSlashAndExtension = locationOfExtension - locationOfLastSlash;
    return context.Request.RawUrl.Substring(locationOfLastSlash + 1, numberOfCharactersBetweenSlashAndExtension - 1);
}
private void GenerateHomepageStylesheetWithOnlyStuffForTheHomePage(HttpContext context, string currentStyleSheet)
{
    var fullCssFileStream = new System.IO.StreamReader(context.Server.MapPath("~/Content/SiteFullCSSX.css"));
    string fullCssFileBody = fullCssFileStream.ReadToEnd();
    fullCssFileStream.Close();
    context.Response.ClearHeaders();
    context.Response.AddHeader("Pragma", "no-cache");
    context.Response.AddHeader("Content-Type", "text/css");
    // Start of a region in the CSS file: /* REGION homepage */
    // End of a region in the CSS file: /* ENDREGION */
    string patternForMatchingRegionForCurrentStyleSheet = @"\/\* REGION "+ currentStyleSheet + @" \*\/(.*?)\/\* ENDREGION \*\/";
    var matchingRegion = new Regex(patternForMatchingRegionForCurrentStyleSheet, RegexOptions.Singleline);
    var matches = matchingRegion.Matches(fullCssFileBody);
    foreach (Match match in matches)
    {
        context.Response.Write(match.Groups[1].Value);
    }
}

To be able to output only the styling which is necessary for the homepage, there needs to be something in place which tells us what this is. For this I’ve chosen to implement regions in the style sheet, like below.

/* REGION homepage */

/* All necessary styling for the homepage */

/* ENDREGION */

All styling necessary for the homepage needs to be implemented in these blocks (there can be multiple blocks in the CSS file(s).

The regular expression will search for these blocks and output the contents of it. By implementing this technique you will only output the styling which is needed for the specific page.

Of course I’ve tested this implementation also, below are the FireBug and YSlow reports.

image

image

The performance is fairly similar to outputting the complete SiteFull.css file via the handler, so there isn’t much of a penalty for using the regular expression. However, if you look closely, the file size is much smaller!

The above test scenario only contained 1 match for the regular expression. I wanted to see what the performance was when I added a lot more matches in the big file and also tried out a different notation to support multiple style sheets with the same block.

/* REGION homepage, about, portfolio, blog */

/* ENDREGION */

Using this and having several other matches in the file resulted in the following statistics.

image

image

The result still is fairly similar to the first CSSX test (between 2ms and 9ms). This means you can generate a really dynamic CSS file, load it via the CSSX handler and get a really fast and small style sheet in your page.

All of these tests are performed on my local machine. The specified code isn’t meant for production environments, as it would need some adjustments. Also, I’ve only refreshed the pages about 10 times on my local development machine. If you want to use this in a production environment, consider the consequences before you do. Caching is a lot harder, you don’t have 1 single file in the cache, but multiple. Also, I don’t know if a proxy or browser would cache a file with the cssx extension. It would be wise to load-test this solution first.

At least I proved it’s possible to generate really dynamic style sheets which load fast and have the smallest possible filesize. If you want to try this handler out yourself, or have improvements, I’ve placed the solution on BitBucket as a public repository.

Would love to hear what you think about this and if it’s usable in the real world? I hope to try it out soon on a new website of mine.


Share

comments powered by Disqus