In this post we describe a step-by-step procedure that enables client-side caching for static content files, with the ability to force a refresh of the client cache before its expiry, as needed.
We'll apply the procedure to the ~/App_Themes folder as all Sitefinity installations make use of themes, and the majority of requests are for stylesheets, and related images located in the theme folder(s).
Caching static content files on the client-side is a sure way to improve the load time of a webpage, as experienced by the end-user, however often overlooked. More than 80% of the time it takes the client browser to render a webpage is spent requesting and eventually downloading external resources: stylesheets, images, javascript.
Below are the statistics for this page, as reported by YSlow, before and after enabling the functionality described in this article:
Before: 15 HTTP requests with a primed cache

After: 3 HTTP requests with a primed cache
By default, the request for a static file is handled by IIS in the following manner:
- Content expiration is not enabled: IIS writes to the response header a "last-modified" value that corresponds to the date-time the resource was last written to, and no cache-control headers. As a result, the client browser will always issue a request for this resource even though it's already available in the browser cache, and, if the resource hasn't changed, receive from IIS a response status 304 (Not Modified) that tells the browser it's okay to use the cached version. If the resource has changed it will be included in the response from IIS with an updated "last-modified" header value. This ensures the client browser uses the latest version of the resource file. However, for static content files that are part of the website design and rarely change - such as theme files - the overhead of all this HTTP request/response chatter for static content is not justified. All browsers limit the number of parallel requests (8 - 12 total, and fewer for the same hostname) so it's important to minimize the number of requests needed to load a page.
- Content expiration is enabled, with a future expiration date: in this case IIS maintains the response cache-control headers such as "max-age" that tells the client browser to use the cached version of the resource and not issue a new request until the cached version expires. As a result, no subsequent HTTP requests will be issued by the browser for all repeat visits (until the cached resource expires), resulting in a dramatic decrease in the number of HTTP requests needed to load a page that uses resources cached on the client-side. The major disadvantage in this case is the client browser will use the cached version of the resource until it expires event though an updated version might be available on the server. Also, there's no way to "notify" the client browser a cached resource should be refreshed before its expiry. This is the reason why many website admins will either avoid using client-side caching, or use a small expiration interval (hours). Others would modify the build / production promotion process to use different file names for different versions - not feasible for a standard implementation using Sitefinity CMS.
What's Needed:
- Access to the IIS server hosting your website;
- Access to the DNS record of your domain name.
Procedure Summary:
- DNS record: create a new A record for the subdomain used to access the static content files and point it to the same IP address as your domain, e.g. "themes", so that the hostname used to access the theme files is "themes.yourdomain.com";
- IIS server: create a new website, set host header values to the newly created subdomain (themes.yourdomain.com), and enable content expiration (use a far-future expiration date);
- IIS Server: create one or more virtual directories under the newly created website and point them to the same physical location: App_Themes folder. The virtual folder names should have a numeric part that can be easily increased as soon as a new version of the theme files needs to be pushed to the client, e.g "v001", "v002", etc.;
- Sitefinity CMS: add the "CSS Link Update" user control (created as described below) to all page templates and set its configuration values in your website configuration file. The user control, when enabled, updates the source URL for all links in the page header that point to a stylesheet located in ~/App_Themes directory by replacing the ~/App_Themes/ with the control configuration value maintained in "web.config" file, e.g "http://themes.yourdomain.com/v001".
"CSS Link Update" user control
Create a new user control named "CssLinkUpdate.ascx" in your user controls folder, e.g. "~/UserControls/Data_Universal/":
<%@ Control Language="C#" AutoEventWireup="true" CodeFile="CssLinkUpdate.ascx.cs" Inherits="UserControls_Data_Universal_CssLinkUpdate" %>
- CssLinkUpdate.ascx.cs file:
using System;
using System.Web.UI;
using System.Configuration;
using System.Web.UI.HtmlControls;
public partial class UserControls_Data_Universal_CssLinkUpdate : System.Web.UI.UserControl
{
protected void Page_Load(object sender, EventArgs e)
{
try
{
if (Convert.ToBoolean(ConfigurationManager.AppSettings["CssLinkUpdate_Enabled"]))
{
string updateFrom = ConfigurationManager.AppSettings["CssLinkUpdate_From"];
if(!updateFrom.EndsWith("/"))
updateFrom += "/";
if(!updateFrom.StartsWith("/"))
updateFrom = "/" + updateFrom;
string updateTo = ConfigurationManager.AppSettings["CssLinkUpdate_To"];
if(!updateTo.EndsWith("/"))
updateTo += "/";
foreach (Control headerControl in Page.Header.Controls)
{
if (headerControl.GetType() == typeof(HtmlLink))
{
HtmlLink headerLink = (HtmlLink)headerControl;
if (!String.IsNullOrEmpty(headerLink.Attributes["rel"]) && headerLink.Attributes["rel"].ToLower() == "stylesheet"
&& !String.IsNullOrEmpty(headerLink.Attributes["href"]))
{
string origHref = "/" + headerLink.Attributes["href"];
int origIndex = origHref.IndexOf(updateFrom);
if(origIndex >= 0)
headerLink.Attributes["href"] = updateTo + origHref.Substring(origIndex + updateFrom.Length);
}
}
}
}
}
catch (Exception ex)
{
Response.Write(ex.Message);
}
}
}
- Add configuration values in "web.config", section "appSettings":
<add key="CssLinkUpdate_Enabled" value="false" />
<add key="CssLinkUpdate_From" value="App_Themes/" />
<add key="CssLinkUpdate_To" value="http://themes.yourdomain.com/v001" />
where "CssLinkUpdate_Enabled" enables / disables the functionality described in this article by setting a value of true / false. Update the value of the "CssLinkUpdate_To" key to your theme-dedicated subdomain name.
- Add the "CSS Link Update" user control to the toolbox by adding the following element to "web.config", section "toolboxControls":
<add name="CSS Link Update" section="Data Universal" url="~/UserControls/Data_Universal/CssLinkUpdate.ascx" />
The user control is now available for use from the control toolbox (displayed when a page / template is in edit mode), section "Data Universal". Add the control to all page templates, no configuration needed, just drag-and-drop it in a content placeholder.
How to initiate an update
Here's how an updated version of a stylesheet located in App_Themes folder (or related images) can be pushed to all clients, including those that already have a cached version locally:
- Promote your updates to the ~/App_Themes folder as you'd normally do it;
- Update the virtual content URL in "web.config" file so that it points to a new virtual folder (e.g. from "http://themes.yourdomain.com/v001" to "http://themes.yourdomain.com/v002") and make sure the new virtual folder exists (if not, create one and point it to the same physical App_Themes directory);
- Save the "web.config" file (this will cause an application restart and clear any pages cached in the application memory; they will be replaced with a new version that uses the most recent stylesheet links, as configured).