|
|||||||||||||||||||||||||||||||
|
|||||||||||||||||||||||||||||||
|
Announcements
Want a new Job?
Chapters
Services
Feature Zones
|
IntroductionThere are a lot of ways to improve performance in web applications. One of the simplest but most effective methods is to cache images on the client. In this article I would like to show how we implemented image caching for our DotNetNuke website. The ProblemWhen I was building the website http://www.software-architects.com I used a lot of images in the css style sheets to display background images for menu items. After transfering the files to our web server I tested how much traffic a request to our start page would produce with Microsoft Network Monitor. This is a Tool to allow capturing and protocol analysis of network traffic. You can download it from the Microsoft Download Center. With Microsoft Network Monitor 3.1 I recorded a call to http://www.software-architects.com. As a result I got 20 requests to 20 different files to display one single page. Microsoft Network Monitor shows that appoximatly half of the requests are required for the menu images.
There are two different ways to avoid this problem. On the one hand you can tell IIS to cache images on the client, and on the other hand you can do this directly in ASP.net (which is a bit more complicated). Caching Images in IISCaching in IIS is very simple. Select a folder in the left pane or a single file in the right pane and open the properties dialog.
Check "Enable content expiration" and choose when your content should expire.
That's it! IIS tells the client with the "Cache-Control" header that the content may be cached on the client. The "Expires" header contains the expiration date. So the client knows that after this date it has to ask the server for the new content. This approach works very well if
Both conditions are not fulfilled in our case. In our DotNetNuke project images are spread accross multiple folders so it would be quite complex to configure IIS. And more important, our hosting provider does not give us access to IIS. Thus I had to look for another solution. Caching Images with a Custom HttpHandlerFirst thing I had to solve was to bypass IIS to get the request to ASP.NET. I deciced to write a custom http handler which listens to files with the paths *.gif.ashx, *.jpg.ashx and *.png.ashx. You can find a good article about I built a new class library project in Visual Studio with a class
namespace SoftwareArchitects.Web
{
public class CachingHandler : IHttpHandler
{
public bool IsReusable
{
get { return true; }
}
public void ProcessRequest(HttpContext context)
{
...
}
}
}
We want our http handler to send a file to the client. As we are listening to files with the paths *.gif.ashx, *.jpg.ashx and *.png.ashx, all we have to do is to remove the ".ashx" from the request path to get the file we want to send to the client. Besides we extract the filename and the extension from the file. public void ProcessRequest(HttpContext context)
{
string file = context.Server.MapPath
(context.Request.FilePath.Replace(".ashx", ""));
string filename = file.Substring(file.LastIndexOf('\\') + 1);
string extension = file.Substring(file.LastIndexOf('.') + 1);
In the next step we load the configuration for the CachingHandler from the web.config file. Therfore I built a class CachingSection (which I will show a bit later), which contains a property CachingTimeSpan and a collection FileExtensions, which knows the content type for each file extension. With help of this config class we configure the HttpCachePolicy object of the response:
CachingSection config = (CachingSection)context.GetSection
("SoftwareArchitects/Caching");
if (config != null)
{
context.Response.Cache.SetExpires
(DateTime.Now.Add(config.CachingTimeSpan));
context.Response.Cache.SetCacheability(HttpCacheability.Public);
context.Response.Cache.SetValidUntilExpires(false);
FileExtension fileExtension = config.FileExtensions[extension];
if (fileExtension != null)
{
context.Response.ContentType = fileExtension.ContentType;
}
}
Finally we add the content-disposition header to the response to tell the client that it should open the file in the browser (inline). Additionally we set the filename to the name without the extension .ashx, because this is the name, that will be displayed when you try to download the file. Then we use context.Response.AddHeader("content-disposition",
"inline; filename=" + filename);
context.Response.WriteFile(file);
}
Defining Custom Configuration Sections in web.configIn the http handler we used a custom class to read some configuration information from the web.config file. Therfore I built the class namespace SoftwareArchitects.Web.Configuration
{
/// <summary>
/// Configuration for caching
/// </summary>
public class CachingSection : ConfigurationSection
{
[ConfigurationProperty("CachingTimeSpan", IsRequired = true)]
public TimeSpan CachingTimeSpan
{
get { return (TimeSpan)base["CachingTimeSpan"]; }
set { base["CachingTimeSpan"] = value; }
}
[ConfigurationProperty("FileExtensions", IsDefaultCollection = true,
IsRequired = true)]
public FileExtensionCollection FileExtensions
{
get { return ((FileExtensionCollection)base["FileExtensions"]); }
}
}
To support not only single values but also collections we have to implement a class derived from You can download the complete code for the file CachingSection.cs. /// <summary>
/// List of available file extensions
/// </summary>
public class FileExtensionCollection : ConfigurationElementCollection
{
...
}
Finally we need a class for each extension, which holds a property for the extension and a property for the content type. /// <summary>
/// Configuration for a file extension
/// </summary>
public class FileExtension : ConfigurationElement
{
[ConfigurationProperty("Extension", IsRequired = true)]
public string Extension
{
get { return (string)base["Extension"]; }
set { base["Extension"] = value.Replace(".", ""); }
}
[ConfigurationProperty("ContentType", IsRequired = true)]
public string ContentType
{
get { return (string)base["ContentType"]; }
set { base["ContentType"] = value; }
}
}
}
All we have to do now is to add a configuration section to our web.config. In the <configuration>
<configSections>
<sectionGroup name="SoftwareArchitects">
<section name="Caching" requirePermission="false"
type="SoftwareArchitects.Web.Configuration.CachingSection,
SoftwareArchitects.Web.CachingHandler" />
</sectionGroup>
</configSections>
<SoftwareArchitects>
<Caching CachingTimeSpan="1">
<FileExtensions>
<add Extension="gif" ContentType="image\gif" />
<add Extension="jpg" ContentType="image\jpeg" />
<add Extension="png" ContentType="image\png" />
</FileExtensions>
</Caching>
</SoftwareArchitects>
...
Now there is only one last thing missing until we can use the <httpHandlers>
<add verb="*" path="*.gif.ashx"
type="SoftwareArchitects.Web.CachingHandler,
SoftwareArchitects.Web.CachingHandler"/>
<add verb="*" path="*.jpg.ashx"
type="SoftwareArchitects.Web.CachingHandler,
SoftwareArchitects.Web.CachingHandler"/>
<add verb="*" path="*.png.ashx"
type="SoftwareArchitects.Web.CachingHandler,
SoftwareArchitects.Web.CachingHandler"/>
</httpHandlers>
</configuration>
You could also use other file extensions like *.gifx. But to do so you need to have access to IIS to configure the new extension to be handeled by the aspnet_isapi.dll. As I do not have access to the IIS of our hosting provider, I had to use *.ashx, because it is already mapped to aspnet_isapi.dll. Finally I added the extension .ashx to all images in the web site (in .css files and .aspx files). When I monitored a request to the main page of http://www.software-architects.com again, the first request still generated 20 requests to the web server but from the second request on it took only 7 requests to load the page, because the images were cached on the client.
You can see how it works on our website at http://www.software-architects.at/TechnicalArticles/CachinginASPNET/tabid/75/language/de-AT/Default.aspx. Right-click on an image and open the properties dialog. You will see, that the URL ends with .ashx. When you right-click on an image and select "Save Picture as..." the suggested filename does not include the extension .ashx because of the content-disposition header. Of course you can use the handler for other file types like javascript files of css files, too. So you could reduce the number of requests again. Testing the CachingHandlerYou can easily test the caching of images with a simple web site. I added a web site project with the name <img src="/Portals/1/App_Themes/Standard/Images/LogoWithMenuBackground.png.ashx" />
On the other hand the web site contains a theme Standard with a stylesheet Style.css. In the stylesheet I use a background image. Again the image source ends with .ashx. body
{
margin: 0px;
padding: 0px;
background-image: url(Images/MenuBackground.png.ashx);
background-repeat: repeat-x;
}
In the web.config of the web site I inserted a custom section to configure the <trace enabled="true" pageOutput="false" requestLimit="50" mostRecent="true" />
When I start my web site project I see the Default.aspx page with the logo, which is defined in Default.aspx, and with the background image, which is defined in the stylesheet.
To view the trace I opened a new tab in IE and replaced Default.aspx with Trace.axd in the URL. The trace shows that four request were necessary to display the page Default.aspx. For the first request and every time the users hits F5 all files are sent to the client.
When I switch back to the first tab I have three possibilities to reload the page. I could
Pressing F5 would reload all the content, whereas clicking the link or the button would only reload content, which is not cached on the client. I clicked the link and the button for the following screenshot. As you can see there were only requests to Default.aspx and Style.css added in the trace.
|
||||||||||||||||||||||||||||||