Faster EPiServer sites – client side performance

Posted on October 9, 2011 by Frederik Vig in ASP.NET, EPiServer

First part of a new series where I’m going to focus on performance and scaling. I’m going to go through all the best practices we as EPiServer developers should know about, not only to create EPiServer sites that are fast, but ultra-fast.

I’m going to use the Overlook hotel sample site. The goal is of course to make it ultra-fast!

To follow along you should have the overlook site installed and setup. Let’s get started!

Client side performance

I’m using Firefox as my main developer browser, I’ll also be using three great addons for Firefox: Firebug, YSlow, and Page Speed.

If we run YSlow on the front page of overlook hotel we’ll get a score of 66/100 (grade D) with a few suggestions for improvements.

YSlow - EPiServer Overlook Hotel demo site before

With Page Speed we get a score of 62/100.

Page Speed - EPiServer Overlook Hotel demo site before

I’m going to focus on IIS 7 and above, most of the stuff is also possible with IIS 6, but I’m not going to go through IIS 6.

Compress components with gzip

Read more about the benefits of gzipping your content here: Gzip Components.

In IIS 7 this is very easy. First make sure you have both static and dynamic compression installed.

Install Static and Dynamic compression features in IIS 7

Next step is enabling both static and dynamic compression in web.config. Go to the <system.webServer> section and add the following:

<urlCompression doDynamicCompression="true" doStaticCompression="true" dynamicCompressionBeforeCache="true" />

Re-run both YSlow and Page Speed. Your score should now be 72 and 78 with an A in gzip.

You can find more information on urlCompression on IIS.NET. By default IIS 7 will actually enable static and dynamic compression for you.

You might have spotted the httpCompression section under <system.webServer>.

<httpCompression>
  <staticTypes>
	<add mimeType="text/*" enabled="true" />
	<add mimeType="message/*" enabled="true" />
	<add mimeType="application/javascript" enabled="true" />
	<add mimeType="application/x-javascript" enabled="true" />
	<add mimeType="*/*" enabled="false" />
  </staticTypes>
  <dynamicTypes>
	<add mimeType="text/*" enabled="true" />
	<add mimeType="message/*" enabled="true" />
	<add mimeType="application/javascript" enabled="true" />
	<add mimeType="application/x-javascript" enabled="true" />
	<add mimeType="*/*" enabled="false" />
  </dynamicTypes>
  <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" />
</httpCompression>

You don’t actually need this section to enable gzip (IIS will just use it’s default settings), but it’s nice to know about it when you need to tweak your default gzip settings a bit. Basically urlCompression specifies what to compress and httpCompression specifies how. More information on httpCompression: HTTP Compression <httpCompression>.

If we check the applicationHost.config file for IIS 7 you can see the default settings (location: %windir%\system32\inetsrv\config\applicationHost.config):

<httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files">
	<scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" />
	<staticTypes>
		<add mimeType="text/*" enabled="true" />
		<add mimeType="message/*" enabled="true" />
		<add mimeType="application/x-javascript" enabled="true" />
		<add mimeType="application/atom+xml" enabled="true" />
		<add mimeType="application/xaml+xml" enabled="true" />
		<add mimeType="*/*" enabled="false" />
	</staticTypes>
	<dynamicTypes>
		<add mimeType="text/*" enabled="true" />
		<add mimeType="message/*" enabled="true" />
		<add mimeType="application/x-javascript" enabled="true" />
		<add mimeType="*/*" enabled="false" />
	</dynamicTypes>
</httpCompression>

By default httpCompression is set to only gzip files over 2700 bytes, we can fix this by adding the following code:

<httpCompression directory="%SystemDrive%\websites\_compressed" minFileSizeForComp="1024">
	<scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" />
	<staticTypes>
		<add mimeType="text/*" enabled="true" />
		<add mimeType="message/*" enabled="true" />
		<add mimeType="application/javascript" enabled="true" />
		<add mimeType="application/json" enabled="true" />
		<add mimeType="*/*" enabled="false" />
	</staticTypes>
</httpCompression>

Add Expires headers

Adding Expires headers to static and dynamic files like CSS, JavaScript and images tells the client to cache the files locally, lessening the burden on the server (less requests). Making your sites resources/components cache-able. More information: Add an Expires or a Cache-Control Header.

Back in our sites web.config file under the <system.webServer> section, we have the <staticContent> section:

<staticContent>
    <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="1.00:00:00"></clientCache>
</staticContent>

The interesting part here is of course the clientCache element.

UseMaxAge will add CacheControlMaxAge to the HTTP Headers sent to the client, we can then tell the client to cache the static files for 1 year:

<staticContent>
    <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00"/>
</staticContent>

With UseExpires you specify a date instead:

<staticContent>
    <clientCache cacheControlMode="UseExpires" httpExpires="Tue, 19 Jan 2038 03:14:07 GMT" />
</staticContent>

You can only use one of these (if you’re not using the location element) at a time. I usually use MaxAge, it’s simpler and you don’t have to remember to update your settings when the date passes.

The important thing to remember is that if you make any changes to the static files you’ll need to use another name. You can add a querystring, change the name, or in some other way update the path to reflect that the file has changed.

Let’s update it to say that the client should cache the files for 1 year:

<staticContent>
    <clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="365.00:00:00"/>
</staticContent>

This helped bring the list of components down from 86 to 15. But we’re not quite finished yet.

Down from 86 to 15 files in YSlow Add Expires headers

EPiServer uses the location element to specify access rights and to use EPiServer’s StaticFileHandler for files located in Global Files, Page Files, Documents and other places that use Virtual Path Providers.

<location path="PageFiles">
	<system.webServer>
		<handlers>
			<add name="webresources" path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" />
			<add name="wildcard" path="*" verb="*" type="EPiServer.Web.StaticFileHandler, EPiServer" />
		</handlers>
	</system.webServer>
	<staticFile expirationTime="365.00:00:00" />
</location>
<location path="Documents">
	<system.webServer>
		<handlers>
			<add name="webresources" path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" />
			<add name="wildcard" path="*" verb="*" type="EPiServer.Web.StaticFileHandler, EPiServer" />
		</handlers>
	</system.webServer>
	<staticFile expirationTime="365.00:00:00" />
</location>
<location path="Global">
	<system.webServer>
		<handlers>
			<add name="webresources" path="WebResource.axd" verb="GET" type="System.Web.Handlers.AssemblyResourceLoader" />
			<add name="wildcard" path="*" verb="*" type="EPiServer.Web.StaticFileHandler, EPiServer" />
		</handlers>
	</system.webServer>
	<staticFile expirationTime="365.00:00:00" />
</location>

More information in EPiServer’s SDK: Caching of static files. This technique uses the Expires header instead of MaxAge, but with the same effect.

You might have noticed the <caching> section:

<caching>
	<profiles>
		<add extension=".gif" policy="DontCache" kernelCachePolicy="CacheUntilChange" duration="0.00:01:00" location="Any" />
		<add extension=".png" policy="DontCache" kernelCachePolicy="CacheUntilChange" duration="0.00:01:00" location="Any" />
		<add extension=".js" policy="DontCache" kernelCachePolicy="CacheUntilChange" duration="0.00:01:00" location="Any" />
		<add extension=".css" policy="DontCache" kernelCachePolicy="CacheUntilChange" duration="0.00:01:00" location="Any" />
		<add extension=".jpg" policy="DontCache" kernelCachePolicy="CacheUntilChange" duration="0.00:01:00" location="Any" />
		<add extension=".jpeg" policy="DontCache" kernelCachePolicy="CacheUntilChange" duration="0.00:01:00" location="Any" />
	</profiles>
</caching>

The caching setting is used to configure output caching for IIS. You have both output caching for IIS and for ASP.NET. More information: Caching <caching>. By default IIS output caching is turned on.

What EPiServer has done is told IIS to not cache static files like images, CSS and JavaScript and let ASP.NET handle it instead. You can read more about IIS output caching here: Configure IIS 7 Output Caching.

Let’s remove the whole <caching> section and use the default settings instead.

Now we have a score of 78 points in YSlow and 95 in Page Speed. Here you can see how much of the total size is now cached:

YSlow showing the pages local cache

On the left we can see the total size and number of requests the first time, on the right is the size and number of requests after that. We’re now down to 3 requests and 16KB. Quite an improvement don’t you think?

Minify JavaScript and CSS

When we’re developing we usually work with multiple JavaScript and CSS files to make it easier to structure and maintain them. However in production we should take care to automatically compress and bundle the files.

It’s important to note the difference between the rule Gzip Components and Minify JavaScript and CSS, which will remove comments, unneeded whitespaces and replace variable/function names with shorter versions.

There are a few tools we can use for this:

We’re going to use ASP.NET 4.5 Optimization preview. You can easily install it through NuGet.

Note: you’ll need to run .NET 4, so we’ll need to upgrade our solution to use .NET 4 instead of .NET 3.5. This is quite easy thanks to David Knipe. Change the projects target framework in Visual Studio and install the EPiServerCMS6ToNetFour package from nuget.episerver.com.

After that you’ll be able to install ASP.NET 4.5 Optimization successfully.

ASP.NET 4.5 Optimization installation with NuGet in Visual Studio 2010

To enable it we just call the EnableDefaultBundles() method inside Application_Start in Global.asax:

public class Global : EPiServer.Global
{
	protected void Application_Start(object sender, EventArgs e)
	{
		BundleTable.Bundles.EnableDefaultBundles();
	}
}

Next step is updating the way external JavaScript and CSS files are included. By default, everything you refer to in a folder gets bundled together (only for the same type, so JavaScript and CSS files don’t get bundled together even if they’re in the same folder).

<link href="/Templates/DemoSite/Styles/Main/css" rel="stylesheet" type="text/css" />
<script src="/Templates/DemoSite/Scripts/Main/js"></script>

We add the path to the folder followed by either /css or /js.

I had to manually update the code-behind files of site.master and the composer functions and change:

Page.RegisterCSSFile("ComposerFunctions.Teaser.Styles.Teaser.css", UriSupport.ResolveUrlBySettings("/Templates/DemoSite/ComposerFunctions/Teaser/Styles/Teaser.css"));

To:

Page.RegisterCSSFile("ComposerFunctions.Teaser.Styles.Teaser.css", UriSupport.ResolveUrlBySettings("/Templates/DemoSite/ComposerFunctions/Teaser/Styles/css"));

What I did was bundle the different JavaScript and CSS files into folders like components/modules and updated their references to use these folders instead.

There were a few inline scripts that I didn’t compress, but overall this helped a lot. I have now 97/100 points in Page Speed and 79/100 in YSlow.

Make fewer HTTP requests

Modern browsers can send up-to 6 requests at a time to the same domain. Older browsers up-to 2. Combining resource files like JavaScript, CSS and images into as few files as possible not only decreases the sites total size, but also make it load much faster for the client.

For JavaScript and CSS files we can use the technique we learned in the previous step (Minify JavaScript and CSS). For images we can use CSS sprites or embed the images.

What we’re going to do is embed the CSS background images into the stylsheets. I’ve borrowed the code from Mads Kristensen’s presentation on Build – Optimize your website using ASP.NET and IIS8.

using System;
using System.IO;
using System.Text.RegularExpressions;
using System.Web;
using System.Web.Hosting;
using Microsoft.Web.Optimization;
 
namespace EPiServer
{
    public class CssWithImagesMinify : CssMinify
    {
        private static readonly Regex url = new Regex(@"url\((([^\)]*)\?embed)\)", RegexOptions.Singleline);
        private const string format = "url(data:image/{0};base64,{1})";
 
        public override void Process(BundleResponse bundle)
        {
            HttpContext.Current.Response.Cache.SetLastModifiedFromFileDependencies();
            base.Process(bundle);
            string reference = HttpContext.Current.Request.Path.Replace("/css", "/");
 
            // When publishing the bundler can be called from "/" causing image reference to be invalid.
            if (reference == "/")
                reference = "/Content/";
 
            foreach (Match match in url.Matches(bundle.Content))
            {
                var file = new FileInfo(HostingEnvironment.MapPath(reference + match.Groups[2].Value));
                if (file.Exists)
                {
                    string dataUri = GetDataUri(file);
                    bundle.Content = bundle.Content.Replace(match.Value, dataUri);
                    HttpContext.Current.Response.AddFileDependency(file.FullName);
                }
            }
        }
 
        private string GetDataUri(FileInfo file)
        {
            byte[] buffer = File.ReadAllBytes(file.FullName);
            string ext = file.Extension.Substring(1);
            return string.Format(format, ext, Convert.ToBase64String(buffer));
        }
    }
}

For all the images we want to embed we just add ?embed behind their name:

background: url('../Images/abuse.png?embed') no-repeat scroll 0 0 transparent;

We also need to update Global.asax and tell BundleTable to use our CSS Minifier instead.

protected void Application_Start(object sender, EventArgs e)
{
	var css = new DynamicFolderBundle("css", typeof(CssWithImagesMinify), "*.css");
	var js = new DynamicFolderBundle("js", typeof(JsMinify), "*.js");
 
	BundleTable.Bundles.Add(css);
	BundleTable.Bundles.Add(js);
}

The CSS files now have their background images embedded automatically!

background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA5IAAAAFCAYAAAA3+tN0AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAGFJREFUeNrs1zEKg1AQRdE/VgYN4uZcZDYXxEDSfd8W0gmeA5fpp3vVWpvSmB5pTs+0pLX3/moAAADcTlVtOe+0pyN90jf9Bu8BAADgH4YkAAAAhiQAAACGJAAAABdxCjAAa0EKFYXT/VsAAAAASUVORK5CYII=") repeat-y scroll 0 0 transparent;

There’s also a nice extension for Visual Studio that let’s right-click a folder inside Visual Studio and optimize all the images inside that folder: Image Optimizer.

I now have 99/100 in Page Speed and 95/100 in YSlow.

Further improvements

Resources

Related Posts: