Using CDNs and Expires to Improve Web Site Performance

Much has been written on the advantages of using a  Content Delivery Network (CDN) to deliver static content ( jQuery, images, CSS files, etc.). (If you’re not familiar with CDNs, read ScottGu’s blog post Announcing the Microsoft AJAX CDN  and  Microsoft Ajax Content Delivery Network ) But the biggest advantages of CDNs are often overlooked:

  1. Many major sites use the Microsoft CDN, so there’s a good chance your customers browsers cache already contains jQuery from the CDN. If another site has loaded your jQuery version from the Microsoft CDN, and your web site requests that version from the Microsoft CDN, the client cache can satisfy the request, eliminating the download cost.
  2. Once content is loaded from the CDN, future requests don’t incur the round trip cost of checking with the server if the content is current. That is, the client avoids the HTTP 304.
  3. Server Farms don’t need to coordinate the ETag cache validator.

I’ll use the Fiddler tool  and my MVC Movie sample to  demonstrate the advantages of using a CDN.

Quick review of browser caching

There are two principal mechanisms to browser caching:

  1. Validate the requested resource in the browser cache is the same as on the server. If the resource is the same, the server can send back a HTTP 304 response, eliminating the need to send a response body with the content. Many people consider HTTP 304’s a good thing since they eliminate the need to send a full response body. I’ll show they are often an drag on performance, because the involve an unnecessary round trip to the server.
  2. Freshness: If the resource in the cache is fresh, pull the resource from the cache without checking with the server.

Browsers use a freshness heuristic to determine if they should validate a resource with the server or just pull it from the cache. If you clear your browser cache and then hit my Movie sample, several static resources will be downloaded to the client cache ( JavaScript, CSS and images). Using IE9, chrome and FireFox, you can hit the Movie web site for the next couple days and the browser will serve these files from the browser specific cache without even checking with the server. The browser will cache these static files without validating them with the server unless one of the following is true.

  1. The freshness heuristic is not met (that is, the file in the cache is not considered fresh).

  2. You have changed the expires header or other caching header.

  3. You have set the browser to disable caching..

  4. The URL to the resource changes or is different. For example, the following URLs are all pointing to the same modernizr script, but because each URL is different, each resource must be downloaded.

    modernizer

The first and last modernizer files were served by Cassini (The default Visual Studio Web Server) or IIS Express, that’s why we see the port number. The middle two were served by the local machines  IIS server, but using two different host names (localserver and the actual name, q1). You can examine the IE9 cache by selecting the Settings button under Browsing history on the General tab, then selecting View Files.

ViewCachedFiles

Each browser has its own cache, so FireFox won’t use files cached by Chrome or IE.

The following image shows a Fiddler session of browsing to my Movie site. Because I hadn’t browsed to the Movie site in several days, IE9 was forced to validate the modernizr and the custom jQuery file.

304s_

Selecting the Caching Tab in Fiddler gives the details on why validation was necessary and how much longer browsers will server these files directly without server validation. In the example below, for the next 2 days, 19 hours and 20 minutes, IE9 will pull the resource directly from the cache without checking with the server (and saving the HTTP 304 round trip). The Caching Inspector in Fiddler will show you when a response expires, based on the headers provided on that response. For instance, here’s the default response from IIS 7.5 which contains an ETAG and Last-Modified header, but no expiration information:

cachingTab

The No Explicit HTTP Expiration information was provided message is a good hint of what you need to do, explicitly set the expiration. Best practices recommend that web developers should specify an explicit expiration time for their content several years out in order to ensure that the browser is able to reuse the content without making conditional HTTP requests to revalidate the content with the server.  If the resource changes, change the name of the resource.  The following markup shows the contents of a Web.config file added to the Content and Scripts folders.

 <?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <staticContent>
             <clientCache cacheControlMode="UseExpires" 
                          httpExpires="Mon, 06 May 2013 00:00:00 GMT" />
        </staticContent>
    </system.webServer>
</configuration>

This sets the Expires Header out a couple years. Using ^R in the IE9 F-12 developer tools, clear the cache, then browse to the Movie site. Using Fiddler we can see the Expires Header will allow IE9 to serve this file directly from the cache without checking with the server for the next two years.

ExpiresHeader

 

Using Fiddler and the IE9 F12 Developer tools to monitor browser requests.

  1. From IE9, hit F12 to start the F12 developer tools, then clear the cache.

    ClearBrowserCache

     

  2. Start Fiddler.

  3. In the IE9 F12 tools, select the Network tab, then select Start capturing. Select Home or About in the Movie application.

    f12

Fiddler correctly shows IE did not make a conditional requests for the static resources, that is, there are no HTTP GET requests from IE and no HTTP 304 responses from the server. Why is IE9 showing GET requests made by IE9 and the server returning 304’s? Eric Lawrence  explains why 13 minutes into his presentation  Debugging with Fiddler.  It is difficult, at the level that the F12 Network Monitor is installed, to determine whether a given “from cache” response was “PreNetIO” (e.g. fresh in the local cache) or “PostNetIO” (e.g. in the local cache but a conditional HTTP request was used to validate freshness). Hence, sometimes F12 will show misleading “(304)”s when it meant “(cache)”.

Firebug is actually worse, showing expensive HTTP 200 results for each of the static resources.

FireBug

Chrome developer tools correctly show each resource coming from the cache.

Chrome

A Simple Helper to Load Resources in ASP.NET MVC Projects

The following code shows  the layout file in my modified MVC Movie project which uses the LoadRes helper to load static and CDN resources.

 <!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8" />
    <title>@ViewBag.Title</title>

    @LoadRes("Site.css")

    @LoadRes("https://ajax.aspnetcdn.com/ajax/jquery.ui/1.8.11/themes/redmond/jquery-ui.css")
    @LoadRes("modernizr-1.7.min.js")
    @LoadRes("https://ajax.aspnetcdn.com/ajax/jQuery/jquery-1.5.1.min.js")
    @LoadRes("https://ajax.aspnetcdn.com/ajax/jquery.validate/1.5.5/jquery.validate.min.js")
    @LoadRes("https://ajax.aspnetcdn.com/ajax/mvc/3.0/jquery.validate.unobtrusive.min.js")
    @LoadRes("jquery-ui-1.8.11.custom.min.js")
</head>
<body>
    <div class="page">
        <header>
            <div id="title">
                <h1>MovieLT</h1>
            </div>
            <div id="logindisplay">
                No Login
            </div>
            <nav>
                <ul id="menu">
                    <li>@Html.ActionLink("Home", "Index", "Movies")</li>
                    <li>@Html.ActionLink("About", "SearchIndex", "Movies")</li>
                </ul>
            </nav>
        </header>
        <section id="main">
            @RenderBody()
        </section>
        <footer>
        </footer>
    </div>
</body>
</html>

@helper LoadRes(string sFile) {    
    
    // Not CDN but JavaScript
    if (!sFile.Contains("https://") && sFile.EndsWith(".js")) {        
    <script src="@Url.Content("~/Scripts/" + sFile)" type="text/javascript"></script>
    }

     // CDN and  JavaScript
    else if (sFile.Contains("https://") && sFile.EndsWith(".js")) {        
    <script src="@sFile" type="text/javascript"></script>
    
    }

    // CDN and CSS
     else if (sFile.Contains("https://") && sFile.EndsWith(".css")) {        
    <link href="@sFile" rel="stylesheet" type="text/css" />
    
     // Not CDN but CSS
    } else if (sFile.EndsWith(".css")) {
    <link href="@Url.Content("~/Content/" + sFile)" rel="stylesheet" type="text/css" />
    }
        
}

I use my LoadRes helper to clean up the markup used to load resources. 

Special thanks to Erick Lawrencefor answering questions. Much of the information came from his blog.

Good Links